diff --git a/src/Assets.php b/src/Assets.php index ed210510..8741f837 100644 --- a/src/Assets.php +++ b/src/Assets.php @@ -28,6 +28,55 @@ private function init() { add_action( 'wp_enqueue_scripts', [ $this, 'maybe_enqueue_search_queries_script' ], 11 ); } + /** + * Enqueue cloaked affiliate links assets if the option is enabled. + * + * @return void + */ + public function maybe_enqueue_cloaked_affiliate_links_assets() { + if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::CLOAKED_AFFILIATE_LINKS ) && Helpers::main_script_is_registered() ) { + wp_enqueue_script( + 'plausible-affiliate-links', + PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-affiliate-links.js', + [ 'plausible-analytics' ], + filemtime( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'assets/dist/js/plausible-affiliate-links.js' ), + ); + + $affiliate_links = Helpers::get_settings()['affiliate_links'] ?? []; + + wp_add_inline_script( 'plausible-affiliate-links', 'const plausibleAffiliateLinks = ' . wp_json_encode( $affiliate_links ) . ';', 'before' ); + } + } + + /** + * Enqueue 404 script if the option is enabled. + * + * @return void + */ + public function maybe_enqueue_four_o_four_script() { + $is_404 = apply_filters( 'plausible_analytics_is_404', is_404() ); + + if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::FOUR_O_FOUR ) && $is_404 && Helpers::main_script_is_registered() ) { + $data = wp_json_encode( + [ + 'props' => [ + 'path' => 'document.location.pathname', + ], + ] + ); + + /** + * document.location.pathname is a variable. @see wp_json_encode() doesn't allow passing variable, only strings. This fixes that. + */ + $data = str_replace( '"document.location.pathname"', 'document.location.pathname', $data ); + $event_name = EnhancedMeasurements::FOUR_O_FOUR; + + wp_add_inline_script( + 'plausible-analytics', + "document.addEventListener('DOMContentLoaded', () => { plausible( $event_name, $data ); });" + ); + } + } /** * Register main JS if this user should be tracked. @@ -40,11 +89,18 @@ private function init() { public function maybe_enqueue_main_script() { $settings = Helpers::get_settings(); $user_role = Helpers::get_user_role(); + $url = $this->get_js_url( true ); + + if ( ! $url ) { + echo '\n"; + + return; + } /** * This is a dummy script that will allow us to attach inline scripts further down the line. */ - wp_register_script( 'plausible-analytics', $this->get_js_url( true ), [], null, apply_filters( 'plausible_load_js_in_footer', false ) ); + wp_register_script( 'plausible-analytics', $url, [], null, apply_filters( 'plausible_load_js_in_footer', false ) ); /** * Bail if tracked_user_roles is empty (which means no roles should be tracked) or if the current role should not be tracked. @@ -85,63 +141,13 @@ protected function get_js_url( bool $local = false ) { return Helpers::get_js_url( $local ); } - /** - * Enqueue cloaked affiliate links assets if the option is enabled. - * - * @return void - */ - public function maybe_enqueue_cloaked_affiliate_links_assets() { - if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::CLOAKED_AFFILIATE_LINKS ) ) { - wp_enqueue_script( - 'plausible-affiliate-links', - PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-affiliate-links.js', - [ 'plausible-analytics' ], - filemtime( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'assets/dist/js/plausible-affiliate-links.js' ), - ); - - $affiliate_links = Helpers::get_settings()['affiliate_links'] ?? []; - - wp_add_inline_script( 'plausible-affiliate-links', 'const plausibleAffiliateLinks = ' . wp_json_encode( $affiliate_links ) . ';', 'before' ); - } - } - - /** - * Enqueue 404 script if the option is enabled. - * - * @return void - */ - public function maybe_enqueue_four_o_four_script() { - $is_404 = apply_filters( 'plausible_analytics_is_404', is_404() ); - - if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::FOUR_O_FOUR ) && $is_404 ) { - $data = wp_json_encode( - [ - 'props' => [ - 'path' => 'document.location.pathname', - ], - ] - ); - - /** - * document.location.pathname is a variable. @see wp_json_encode() doesn't allow passing variable, only strings. This fixes that. - */ - $data = str_replace( '"document.location.pathname"', 'document.location.pathname', $data ); - $event_name = EnhancedMeasurements::FOUR_O_FOUR; - - wp_add_inline_script( - 'plausible-analytics', - "document.addEventListener('DOMContentLoaded', () => { plausible( $event_name, $data ); });" - ); - } - } - /** * Enqueue Query Params script if the option is enabled. * * @return void */ public function maybe_enqueue_query_params_script() { - if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::QUERY_PARAMS ) ) { + if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::QUERY_PARAMS ) && Helpers::main_script_is_registered() ) { $query_params = Helpers::get_settings()['query_params'] ?? []; $props = []; @@ -176,7 +182,7 @@ public function maybe_enqueue_query_params_script() { public function maybe_enqueue_search_queries_script() { $is_search = apply_filters( 'plausible_analytics_is_search', is_search() ); - if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::SEARCH_QUERIES ) && $is_search ) { + if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::SEARCH_QUERIES ) && $is_search && Helpers::main_script_is_registered() ) { global $wp_query; $search_source = isset( $_REQUEST['search_source'] ) ? sanitize_text_field( $_REQUEST['search_source'] ) : wp_get_referer(); diff --git a/src/Cron.php b/src/Cron.php index eded4252..f1742c44 100644 --- a/src/Cron.php +++ b/src/Cron.php @@ -57,20 +57,26 @@ private function maybe_download() { $remote = Helpers::get_js_url(); $local = Helpers::get_js_path(); + if ( ! $remote || ! $local ) { + return false; + } + return $this->download_file( $remote, $local ); } /** * Downloads a remote file to this server. * - * @param string $local_file Absolute path to where to store the $remote_file. + * @since 1.3.0 + * * @param string $remote_file Full URL to file to download. * + * @param string $local_file Absolute path to where to store the $remote_file. + * * @return bool True when successful. False if it fails. - * @throws Exception * @throws InvalidArgument * - * @since 1.3.0 + * @throws Exception */ private function download_file( $remote_file, $local_file ) { $file_contents = wp_remote_get( $remote_file ); diff --git a/src/Helpers.php b/src/Helpers.php index edc647d8..257b4d8e 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -16,79 +16,30 @@ */ class Helpers { /** - * Get Analytics URL. + * Get entered Domain Name or provide an alternative if not entered. * - * @return string - * @throws Exception * @since 1.0.0 - * - */ - public static function get_js_url( $local = false ) { - $file_name = static::get_filename(); - - /** - * If the Avoid Ad Blockers option is enabled, return URL pointing to the local file. - */ - if ( $local && static::proxy_enabled() ) { - return esc_url( static::get_proxy_resource( 'cache_url' ) . $file_name . '.js' ); - } - - return esc_url( static::get_hosted_domain_url() . "/js/$file_name.js" ); - } - - /** - * Get filename (without file extension) - * + * @access public * @return string - * @throws Exception - * - * @codeCoverageIgnore - * @since 1.3.0 */ - public static function get_filename() { - $client = static::get_client(); + public static function get_domain() { + $settings = static::get_settings(); - if ( $client instanceof Client ) { - return $client->get_tracker_id(); + if ( ! empty( $settings['domain_name'] ) ) { + return $settings['domain_name']; } - return ''; - } - - /** - * Build the API client. - * - * @return false|Client - * - * @codeCoverageIgnore This seam's only function is to keep our code testable. - */ - protected static function get_client() { - $client = new ClientFactory(); - - return $client->build(); - } - - /** - * Is the proxy enabled? - * - * @param array $settings Allows passing a current settings object. - * - * @return bool - */ - public static function proxy_enabled( $settings = [] ) { - if ( empty( $settings ) ) { - $settings = static::get_settings(); - } + $url = home_url(); - return ! empty( $settings['proxy_enabled'] ) || isset( $_GET['plausible_proxy'] ); + return preg_replace( '/^http(s?):\/\/(www\.)?/i', '', $url ); } /** * Get Settings. * - * @return array * @since 1.0.0 * @access public + * @return array */ public static function get_settings() { $defaults = [ @@ -119,6 +70,60 @@ public static function get_settings() { return apply_filters( 'plausible_analytics_settings', wp_parse_args( $settings, $defaults ) ); } + /** + * Get Data API URL. + * + * @since 1.2.2 + * @access public + * @return string + * @throws Exception + */ + public static function get_endpoint_url() { + if ( static::proxy_enabled() ) { + // This will make sure the API endpoint is properly registered when we're testing. + $append = isset( $_GET['plausible_proxy'] ) ? '?plausible_proxy=1' : ''; + + return static::get_rest_endpoint() . $append; + } + + return esc_url( static::get_hosted_domain_url() . '/api/event' ); + } + + /** + * Is the proxy enabled? + * + * @param array $settings Allows passing a current settings object. + * + * @return bool + */ + public static function proxy_enabled( $settings = [] ) { + if ( empty( $settings ) ) { + $settings = static::get_settings(); + } + + return ! empty( $settings['proxy_enabled'] ) || isset( $_GET['plausible_proxy'] ); + } + + /** + * Returns the Proxy's REST endpoint. + * + * @return string + * @throws Exception + */ + public static function get_rest_endpoint( $abs_url = true ) { + $namespace = static::get_proxy_resource( 'namespace' ); + $base = static::get_proxy_resource( 'base' ); + $endpoint = static::get_proxy_resource( 'endpoint' ); + + $uri = "$namespace/v1/$base/$endpoint"; + + if ( $abs_url ) { + return get_rest_url( null, $uri ); + } + + return '/' . rest_get_url_prefix() . '/' . $uri; + } + /** * Get a proxy resource by name. * @@ -201,19 +206,6 @@ public static function get_hosted_domain_url() { return esc_url( 'https://plausible.io' ); } - /** - * @param $option_name - * @param $option_value - * - * @return void - */ - public static function update_setting( $option_name, $option_value ) { - $settings = static::get_settings(); - $settings[ $option_name ] = $option_value; - - update_option( 'plausible_analytics_settings', $settings ); - } - /** * A convenient way to retrieve the absolute path to the local JS file. Proxy should be enabled when this method is called! * @@ -221,73 +213,78 @@ public static function update_setting( $option_name, $option_value ) { * @throws Exception */ public static function get_js_path() { - return static::get_proxy_resource( 'cache_dir' ) . static::get_filename() . '.js'; + $filename = static::get_filename(); + + if ( empty( $filename ) ) { + return ''; // @codeCoverageIgnore + } + + return static::get_proxy_resource( 'cache_dir' ) . $filename . '.js'; } /** - * Get entered Domain Name or provide alternative if not entered. + * Get filename (without file extension) * + * @since 1.3.0 * @return string - * @since 1.0.0 - * @access public + * @throws Exception + * + * @codeCoverageIgnore */ - public static function get_domain() { - $settings = static::get_settings(); + public static function get_filename() { + $client = static::get_client(); - if ( ! empty( $settings['domain_name'] ) ) { - return $settings['domain_name']; + if ( $client instanceof Client ) { + return $client->get_tracker_id(); } - $url = home_url(); - - return preg_replace( '/^http(s?):\/\/(www\.)?/i', '', $url ); + return ''; } /** - * Get Data API URL. + * Build the API client. * - * @return string - * @throws Exception - * @since 1.2.2 - * @access public + * @return false|Client + * + * @codeCoverageIgnore This seam's only function is to keep our code testable. */ - public static function get_endpoint_url() { - if ( static::proxy_enabled() ) { - // This will make sure the API endpoint is properly registered when we're testing. - $append = isset( $_GET['plausible_proxy'] ) ? '?plausible_proxy=1' : ''; - - return static::get_rest_endpoint() . $append; - } + protected static function get_client() { + $client = new ClientFactory(); - return esc_url( static::get_hosted_domain_url() . '/api/event' ); + return $client->build(); } /** - * Returns the Proxy's REST endpoint. + * Get Analytics URL. Returns an empty string if @see self::get_filename() returns empty. + * + * @since 1.0.0 * * @return string * @throws Exception */ - public static function get_rest_endpoint( $abs_url = true ) { - $namespace = static::get_proxy_resource( 'namespace' ); - $base = static::get_proxy_resource( 'base' ); - $endpoint = static::get_proxy_resource( 'endpoint' ); + public static function get_js_url( $local = false ) { + $file_name = static::get_filename(); - $uri = "$namespace/v1/$base/$endpoint"; + if ( empty( $file_name ) ) { + return ''; // @codeCoverageIgnore + } - if ( $abs_url ) { - return get_rest_url( null, $uri ); + /** + * If the Avoid Ad Blockers option is enabled, return URL pointing to the local file. + */ + if ( $local && static::proxy_enabled() ) { + return esc_url( static::get_proxy_resource( 'cache_url' ) . $file_name . '.js' ); } - return '/' . rest_get_url_prefix() . '/' . $uri; + return esc_url( static::get_hosted_domain_url() . "/js/$file_name.js" ); } /** * Get user role for the logged-in user. * - * @return string * @since 1.3.0 * @access public + * @return string */ public static function get_user_role() { global $current_user; @@ -296,4 +293,26 @@ public static function get_user_role() { return array_shift( $user_roles ); } + + /** + * Checks if the main Plausible Analytics script is registered. + * + * @return bool + */ + public static function main_script_is_registered() { + return wp_script_is( 'plausible-analytics', 'registered' ); + } + + /** + * @param $option_name + * @param $option_value + * + * @return void + */ + public static function update_setting( $option_name, $option_value ) { + $settings = static::get_settings(); + $settings[ $option_name ] = $option_value; + + update_option( 'plausible_analytics_settings', $settings ); + } } diff --git a/src/Integrations/FormSubmit.php b/src/Integrations/FormSubmit.php index 96bc85cb..d7e46d5f 100644 --- a/src/Integrations/FormSubmit.php +++ b/src/Integrations/FormSubmit.php @@ -8,6 +8,7 @@ namespace Plausible\Analytics\WP\Integrations; +use Plausible\Analytics\WP\Helpers; use Plausible\Analytics\WP\Proxy; class FormSubmit { @@ -49,6 +50,10 @@ private function init() { * @codeCoverageIgnore because there's nothing to test here. */ public function add_js() { + if ( ! Helpers::main_script_is_registered() ) { + return; + } + wp_register_script( 'plausible-form-submit-integration', PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-form-submit-integration.js', @@ -71,7 +76,7 @@ public function add_js() { * @filter wpcf7_validate * * @param \WPCF7_Validation $result Form submission result object containing validation results. - * @param array $tags Array of tags associated with the form fields. + * @param array $tags Array of tags associated with the form fields. * * @return \WPCF7_Validation * diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 352e1612..e20c408c 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -77,6 +77,29 @@ private function init( $init ) { add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); } + /** + * A bit of a hacky approach to ensure the _wp_http_referer header is available to us when hitting the Proxy in @see self::track_remove_cart_item(). + * + * @see self::track_add_to_cart() + * + * @param $add_to_cart_data + * + * @param $request + * + * @return mixed + * + * @codeCoverageIgnore Because there's nothing to test here. + */ + public function add_http_referer( $add_to_cart_data, $request ) { + $http_referer = $request->get_param( '_wp_http_referer' ); + + if ( ! empty( $http_referer ) ) { + $_REQUEST['_wp_http_referer'] = sanitize_url( $http_referer ); + } + + return $add_to_cart_data; + } + /** * Enqueue required JS in frontend. * @@ -99,25 +122,22 @@ public function add_js() { } /** - * A bit of a hacky approach to ensure the _wp_http_referer header is available to us when hitting the Proxy in @param $add_to_cart_data - * - * @param $request + * Track (non-Interactivity API i.e., AJAX) add to cart events. * - * @return mixed + * @param string|int $product_id ID of the product added to the cart. * - * @codeCoverageIgnore Because there's nothing to test here. - * @see self::track_remove_cart_item(). + * @return void * - * @see self::track_add_to_cart() - * and/ - public function add_http_referer( $add_to_cart_data, $request ) { - $http_referer = $request->get_param( '_wp_http_referer' ); - - if ( ! empty( $http_referer ) ) { - $_REQUEST[ '_wp_http_referer' ] = sanitize_url( $http_referer ); - } + * @codeCoverageIgnore Because we can't test XHR requests here. + */ + public function track_ajax_add_to_cart( $product_id ) { + $product = wc_get_product( $product_id ); + $add_to_cart_data = [ + 'id' => $product_id, + 'quantity' => $_POST['quantity'] ?? 1, + ]; - return $add_to_cart_data; + $this->track_add_to_cart( $product, $add_to_cart_data ); } /** @@ -142,8 +162,8 @@ public function track_direct_add_to_cart() { /** * Track regular (i.e., interactivity API) add to cart events. * - * @param WC_Product $product General information about the product added to cart. - * @param array $add_to_cart_data Cart data for the product added to the cart, e.g. quantity, variation ID, etc. + * @param WC_Product $product General information about the product added to cart. + * @param array $add_to_cart_data Cart data for the product added to the cart, e.g. quantity, variation ID, etc. * * @return void * @@ -204,64 +224,6 @@ protected function get_wc_cart() { return WC()->cart; } - /** - * Track (non-Interactivity API i.e., AJAX) add to cart events. - * - * @param string|int $product_id ID of the product added to the cart. - * - * @return void - * - * @codeCoverageIgnore Because we can't test XHR requests here. - */ - public function track_ajax_add_to_cart( $product_id ) { - $product = wc_get_product( $product_id ); - $add_to_cart_data = [ - 'id' => $product_id, - 'quantity' => $_POST['quantity'] ?? 1, - ]; - - $this->track_add_to_cart( $product, $add_to_cart_data ); - } - - /** - * Track Remove from cart events. - * - * @param string $cart_item_key Key of item being removed from cart. - * @param WC_Cart $cart Instance of the current cart. - * - * @return void - * - * @codeCoverageIgnore because we can't test XHR requests here. - */ - public function track_remove_cart_item( $cart_item_key, $cart ) { - $cart_contents = $cart->get_cart_contents(); - $item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] ); - $product = null; - - if ( isset( $item_removed_from_cart['product_id'] ) ) { - $product = wc_get_product( $item_removed_from_cart['product_id'] ); - } - - if ( ! $product ) { - return; - } - - $props = apply_filters( - 'plausible_analytics_woocommerce_remove_cart_item_custom_properties', - [ - 'product_name' => $product->get_name(), - 'product_id' => $item_removed_from_cart['product_id'], - 'variation_id' => $item_removed_from_cart['variation_id'], - 'quantity' => $item_removed_from_cart['quantity'], - 'cart_total_items' => count( $cart_contents ), - 'cart_total' => $cart->get_total( null ), - ] - ); - $proxy = new Proxy( false ); - - $proxy->do_request( $this->event_goals['remove-from-cart'], null, null, $props ); - } - /** * Tracks when a user enters the checkout process and sends event data to Plausible Analytics. * @@ -313,7 +275,7 @@ public function track_purchase( $order_id ) { [ EnhancedMeasurements::ECOMMERCE_REVENUE => [ 'amount' => (string) $order->get_total(), - 'currency' => $order->get_currency() + 'currency' => $order->get_currency(), ], ] ); @@ -324,4 +286,43 @@ public function track_purchase( $order_id ) { $order->add_meta_data( Integrations::PURCHASE_TRACKED_META_KEY, true ); $order->save(); } + + /** + * Track Remove from cart events. + * + * @param string $cart_item_key Key of item being removed from cart. + * @param WC_Cart $cart Instance of the current cart. + * + * @return void + * + * @codeCoverageIgnore because we can't test XHR requests here. + */ + public function track_remove_cart_item( $cart_item_key, $cart ) { + $cart_contents = $cart->get_cart_contents(); + $item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] ); + $product = null; + + if ( isset( $item_removed_from_cart['product_id'] ) ) { + $product = wc_get_product( $item_removed_from_cart['product_id'] ); + } + + if ( ! $product ) { + return; + } + + $props = apply_filters( + 'plausible_analytics_woocommerce_remove_cart_item_custom_properties', + [ + 'product_name' => $product->get_name(), + 'product_id' => $item_removed_from_cart['product_id'], + 'variation_id' => $item_removed_from_cart['variation_id'], + 'quantity' => $item_removed_from_cart['quantity'], + 'cart_total_items' => count( $cart_contents ), + 'cart_total' => $cart->get_total( null ), + ] + ); + $proxy = new Proxy( false ); + + $proxy->do_request( $this->event_goals['remove-from-cart'], null, null, $props ); + } }