diff --git a/README.md b/README.md index 5f7f40a0..3fa2171b 100644 --- a/README.md +++ b/README.md @@ -4,37 +4,45 @@ [![Github CI](https://github.com/plausible/wordpress/actions/workflows/push.yml/badge.svg)](https://github.com/plausible/wordpress/actions/workflows/push.yml) ![WordPress version](https://img.shields.io/wordpress/plugin/v/plausible-analytics.svg) ![WordPress Rating](https://img.shields.io/wordpress/plugin/r/plausible-analytics.svg) ![WordPress Downloads](https://img.shields.io/wordpress/plugin/dt/plausible-analytics.svg) -Welcome to the Plausible Analytics WordPress Plugin GitHub repository. This is the code source and the center of active development. Here you can +Welcome to the Plausible Analytics WordPress Plugin GitHub repository. This is the code source and the center of active +development. Here you can browse the source, look at open issues, and contribute to the project. ## Getting Started If you're looking to contribute or actively develop on Plausible Analytics then skip ahead to -the [Local Development](https://github.com/plausible/wordpress/#local-development) section below. The following is if you're looking to actively use +the [Local Development](https://github.com/plausible/wordpress/#local-development) section below. The following is if +you're looking to actively use the plugin on your WordPress site. ### Minimum Requirements -* WordPress 5.3 or greater +* WordPress 5.9 or greater * PHP version 7.4 or greater * MySQL version 5.5 or greater ### Automatic installation -Automatic installation is the easiest option as WordPress handles the file transfers itself, and you don't need to leave your web browser. To do an -automatic installation of Plausible Analytics, log in to your WordPress dashboard, navigate to the Plugins menu and click "Add New". +Automatic installation is the easiest option as WordPress handles the file transfers itself, and you don't need to leave +your web browser. To do an +automatic installation of Plausible Analytics, log in to your WordPress dashboard, navigate to the Plugins menu and +click "Add New". -In the search field type "Plausible Analytics" and click Search Plugins. Once you have found the plugin you can view details about it such as the +In the search field type "Plausible Analytics" and click Search Plugins. Once you have found the plugin you can view +details about it such as the point release, rating and description. Most importantly of course, you can install it by simply clicking "Install Now". ### Manual installation -The manual installation method involves downloading our plugin and uploading it to your server via your favorite FTP application. The -WordPress codex contains [instructions on how to do this](https://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation). +The manual installation method involves downloading our plugin and uploading it to your server via your favorite FTP +application. The +WordPress codex +contains [instructions on how to do this](https://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation). ### Support -This repository is not suitable for support. Please don't use GitHub issues for support requests. To get support please use the following channels: +This repository is not suitable for support. Please don't use GitHub issues for support requests. To get support please +use the following channels: * [WP.org Support Forums](https://wordpress.org/support/plugin/plausible-analytics) - for all users @@ -45,25 +53,33 @@ This repository is not suitable for support. Please don't use GitHub issues for - `plausible_analytics_settings`: Allows you to modify and/or force values for each of the plugin's settings. - `plausible_load_js_in_footer`: Allows you to load the JS code snippet in the footer. - `plausible_analytics_script_params`: Allows you to modify the `script` element, loading the Plausible JS library. - - Example: using this filter and the `file-types` attribute will allow you to track downloads of certain file types when File Downloads tracking is + - Example: using this filter and the `file-types` attribute will allow you to track downloads of certain file types + when File Downloads tracking is enabled. -- `plausible_analytics_pageview_properties`: Allows you to add custom pageview properties when the Pageview Properties option is enabled under - Enhanced Measurements. For examples, read the [documentation on Pageview Properties](https://plausible.io/docs/custom-props/for-pageviews). +- `plausible_analytics_pageview_properties`: Allows you to add custom pageview properties when the Pageview Properties + option is enabled under + Enhanced Measurements. For examples, read + the [documentation on Pageview Properties](https://plausible.io/docs/custom-props/for-pageviews). ### Actions - `plausible_analytics_settings_saved`: Trigger additional tasks directly after settings are saved. -- `plausible_analytics_after_register_assets`: This action allows you to trigger additional tasks or add custom JS (e.g. events) to the tracking code. +- `plausible_analytics_after_register_assets`: This action allows you to trigger additional tasks or add custom JS (e.g. + events) to the tracking code. ### Toggles -Using constants, you can modify the behavior of the plugin. `wp-config.php` is the best place to define constants. If you're using a custom plugin, +Using constants, you can modify the behavior of the plugin. `wp-config.php` is the best place to define constants. If +you're using a custom plugin, make sure its code is loaded before this plugin. -- `PLAUSIBLE_SELF_HOSTED_DOMAIN`: Especially useful for Multisite instances using the self-hosted version of Plausible, this constant allows you to - specify the Self-Hosted Domain for all subsites at once. **IMPORTANT**: this constant takes precedence over the plugin's setting. So, if this +- `PLAUSIBLE_SELF_HOSTED_DOMAIN`: Especially useful for Multisite instances using the self-hosted version of Plausible, + this constant allows you to + specify the Self-Hosted Domain for all subsites at once. **IMPORTANT**: this constant takes precedence over the + plugin's setting. So, if this constant is defined, changing the setting won't have any effect. -- `plausible_proxy`: Appending this `GET`-parameter will force enable the proxy on the page you\'re calling it. This will allow you to test your proxy +- `plausible_proxy`: Appending this `GET`-parameter will force enable the proxy on the page you\'re calling it. This + will allow you to test your proxy in the frontend, before enabling the option. ## Local Development @@ -83,7 +99,8 @@ That's it. You're now ready to start development. Plausible Analytics relies on several npm commands to get you started: -* `npm run watch` - Live reloads JS and SASS files. Typically, you'll run this command before you start development. It's necessary to build the +* `npm run watch` - Live reloads JS and SASS files. Typically, you'll run this command before you start development. + It's necessary to build the JS/CSS however if you're working strictly within PHP it may not be necessary to run. * `npm run dev` - Runs a one time build for development. No production files are created. @@ -91,7 +108,8 @@ Plausible Analytics relies on several npm commands to get you started: ### Development Notes -* Ensure that you have `SCRIPT_DEBUG` enabled within your wp-config.php file. Here's a good example of wp-config.php for debugging: +* Ensure that you have `SCRIPT_DEBUG` enabled within your wp-config.php file. Here's a good example of wp-config.php for + debugging: ``` // Enable WP_DEBUG mode define( 'WP_DEBUG', true ); @@ -103,21 +121,27 @@ Plausible Analytics relies on several npm commands to get you started: define( 'SCRIPT_DEBUG', true ); ``` * Commit the `package.lock` file. Read more about why [here](https://docs.npmjs.com/files/package-lock.json). -* Your editor should recognize the `.eslintrc` and `.editorconfig` files within the Repo's root directory. Please only submit PRs following those +* Your editor should recognize the `.eslintrc` and `.editorconfig` files within the Repo's root directory. Please only + submit PRs following those coding style rulesets. ### Regenerating the OpenAPI PHP Client -This plugin uses a OpenAPI PHP Client which is autogenerated by the OpenAPI generator to reduce contract violations, etc. to a minimum. But, since -this is a WordPress plugin, some manual modifications need to be done to make sure it doesn't conflict with other plugins: +This plugin uses a OpenAPI PHP Client which is autogenerated by the OpenAPI generator to reduce contract violations, +etc. to a minimum. But, since +this is a WordPress plugin, some manual modifications need to be done to make sure it doesn't conflict with other +plugins: -- (Re)generate the PHP client using the following command (trigger it from the Plugin's root dir as output will be saved to `src/Client`): +- (Re)generate the PHP client using the following command (trigger it from the Plugin's root dir as output will be saved + to `src/Client`): `openapi-generator-cli generate -i https://plausible.io/api/plugins/spec/openapi -g php -o src/Client --additional-properties=identifierNamingConvention=snake_case,invokerPackage="Plausible\\Analytics\\WP\\Client" --global-property=apis,models,supportingFiles,modelDocs=false,modelTests=false,apiDocs=false,apiTests=false` - (When regenerating the PHP client this step can be skipped) Navigate to the `src/Client` director and install Composer dependencies: `composer install --no-dev` -- (When regenerating the PHP client this step can be skipped) Run `mozart compose` from the `src/Client` directory (Make sure Mozart is installed +- (When regenerating the PHP client this step can be skipped) Run `mozart compose` from the `src/Client` directory (Make + sure Mozart is installed globally) -- In the `src/Client/lib` directory, replace all occurrences of ` GuzzleHttp` (mind the space) with ` Plausible\Analytics\WP\Client\Lib\GuzzleHttp` ( +- In the `src/Client/lib` directory, replace all occurrences of ` GuzzleHttp` (mind the space) with + ` Plausible\Analytics\WP\Client\Lib\GuzzleHttp` ( again, mind the space at the beginning) - In the same directory, replace all occurrences of ` \GuzzleHttp` (mind the space and backslash) with ` \Plausible\Analytics\WP\Client\Lib\GuzzleHttp`. diff --git a/assets/src/js/admin/main.js b/assets/src/js/admin/main.js index 0b830f90..f3f36ab0 100644 --- a/assets/src/js/admin/main.js +++ b/assets/src/js/admin/main.js @@ -84,7 +84,7 @@ document.addEventListener('DOMContentLoaded', () => { * * @param e */ - toggleOption: function (e) { + toggleOption: async function (e) { /** * Make sure event target is a toggle. */ @@ -109,11 +109,13 @@ document.addEventListener('DOMContentLoaded', () => { // Toggle: off button.classList.replace('bg-indigo-600', 'bg-gray-200'); toggle.classList.replace('translate-x-5', 'translate-x-0'); + button.dataset.status = 'off'; toggleStatus = ''; } else { // Toggle: on button.classList.replace('bg-gray-200', 'bg-indigo-600'); toggle.classList.replace('translate-x-0', 'translate-x-5'); + button.dataset.status = 'on'; toggleStatus = 'on'; } @@ -126,7 +128,13 @@ document.addEventListener('DOMContentLoaded', () => { form.append('is_list', button.dataset.list); form.append('_nonce', plausible.nonce); - plausible.ajax(form); + let data = await plausible.ajax(form); + + if (data.capabilities === undefined) { + return; + } + + plausible.maybeDisableOptions(data.capabilities); }, /** @@ -160,6 +168,33 @@ document.addEventListener('DOMContentLoaded', () => { plausible.ajax(form, button); }, + /** + * Disable options based on the capabilities retrieved from the API. + * + * @param capabilities + */ + maybeDisableOptions: function (capabilities) { + let options = document.querySelectorAll('button[data-caps]'); + + options.forEach(function (option) { + let caps = option.dataset.caps.split(','); + let disable = false; + + caps.forEach(function (cap) { + if (capabilities[cap] === false) { + disable = true; + } + }); + + if (disable === true) { + // Trigger a click to make sure the option is disabled. + if (option.dataset.status === 'on') { + option.dispatchEvent(new Event('click', {bubbles: true})); + } + } + }); + }, + /** * Currently only validates the domain_name input, but can be used in the future for other custom input validations. * @@ -375,7 +410,8 @@ document.addEventListener('DOMContentLoaded', () => { } } - if (response.status === 200) { + // We still want the data, if it's a Payment Required error. + if (response.status === 200 || response.status === 402) { return response.json(); } @@ -478,6 +514,7 @@ document.addEventListener('DOMContentLoaded', () => { }, 2000); } }, + /** * Renders a HTML box containing additional information about the enabled option. * diff --git a/assets/src/js/admin/main.min.js b/assets/src/js/admin/main.min.js index b9929c61..79eb2d3e 100644 --- a/assets/src/js/admin/main.min.js +++ b/assets/src/js/admin/main.min.js @@ -1 +1 @@ -document.addEventListener("DOMContentLoaded",()=>{if(!document.location.href.includes("plausible_analytics")){return}let plausible={nonceElem:document.getElementById("_wpnonce"),nonce:"",showWizardElem:document.getElementById("show_wizard"),domainNameElem:document.getElementById("domain_name"),apiTokenElem:document.getElementById("api_token"),createAPITokenElems:document.getElementsByClassName("plausible-create-api-token"),buttonElems:document.getElementsByClassName("plausible-analytics-button"),stepElems:document.getElementsByClassName("plausible-analytics-wizard-next-step"),init:function(){if(document.location.hash===""&&document.getElementById("plausible-analytics-wizard")!==null){document.location.hash="#welcome_slide"}if(this.nonceElem!==null){this.nonce=this.nonceElem.value}this.toggleWizardStep();window.addEventListener("hashchange",this.toggleWizardStep);if(this.showWizardElem!==null){this.showWizardElem.addEventListener("click",this.showWizard)}if(this.domainNameElem!==null){this.domainNameElem.addEventListener("keyup",this.disableConnectButton)}if(this.apiTokenElem!==null){this.apiTokenElem.addEventListener("keyup",this.disableConnectButton)}if(this.createAPITokenElems.length>0){for(let i=0;i0){for(let i=0;i0){for(let i=0;i0){button.children[0].classList.remove("hidden")}button.setAttribute("disabled","disabled");plausible.ajax(form,button)},validateInput:function(input){if(input.name==="domain_name"&&input.value.match(/^(https?:\/\/)?(www.)?/).length>0){input.value=input.value.replace(/^(https?:\/\/)?(www.)?/,"")}return input},saveOptionOnNext:function(e){let hash=document.location.hash.replace("#","");if(hash!=="api_token_slide"&&hash!=="domain_name_slide"){return}let form=e.target.closest(".plausible-analytics-wizard-step-section");let inputs=form.getElementsByTagName("INPUT");let options=[];for(let input of inputs){input=plausible.validateInput(input);options.push({name:input.name,value:input.value})}let data=new FormData;data.append("action","plausible_analytics_save_options");data.append("options",JSON.stringify(options));data.append("_nonce",plausible.nonce);plausible.ajax(data).then(response=>{if(hash==="api_token_slide"&&response.success===true){let stats_button=document.getElementById("enable_analytics_dashboard_view_stats_in_wordpress");stats_button.removeAttribute("disabled")}})},disableConnectButton:function(e){let target=e.target;let button=document.getElementById("connect_plausible_analytics");let buttonIsHref=false;if(button===null){let slide_id=document.location.hash;button=document.querySelector(slide_id+" .plausible-analytics-wizard-next-step");buttonIsHref=true}if(button===null){return}if(target.value!==""){if(!buttonIsHref){button.disabled=false}else{button.classList.remove("pointer-events-none");button.classList.replace("bg-gray-200","bg-indigo-600")}return}if(!buttonIsHref){button.disabled=true;button.innerHTML=button.innerHTML.replace("Connected","Connect")}else{button.classList+=" pointer-events-none";button.classList.replace("bg-indigo-600","bg-gray-200")}},createAPIToken:function(e){e.preventDefault();let domain=document.getElementById("domain_name").value;domain=domain.replaceAll("/","%2F");window.open(`${plausible_analytics_hosted_domain}/${domain}/settings/integrations?new_token=WordPress`,"_blank","location=yes,height=768,width=1024,scrollbars=yes,status=no")},showWizard:function(e){let data=new FormData;data.append("action","plausible_analytics_show_wizard");data.append("_nonce",e.target.dataset.nonce);plausible.ajax(data)},toggleWizardStep:function(){if(document.getElementById("plausible-analytics-wizard")===null){return}const hash=document.location.hash.substring(1).replace("_slide","");let allSteps=document.querySelectorAll(".plausible-analytics-wizard-step");let activeSteps=document.querySelectorAll(".plausible-analytics-wizard-active-step");let completedSteps=document.querySelectorAll(".plausible-analytics-wizard-completed-step");for(let i=0;in);if(currentlyCompletedSteps.length<1){return}currentlyCompletedSteps.forEach(function(step){let completedStep=document.getElementById("completed-step-"+step);let inactiveStep=document.getElementById("step-"+step);completedStep.classList.remove("hidden");inactiveStep.classList+=" hidden"})},ajax:function(data,button=null,showMessages=true){return fetch(ajaxurl,{method:"POST",body:data}).then(response=>{if(button){if(button.children.length>0){button.children[0].classList+=" hidden"}if(button.id==="connect_plausible_analytics"&&response.status===200){button.innerText=plausible_analytics_i18n.connected}else{button.removeAttribute("disabled")}}if(response.status===200){return response.json()}return false}).then(response=>{if(showMessages===true){plausible.showMessages()}let event=new CustomEvent("plausibleAjaxDone",{detail:response});document.dispatchEvent(event);if(response.data!==undefined){return response.data}else{return response}})},showMessages:function(){let messages=plausible.fetchMessages();messages.then(function(messages){if(messages.error!==false){plausible.showMessage(messages.error,"error")}else if(messages.notice!==false){plausible.showMessage(messages.notice,"notice")}else if(messages.success!==false){plausible.showMessage(messages.success,"success")}if(messages.additional.length===0||document.getElementById("plausible-analytics-wizard")!==null){return}if(messages.additional.id!==undefined&&messages.additional.message){plausible.showAdditionalMessage(messages.additional.message,messages.additional.id)}else if(messages.additional.id!==undefined&&messages.additional.message===""){plausible.removeAdditionalMessage(messages.additional.id)}})},fetchMessages:function(){let data=new FormData;data.append("action","plausible_analytics_messages");let result=plausible.ajax(data,null,false);return result.then(function(response){return response})},showMessage:function(message,type="success"){if(type==="error"){document.getElementById("icon-error").classList.remove("hidden");document.getElementById("icon-success").classList.add("hidden");document.getElementById("icon-notice").classList.add("hidden")}else if(type==="notice"){document.getElementById("icon-notice").classList.remove("hidden");document.getElementById("icon-error").classList.add("hidden");document.getElementById("icon-success").classList.add("hidden")}else{document.getElementById("icon-success").classList.remove("hidden");document.getElementById("icon-error").classList.add("hidden");document.getElementById("icon-notice").classList.add("hidden")}let notice=document.getElementById("plausible-analytics-notice");document.getElementById("plausible-analytics-notice-text").innerHTML=message;notice.classList.remove("hidden");setTimeout(function(){notice.classList.replace("opacity-0","opacity-100")},200);if(type!=="error"){setTimeout(function(){notice.classList.replace("opacity-100","opacity-0");setTimeout(function(){notice.classList+=" hidden"},200)},2e3)}},showAdditionalMessage:function(html,target){let targetElem=document.querySelector(`[name='${target}']`);let container=targetElem.closest(".plausible-analytics-group");if(container.children.length>0){for(let i=0;i0){for(let i=0;i{if(!document.location.href.includes("plausible_analytics")){return}let plausible={nonceElem:document.getElementById("_wpnonce"),nonce:"",showWizardElem:document.getElementById("show_wizard"),domainNameElem:document.getElementById("domain_name"),apiTokenElem:document.getElementById("api_token"),createAPITokenElems:document.getElementsByClassName("plausible-create-api-token"),buttonElems:document.getElementsByClassName("plausible-analytics-button"),stepElems:document.getElementsByClassName("plausible-analytics-wizard-next-step"),init:function(){if(document.location.hash===""&&document.getElementById("plausible-analytics-wizard")!==null){document.location.hash="#welcome_slide"}if(this.nonceElem!==null){this.nonce=this.nonceElem.value}this.toggleWizardStep();window.addEventListener("hashchange",this.toggleWizardStep);if(this.showWizardElem!==null){this.showWizardElem.addEventListener("click",this.showWizard)}if(this.domainNameElem!==null){this.domainNameElem.addEventListener("keyup",this.disableConnectButton)}if(this.apiTokenElem!==null){this.apiTokenElem.addEventListener("keyup",this.disableConnectButton)}if(this.createAPITokenElems.length>0){for(let i=0;i0){for(let i=0;i0){for(let i=0;i0){button.children[0].classList.remove("hidden")}button.setAttribute("disabled","disabled");plausible.ajax(form,button)},maybeDisableOptions:function(capabilities){let options=document.querySelectorAll("button[data-caps]");options.forEach(function(option){let caps=option.dataset.caps.split(",");let disable=false;caps.forEach(function(cap){if(capabilities[cap]===false){disable=true}});if(disable===true){if(option.dataset.status==="on"){option.dispatchEvent(new Event("click",{bubbles:true}))}}})},validateInput:function(input){if(input.name==="domain_name"&&input.value.match(/^(https?:\/\/)?(www.)?/).length>0){input.value=input.value.replace(/^(https?:\/\/)?(www.)?/,"")}return input},saveOptionOnNext:function(e){let hash=document.location.hash.replace("#","");if(hash!=="api_token_slide"&&hash!=="domain_name_slide"){return}let form=e.target.closest(".plausible-analytics-wizard-step-section");let inputs=form.getElementsByTagName("INPUT");let options=[];for(let input of inputs){input=plausible.validateInput(input);options.push({name:input.name,value:input.value})}let data=new FormData;data.append("action","plausible_analytics_save_options");data.append("options",JSON.stringify(options));data.append("_nonce",plausible.nonce);plausible.ajax(data).then(response=>{if(hash==="api_token_slide"&&response.success===true){let stats_button=document.getElementById("enable_analytics_dashboard_view_stats_in_wordpress");stats_button.removeAttribute("disabled")}})},disableConnectButton:function(e){let target=e.target;let button=document.getElementById("connect_plausible_analytics");let buttonIsHref=false;if(button===null){let slide_id=document.location.hash;button=document.querySelector(slide_id+" .plausible-analytics-wizard-next-step");buttonIsHref=true}if(button===null){return}if(target.value!==""){if(!buttonIsHref){button.disabled=false}else{button.classList.remove("pointer-events-none");button.classList.replace("bg-gray-200","bg-indigo-600")}return}if(!buttonIsHref){button.disabled=true;button.innerHTML=button.innerHTML.replace("Connected","Connect")}else{button.classList+=" pointer-events-none";button.classList.replace("bg-indigo-600","bg-gray-200")}},createAPIToken:function(e){e.preventDefault();let domain=document.getElementById("domain_name").value;domain=domain.replaceAll("/","%2F");window.open(`${plausible_analytics_hosted_domain}/${domain}/settings/integrations?new_token=WordPress`,"_blank","location=yes,height=768,width=1024,scrollbars=yes,status=no")},showWizard:function(e){let data=new FormData;data.append("action","plausible_analytics_show_wizard");data.append("_nonce",e.target.dataset.nonce);plausible.ajax(data)},toggleWizardStep:function(){if(document.getElementById("plausible-analytics-wizard")===null){return}const hash=document.location.hash.substring(1).replace("_slide","");let allSteps=document.querySelectorAll(".plausible-analytics-wizard-step");let activeSteps=document.querySelectorAll(".plausible-analytics-wizard-active-step");let completedSteps=document.querySelectorAll(".plausible-analytics-wizard-completed-step");for(let i=0;in);if(currentlyCompletedSteps.length<1){return}currentlyCompletedSteps.forEach(function(step){let completedStep=document.getElementById("completed-step-"+step);let inactiveStep=document.getElementById("step-"+step);completedStep.classList.remove("hidden");inactiveStep.classList+=" hidden"})},ajax:function(data,button=null,showMessages=true){return fetch(ajaxurl,{method:"POST",body:data}).then(response=>{if(button){if(button.children.length>0){button.children[0].classList+=" hidden"}if(button.id==="connect_plausible_analytics"&&response.status===200){button.innerText=plausible_analytics_i18n.connected}else{button.removeAttribute("disabled")}}if(response.status===200||response.status===402){return response.json()}return false}).then(response=>{if(showMessages===true){plausible.showMessages()}let event=new CustomEvent("plausibleAjaxDone",{detail:response});document.dispatchEvent(event);if(response.data!==undefined){return response.data}else{return response}})},showMessages:function(){let messages=plausible.fetchMessages();messages.then(function(messages){if(messages.error!==false){plausible.showMessage(messages.error,"error")}else if(messages.notice!==false){plausible.showMessage(messages.notice,"notice")}else if(messages.success!==false){plausible.showMessage(messages.success,"success")}if(messages.additional.length===0||document.getElementById("plausible-analytics-wizard")!==null){return}if(messages.additional.id!==undefined&&messages.additional.message){plausible.showAdditionalMessage(messages.additional.message,messages.additional.id)}else if(messages.additional.id!==undefined&&messages.additional.message===""){plausible.removeAdditionalMessage(messages.additional.id)}})},fetchMessages:function(){let data=new FormData;data.append("action","plausible_analytics_messages");let result=plausible.ajax(data,null,false);return result.then(function(response){return response})},showMessage:function(message,type="success"){if(type==="error"){document.getElementById("icon-error").classList.remove("hidden");document.getElementById("icon-success").classList.add("hidden");document.getElementById("icon-notice").classList.add("hidden")}else if(type==="notice"){document.getElementById("icon-notice").classList.remove("hidden");document.getElementById("icon-error").classList.add("hidden");document.getElementById("icon-success").classList.add("hidden")}else{document.getElementById("icon-success").classList.remove("hidden");document.getElementById("icon-error").classList.add("hidden");document.getElementById("icon-notice").classList.add("hidden")}let notice=document.getElementById("plausible-analytics-notice");document.getElementById("plausible-analytics-notice-text").innerHTML=message;notice.classList.remove("hidden");setTimeout(function(){notice.classList.replace("opacity-0","opacity-100")},200);if(type!=="error"){setTimeout(function(){notice.classList.replace("opacity-100","opacity-0");setTimeout(function(){notice.classList+=" hidden"},200)},2e3)}},showAdditionalMessage:function(html,target){let targetElem=document.querySelector(`[name='${target}']`);let container=targetElem.closest(".plausible-analytics-group");if(container.children.length>0){for(let i=0;i0){for(let i=0;icustom_search_properties as $property ) { + if ( empty( $caps[ 'props' ] ) && $property === 'result_count' ) { + continue; + } + $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => $property ] ] ); } } @@ -373,4 +379,32 @@ public function maybe_create_custom_properties( $old_settings, $settings ) { $this->client->enable_custom_property( $create_request ); } + + /** + * Auto-enables tracking of the 'Customer' user role for WC, 'Subscriber' user role for EDD and 'EDD_Subscriber' user role for EDD Recurring + * if Revenue tracking and one of these plugins is enabled. + * + * @param $settings + * + * @return array + */ + public function maybe_enable_customer_user_roles( $settings ) { + $enhanced_measurements = $settings[ 'enhanced_measurements' ]; + + if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) ) { + if ( Integrations::is_wc_active() && ! in_array( 'customer', $settings[ 'tracked_user_roles' ] ) ) { + $settings[ 'tracked_user_roles' ][] = 'customer'; + } + + if ( Integrations::is_edd_active() && ! in_array( 'subscriber', $settings[ 'tracked_user_roles' ] ) ) { + $settings[ 'tracked_user_roles' ][] = 'subscriber'; + } + + if ( Integrations::is_edd_recurring_active() && ! in_array( 'edd_subscriber', $settings[ 'tracked_user_roles' ] ) ) { + $settings[ 'tracked_user_roles' ][] = 'edd_subscriber'; + } + } + + return $settings; + } } diff --git a/src/Admin/Settings/API.php b/src/Admin/Settings/API.php index 75f2a274..56dc1cee 100644 --- a/src/Admin/Settings/API.php +++ b/src/Admin/Settings/API.php @@ -45,15 +45,15 @@ class API { /** * Render Fields. * - * @return void * @since 1.3.0 * @access public + * @return void */ public function settings_page() { wp_nonce_field( 'plausible_analytics_toggle_option' ); $settings = Helpers::get_settings(); - $followed_wizard = get_option( 'plausible_analytics_wizard_done' ) || ! empty( $settings['self_hosted_domain'] ); + $followed_wizard = get_option( 'plausible_analytics_wizard_done' ) || ! empty( $settings[ 'self_hosted_domain' ] ); /** * On-boarding wizard. @@ -100,23 +100,19 @@ public function settings_page() { 'plausible-analytics' ), admin_url( 'index.php?page=plausible_analytics_statistics' ), - admin_url( 'admin-ajax.php?action=plausible_analytics_quit_wizard&_nonce=' ) . - wp_create_nonce( 'plausible_analytics_quit_wizard' ) . - '&redirect=tracked_user_roles', + admin_url( 'admin-ajax.php?action=plausible_analytics_quit_wizard&_nonce=' ) . wp_create_nonce( 'plausible_analytics_quit_wizard' ) . '&redirect=tracked_user_roles', 'https://plausible.io/docs?utm_source=WordPress&utm_medium=Referral&utm_campaign=WordPress+plugin', 'https://plausible.io/contact?utm_source=WordPress&utm_medium=Referral&utm_campaign=WordPress+plugin' ), ]; - if ( empty( $settings['enable_analytics_dashboard'] ) ) { - $this->slides_description['success'] = sprintf( + if ( empty( $settings[ 'enable_analytics_dashboard' ] ) ) { + $this->slides_description[ 'success' ] = sprintf( __( '

Congrats! Your traffic is now being counted without compromising the user experience and privacy of your visitors.

Note that visits from logged in users aren\'t tracked. If you want to track visits for certain user roles, then please specify them in the plugin\'s settings.

Need help? Our documentation is the best place to find most answers right away.

Still haven\'t found the answer you\'re looking for? We\'re here to help. Please contact our support.

', 'plausible-analytics' ), - admin_url( 'admin-ajax.php?action=plausible_analytics_quit_wizard&_nonce=' ) . - wp_create_nonce( 'plausible_analytics_quit_wizard' ) . - '&redirect=tracked_user_roles', + admin_url( 'admin-ajax.php?action=plausible_analytics_quit_wizard&_nonce=' ) . wp_create_nonce( 'plausible_analytics_quit_wizard' ) . '&redirect=tracked_user_roles', 'https://plausible.io/docs?utm_source=WordPress&utm_medium=Referral&utm_campaign=WordPress+plugin', 'https://plausible.io/contact?utm_source=WordPress&utm_medium=Referral&utm_campaign=WordPress+plugin' ); @@ -130,7 +126,7 @@ public function settings_page() { /** * Settings screen */ - $current_tab = ! empty( $_GET['tab'] ) ? $_GET['tab'] : 'general'; + $current_tab = ! empty( $_GET[ 'tab' ] ) ? $_GET[ 'tab' ] : 'general'; ?>
@@ -157,7 +153,7 @@ public function settings_page() { fields[ $current_tab ] as $tab => $field ): ?>
get_wizard_option_properties( $id ); if ( ! empty( $field ) ) { - $hide_header = $field['type'] === 'group'; - - echo call_user_func( [ - $this, - "render_{$field['type']}_field" - ], $field, $hide_header ); + $hide_header = $field[ 'type' ] === 'group'; + + echo call_user_func( + [ + $this, + "render_{$field['type']}_field", + ], + $field, + $hide_header + ); } ?> @@ -423,13 +423,13 @@ class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">

* @return array|mixed */ private function get_wizard_option_properties( $slug ) { - foreach ( $this->fields['general'] as $group ) { - if ( $group['slug'] === $slug ) { + foreach ( $this->fields[ 'general' ] as $group ) { + if ( $group[ 'slug' ] === $slug ) { return $group; } - foreach ( $group['fields'] as $field ) { - if ( $field['slug'] === $slug ) { + foreach ( $group[ 'fields' ] as $field ) { + if ( $field[ 'slug' ] === $slug ) { return $field; } } @@ -441,9 +441,9 @@ private function get_wizard_option_properties( $slug ) { /** * Render Header Navigation. * - * @return void * @since 1.3.0 * @access public + * @return void */ public function render_navigation() { $screen = get_current_screen(); @@ -453,7 +453,7 @@ public function render_navigation() { return; } - $current_tab = ! empty( $_GET['tab'] ) ? $_GET['tab'] : ''; + $current_tab = ! empty( $_GET[ 'tab' ] ) ? $_GET[ 'tab' ] : ''; $tabs = apply_filters( 'plausible_analytics_settings_navigation_tabs', [ @@ -474,9 +474,9 @@ public function render_navigation() { foreach ( $tabs as $tab ) { printf( '%3$s', - esc_url( $tab['url'] ), - esc_attr( $tab['class'] ), - esc_html( $tab['name'] ) + esc_url( $tab[ 'url' ] ), + esc_attr( $tab[ 'class' ] ), + esc_html( $tab[ 'name' ] ) ); } ?> @@ -486,8 +486,8 @@ public function render_navigation() { /** * Render Quick Actions * - * @return string * @since 1.3.0 + * @return string */ private function render_quick_actions() { ob_start(); @@ -495,13 +495,13 @@ private function render_quick_actions() { ?> 0 ) : ?> - + - - + + @@ -512,8 +512,8 @@ private function render_quick_actions() { /** * Get Quick Actions. * - * @return array * @since 1.3.0 + * @return array */ private function get_quick_actions() { $settings = Helpers::get_settings(); @@ -525,7 +525,7 @@ private function get_quick_actions() { 'url' => admin_url( "admin-ajax.php?action=plausible_analytics_show_wizard&_nonce=$nonce&redirect=1" ), 'id' => 'show_wizard', 'target' => '_self', - 'disabled' => ! empty( $settings['self_hosted_domain'] ), + 'disabled' => ! empty( $settings[ 'self_hosted_domain' ] ), ], 'view-docs' => [ 'label' => esc_html__( 'Documentation', 'plausible-analytics' ), @@ -545,13 +545,13 @@ private function get_quick_actions() { /** * Render Group Field. * - * @return string * @since 1.3.0 * @access public + * @return string */ public function render_group_field( array $group, $hide_header = false ) { - $toggle = $group['toggle'] ?? []; - $fields = $group['fields']; + $toggle = $group[ 'toggle' ] ?? []; + $fields = $group[ 'fields' ]; ob_start(); ?>

+ id="">
- +
- - + + @@ -578,7 +578,7 @@ class="
+ for="">
/>
@@ -630,12 +630,12 @@ class="block w-full !border-gray-300 !dark:border-gray-700 !rounded-md focus:rin */ public function render_button_field( array $field ) { ob_start(); - $disabled = isset( $field['disabled'] ) && $field['disabled'] === true; + $disabled = isset( $field[ 'disabled' ] ) && $field[ 'disabled' ] === true; ?>
- - - - - + + + @@ -700,29 +695,29 @@ class="plausible-analytics-toggle
+ name="">
-
@@ -776,9 +771,9 @@ class="w-5 h-12 text-yellow-400">
-
+
-

+

diff --git a/src/Admin/Settings/Page.php b/src/Admin/Settings/Page.php index 742e570c..416d3367 100644 --- a/src/Admin/Settings/Page.php +++ b/src/Admin/Settings/Page.php @@ -54,6 +54,14 @@ class Page extends API { 'hook_type' => 'success', ]; + const CAP_GOALS = 'goals'; + + const CAP_PROPS = 'props'; + + const CAP_FUNNELS = 'funnels'; + + const CAP_REVENUE = 'revenue'; + /** * @var array|array[] $fields */ @@ -117,15 +125,11 @@ public function __construct() { 'value' => $settings[ 'api_token' ], ], [ - 'label' => empty( $settings[ 'domain_name' ] ) || empty( $settings[ 'api_token' ] ) ? - esc_html__( 'Connect', 'plausible-analytics' ) : + 'label' => empty( $settings[ 'domain_name' ] ) || empty( $settings[ 'api_token' ] ) ? esc_html__( 'Connect', 'plausible-analytics' ) : esc_html__( 'Connected', 'plausible-analytics' ), 'slug' => 'connect_plausible_analytics', 'type' => 'button', - 'disabled' => empty( $settings[ 'domain_name' ] ) || - empty( $settings[ 'api_token' ] ) || - ! $this->client instanceof Client || - $this->client->is_api_token_valid(), + 'disabled' => empty( $settings[ 'domain_name' ] ) || empty( $settings[ 'api_token' ] ) || ! $this->client instanceof Client || $this->client->is_api_token_valid(), ], ], ], @@ -145,6 +149,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => '404', + 'caps' => [ self::CAP_GOALS ], ], 'outbound-links' => [ 'label' => esc_html__( 'Outbound links', 'plausible-analytics' ), @@ -152,6 +157,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'outbound-links', + 'caps' => [ self::CAP_GOALS ], ], 'file-downloads' => [ 'label' => esc_html__( 'File downloads', 'plausible-analytics' ), @@ -159,6 +165,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'file-downloads', + 'caps' => [ self::CAP_GOALS ], ], 'search' => [ 'label' => esc_html__( 'Search queries', 'plausible-analytics' ), @@ -166,6 +173,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'search', + 'caps' => [ self::CAP_GOALS ], ], 'tagged-events' => [ 'label' => esc_html__( 'Custom events', 'plausible-analytics' ), @@ -173,14 +181,15 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'tagged-events', + 'caps' => [ self::CAP_GOALS ], ], 'revenue' => [ - 'label' => esc_html__( 'Ecommerce revenue', 'plausible-analytics' ), - 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-ecommerce-revenue', - 'slug' => 'enhanced_measurements', - 'type' => 'checkbox', - 'value' => 'revenue', - 'disabled' => ! empty( $settings[ 'self_hosted_domain' ] ), + 'label' => esc_html__( 'Ecommerce revenue', 'plausible-analytics' ), + 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-ecommerce-revenue', + 'slug' => 'enhanced_measurements', + 'type' => 'checkbox', + 'value' => 'revenue', + 'caps' => [ self::CAP_GOALS, self::CAP_FUNNELS, self::CAP_PROPS, self::CAP_REVENUE ], ], 'pageview-props' => [ 'label' => esc_html__( 'Authors and categories', 'plausible-analytics' ), @@ -188,6 +197,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'pageview-props', + 'caps' => [ self::CAP_PROPS ], ], 'form-completions' => [ 'label' => esc_html__( 'Form completions', 'plausible-analytics' ), @@ -195,6 +205,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'form-completions', + 'caps' => [ self::CAP_GOALS ], ], 'hash' => [ 'label' => esc_html__( 'Hash-based routing', 'plausible-analytics' ), @@ -202,6 +213,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'hash', + 'caps' => [], ], 'compat' => [ 'label' => esc_html__( 'IE compatibility', 'plausible-analytics' ), @@ -209,6 +221,7 @@ public function __construct() { 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'compat', + 'caps' => [], ], ], ], @@ -227,8 +240,7 @@ public function __construct() { get_site_url( null, rest_get_url_prefix() ), empty( Helpers::get_settings()[ 'proxy_enabled' ] - ) ? 'a random directory/file for storing the JS file' : - 'a JS file, called ' . str_replace( + ) ? 'a random directory/file for storing the JS file' : 'a JS file, called ' . str_replace( ABSPATH, '', Helpers::get_proxy_resource( 'cache_dir' ) . Helpers::get_proxy_resource( @@ -263,8 +275,7 @@ public function __construct() { 'slug' => 'enable_analytics_dashboard', 'type' => 'checkbox', 'value' => 'on', - 'disabled' => empty( Helpers::get_settings()[ 'api_token' ] ) && - empty( Helpers::get_settings()[ 'self_hosted_domain' ] ), + 'disabled' => empty( Helpers::get_settings()[ 'api_token' ] ) && empty( Helpers::get_settings()[ 'self_hosted_domain' ] ), ], ], ], @@ -367,8 +378,7 @@ public function __construct() { 'label' => esc_html__( 'Domain name', 'plausible-analytics' ), 'slug' => 'self_hosted_domain', 'type' => 'text', - 'value' => defined( 'PLAUSIBLE_SELF_HOSTED_DOMAIN' ) ? PLAUSIBLE_SELF_HOSTED_DOMAIN : - $settings[ 'self_hosted_domain' ], + 'value' => defined( 'PLAUSIBLE_SELF_HOSTED_DOMAIN' ) ? PLAUSIBLE_SELF_HOSTED_DOMAIN : $settings[ 'self_hosted_domain' ], 'placeholder' => 'e.g. ' . Helpers::get_domain(), 'disabled' => Helpers::proxy_enabled(), ], @@ -552,8 +562,7 @@ public function register_menu() { /** * Don't show the Analytics dashboard, if View Stats is disabled. */ - if ( ! empty( $settings[ 'enable_analytics_dashboard' ] ) || - ( ! empty( $settings[ 'self_hosted_domain' ] ) && ! empty( $settings[ 'self_hosted_shared_link' ] ) ) ) { + if ( ! empty( $settings[ 'enable_analytics_dashboard' ] ) || ( ! empty( $settings[ 'self_hosted_domain' ] ) && ! empty( $settings[ 'self_hosted_shared_link' ] ) ) ) { // Setup `Analytics` page under Dashboard. add_dashboard_page( esc_html__( 'Analytics', 'plausible-analytics' ), @@ -649,9 +658,7 @@ public function render_analytics_dashboard() { * For regular users, the shared link is provisioned by the API, so it shouldn't be empty. * @since v2.0.3 */ - if ( ( ! $self_hosted && ! empty( $analytics_enabled ) && ! empty( $shared_link ) ) || - ( $self_hosted && ! empty( $shared_link ) ) || - strpos( $shared_link, 'XXXXXX' ) !== false ) { + if ( ( ! $self_hosted && ! empty( $analytics_enabled ) && ! empty( $shared_link ) ) || ( $self_hosted && ! empty( $shared_link ) ) || strpos( $shared_link, 'XXXXXX' ) !== false ) { $page_url = isset( $_GET[ 'page-url' ] ) ? esc_url( $_GET[ 'page-url' ] ) : ''; // Append individual page URL if it exists. @@ -720,4 +727,20 @@ public function render_analytics_dashboard() { upgrade_to_230(); } + if ( version_compare( $plausible_analytics_version, '2.3.1', '<' ) ) { + $this->upgrade_to_231(); + } + // Add required upgrade routines for future versions here. } @@ -304,4 +309,17 @@ public function upgrade_to_230() { update_option( 'plausible_analytics_version', '2.3.0' ); } + + /** + * Make sure the cron event is scheduled. If it's already scheduled or the Proxy isn't enabled, it'll bail. + * + * @return void + */ + public function upgrade_to_231() { + $setup = new Setup(); + + $setup->activate_cron(); + + update_option( 'plausible_analytics_version', '2.3.1' ); + } } diff --git a/src/Ajax.php b/src/Ajax.php index dc41a717..2e059408 100644 --- a/src/Ajax.php +++ b/src/Ajax.php @@ -190,10 +190,12 @@ public function toggle_option() { * Toggle lists. */ if ( $post_data[ 'toggle_status' ] === 'on' ) { + // If toggle is on, store the value under a new key. if ( ! in_array( $post_data[ 'option_value' ], $settings[ $post_data[ 'option_name' ] ] ) ) { $settings[ $post_data[ 'option_name' ] ][] = $post_data[ 'option_value' ]; } } else { + // If toggle is off, find the key by its value and unset it. if ( ( $key = array_search( $post_data[ 'option_value' ], $settings[ $post_data[ 'option_name' ] ] ) ) !== false ) { unset( $settings[ $post_data[ 'option_name' ] ][ $key ] ); } diff --git a/src/Client.php b/src/Client.php index b9f7e26e..4b1f3415 100644 --- a/src/Client.php +++ b/src/Client.php @@ -11,7 +11,9 @@ use Plausible\Analytics\WP\Client\Model\Capabilities; use Plausible\Analytics\WP\Client\Model\CapabilitiesFeatures; use Plausible\Analytics\WP\Client\Model\CustomPropEnableRequestBulkEnable; +use Plausible\Analytics\WP\Client\Model\FunnelCreateRequest; use Plausible\Analytics\WP\Client\Model\GoalCreateRequestBulkGetOrCreate; +use Plausible\Analytics\WP\Client\Model\GoalListResponse; use Plausible\Analytics\WP\Client\Model\PaymentRequiredError; use Plausible\Analytics\WP\Client\Model\SharedLink; use Plausible\Analytics\WP\Client\Model\UnauthorizedError; @@ -58,13 +60,15 @@ public function validate_api_token() { $data_domain = $this->get_data_domain(); $token = $this->api_instance->getConfig()->getPassword(); - $is_valid = strpos( $token, 'plausible-plugin' ) !== false && ! empty( $features->getGoals() ) && $data_domain === Helpers::get_domain(); + $is_valid = str_contains( $token, 'plausible-plugin' ) && ! empty( $features->getGoals() ) && $data_domain === Helpers::get_domain(); /** * Don't cache invalid API tokens. */ if ( $is_valid ) { set_transient( 'plausible_analytics_valid_token', [ $token => true ], 86400 ); // @codeCoverageIgnore + + $this->update_capabilities( $token ); // @codeCoverageIgnore } return $is_valid; @@ -87,7 +91,7 @@ public function is_api_token_valid() { * * @return false|Client\Model\CapabilitiesFeatures */ - private function get_features() { + public function get_features() { $capabilities = $this->get_capabilities(); if ( $capabilities instanceof Capabilities ) { @@ -129,6 +133,44 @@ private function get_data_domain() { return false; } + /** + * Stores the capabilities for the currently entered API token in the DB for later use. + * + * @param $token + * + * @return false|array + * + * @codeCoverageIgnore + */ + private function update_capabilities( $token = '' ) { + $client_factory = new ClientFactory( $token ); + /** @var Client $client */ + $client = $client_factory->build(); + + if ( ! $client instanceof Client ) { + return false; + } + + /** @var Client\Model\CapabilitiesFeatures $features */ + $features = $client->get_features(); + + if ( ! $features ) { + return false; + } + + $caps = [ + 'funnels' => $features->getFunnels(), + 'goals' => $features->getGoals(), + 'props' => $features->getProps(), + 'revenue' => $features->getRevenueGoals(), + 'stats' => $features->getStatsApi(), + ]; + + update_option( 'plausible_analytics_api_token_caps', $caps ); + + return $caps; + } + /** * Create Shared Link in Plausible Dashboard. * @@ -212,7 +254,9 @@ private function send_json_error( $e, $error_message ) { Messages::set_error( sprintf( $error_message, $message ) ); - wp_send_json_error( null, $code ); + $caps = $this->update_capabilities(); + + wp_send_json_error( [ 'capabilities' => $caps ], $code ); } /** @@ -220,7 +264,7 @@ private function send_json_error( $e, $error_message ) { * * @param GoalCreateRequestBulkGetOrCreate $goals * - * @return Client\Model\PaymentRequiredError|Client\Model\PlausibleWebPluginsAPIControllersGoalsCreate201Response|Client\Model\UnauthorizedError|Client\Model\UnprocessableEntityError|null + * @return GoalListResponse|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void * * @codeCoverageIgnore */ @@ -235,7 +279,7 @@ public function create_goals( $goals ) { /** * Allows creating Funnels in bulk. * - * @param \Plausible\Analytics\WP\Client\Model\FunnelCreateRequest $funnel + * @param FunnelCreateRequest $funnel * * @return Client\Model\Funnel|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void * diff --git a/src/Compatibility.php b/src/Compatibility.php index a3045cdd..5a5a1252 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -43,6 +43,11 @@ public function __construct() { add_filter( 'sgo_javascript_combine_excluded_external_paths', [ $this, 'exclude_plausible_js' ] ); } + // W3 Total Cache + if ( defined( 'W3TC_VERSION' ) ) { + add_filter( 'w3tc_minify_js_script_tags', [ $this, 'unset_plausible_js' ] ); + } + // WPML if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_filter( 'rest_url', [ $this, 'wpml_compatibility' ], 10, 1 ); @@ -59,6 +64,7 @@ public function __construct() { add_filter( 'rocket_exclude_js', [ $this, 'exclude_plausible_js' ] ); add_filter( 'rocket_minify_excluded_external_js', [ $this, 'exclude_plausible_js' ] ); add_filter( 'rocket_delay_js_exclusions', [ $this, 'exclude_plausible_inline_js' ] ); + add_filter( 'rocket_delay_js_exclusions', [ $this, 'exclude_by_proxy_endpoint' ] ); add_filter( 'rocket_exclude_defer_js', [ $this, 'exclude_plausible_js_by_relative_url' ] ); } } @@ -109,6 +115,26 @@ public function exclude_plausible_js( $excluded_js ) { return $excluded_js; } + /** + * Remove Plausible.js (or the local file, when proxy is enabled) of the list of JS files to minify. + * + * @filter w3tc_minify_js_script_tags + * @since 2.4.0 + * + * @param $script_tags + * + * @return array + * @throws Exception + */ + public function unset_plausible_js( $script_tags ) { + return array_filter( + $script_tags, + function ( $tag ) { + return str_contains( $tag, Helpers::get_js_url( true ) ) === false; + } + ); + } + /** * Dear WP Rocket/SG Optimizer/Etc., don't minify/combine/delay our external JS, please. * @@ -126,6 +152,24 @@ public function exclude_plausible_js_by_relative_url( $excluded_js ) { return $excluded_js; } + /** + * Some optimization plugins (WP Rocket) replace the JS src URL with their own URL, before being able to exclude it. + * So, when the proxy is enabled, exclusion fails. That's why we exclude again by proxy endpoint. + * + * @filter rocket_delay_js_exclusions + * @since 2.4.0 + * + * @param $excluded_js + * + * @return mixed + * @throws Exception + */ + public function exclude_by_proxy_endpoint( $excluded_js ) { + $excluded_js[] = Helpers::get_rest_endpoint( false ); + + return $excluded_js; + } + /** * Dear WP Rocket/SG Optimizer/Etc., don't minify/combine/delay our external JS, please. * diff --git a/src/Cron.php b/src/Cron.php index 42120b90..8fb12269 100644 --- a/src/Cron.php +++ b/src/Cron.php @@ -13,6 +13,13 @@ use Exception; class Cron { + /** + * Cron job handle + * + * @var string + */ + const TASK_NAME = 'plausible_analytics_update_js'; + /** * Build class * diff --git a/src/Filters.php b/src/Filters.php index 8255123e..ac79eb29 100644 --- a/src/Filters.php +++ b/src/Filters.php @@ -51,8 +51,11 @@ public function add_plausible_attributes( $tag, $handle ) { $api_url = Helpers::get_data_api_url(); $domain_name = Helpers::get_domain(); - // We need the correct id attribute for IE compatibility. - $tag = preg_replace( "/\sid=(['\"])plausible-analytics-js(['\"])/", " id=$1plausible$2", $tag ); + if ( Helpers::is_enhanced_measurement_enabled( 'compat' ) ) { + // We need the correct id attribute for IE compatibility. + $tag = preg_replace( "/\sid=(['\"])plausible-analytics-js(['\"])/", " id=$1plausible$2", $tag ); + } + /** * the data-cfasync ensures this script isn't processed by CF Rocket Loader @see https://developers.cloudflare.com/speed/optimization/content/rocket-loader/ignore-javascripts/ */ diff --git a/src/Integrations.php b/src/Integrations.php index bcda6410..e54a6406 100644 --- a/src/Integrations.php +++ b/src/Integrations.php @@ -27,17 +27,17 @@ public function __construct() { */ private function init() { // WooCommerce - if ( self::is_wc_active() ) { + if ( self::is_wc_active() && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ) { new Integrations\WooCommerce(); } // Easy Digital Downloads - if ( self::is_edd_active() ) { + if ( self::is_edd_active() && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ) { new Integrations\EDD(); } // Form Plugins - if ( self::is_form_submit_active() ) { + if ( Helpers::is_enhanced_measurement_enabled( 'form-completions' ) ) { new Integrations\FormSubmit(); } } @@ -47,7 +47,7 @@ private function init() { * @return bool */ public static function is_wc_active() { - return apply_filters( 'plausible_analytics_integrations_woocommerce', function_exists( 'WC' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); + return apply_filters( 'plausible_analytics_integrations_woocommerce', function_exists( 'WC' ) ); } /** @@ -55,14 +55,14 @@ public static function is_wc_active() { * @return bool */ public static function is_edd_active() { - return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); + return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) ); } /** - * Check if Form Submissions option is enabled in Enhanced Measurements. + * Checks if EDD Recurring is installed and activated. * @return mixed|null */ - public static function is_form_submit_active() { - return apply_filters( 'plausible_analytics_integrations_form_submit', Helpers::is_enhanced_measurement_enabled( 'form-completions' ) ); + public static function is_edd_recurring_active() { + return apply_filters( 'plausible_analytics_integrations_edd_recurring', function_exists( 'EDD_Recurring' ) ); } } diff --git a/src/Integrations/FormSubmit.php b/src/Integrations/FormSubmit.php index b74d0561..1bc8d821 100644 --- a/src/Integrations/FormSubmit.php +++ b/src/Integrations/FormSubmit.php @@ -13,6 +13,7 @@ class FormSubmit { /** * Build class. + * * @codeCoverageIgnore */ public function __construct() { @@ -21,7 +22,9 @@ public function __construct() { /** * Init + * * @return void + * * @codeCoverageIgnore */ private function init() { @@ -33,11 +36,16 @@ private function init() { * Contact Form 7 doesn't respect JS checkValidity() function, so this is a custom compatibility fix. */ add_filter( 'wpcf7_validate', [ $this, 'maybe_track_submission' ], 10, 2 ); + /** + * Gravity Forms contains its own form submission handler, so this is a custom compatibility fix. + */ + add_action( 'gform_after_submission', [ $this, 'track_gravity_forms_submission' ], 10 ); } /** * Enqueues the required JavaScript for form submissions integration. * @return void + * * @codeCoverageIgnore because there's nothing to test here. */ public function add_js() { @@ -60,10 +68,13 @@ public function add_js() { /** * Tracks the form submission if CF7 says it's valid. * + * @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. * * @return \WPCF7_Validation + * * @codeCoverageIgnore because we can't test XHR requests here. */ public function maybe_track_submission( $result, $tags ) { @@ -73,15 +84,51 @@ public function maybe_track_submission( $result, $tags ) { $post = get_post( $_POST[ '_wpcf7_container_post' ] ); $uri = '/' . $post->post_name . '/'; - $proxy = new Proxy( false ); - $proxy->do_request( - __( 'WP Form Completions', 'plausible-analytics' ), - null, - null, - [ 'path' => $uri ] - ); + $this->track_submission( $uri ); } return $result; } + + /** + * Track submission using the Proxy. + * + * @param $uri + * + * @return void + * + * @codeCoverageIgnore because we can't test XHR requests here. + */ + private function track_submission( $uri ) { + $proxy = new Proxy( false ); + + $proxy->do_request( + __( 'WP Form Completions', 'plausible-analytics' ), + null, + null, + [ 'path' => $uri ] + ); + } + + /** + * Compatibility fix for Gravity Forms. + * + * @action gform_after_submission + * + * @param $form + * @param $entry + * + * @return void + * + * @codeCoverageIgnore because we can't test XHR requests here. + */ + public function track_gravity_forms_submission( $form ) { + $uri = str_replace( home_url(), '', $form[ 'source_url' ] ) ?? ''; + + if ( empty( $uri ) ) { + return; + } + + $this->track_submission( $uri ); + } } diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index eac2f576..32eed402 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -69,8 +69,10 @@ private function init( $init ) { */ add_action( 'woocommerce_before_add_to_cart_quantity', [ $this, 'add_cart_form_hidden_input' ] ); add_action( 'woocommerce_after_add_to_cart_form', [ $this, 'track_add_to_cart_on_product_page' ] ); - add_filter( 'woocommerce_store_api_validate_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 2 ); - add_filter( 'woocommerce_ajax_added_to_cart', [ $this, 'track_ajax_add_to_cart' ] ); + add_action( 'woocommerce_store_api_validate_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 2 ); + add_action( 'woocommerce_ajax_added_to_cart', [ $this, 'track_ajax_add_to_cart' ] ); + /** @see \WC_Form_Handler::add_to_cart_action() runs on priority 20. We need to run before that, in case redirect is enabled. */ + add_action( 'wp_loaded', [ $this, 'track_direct_add_to_cart' ], 19 ); add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); add_action( 'wp_head', [ $this, 'track_entered_checkout' ] ); add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); @@ -173,22 +175,22 @@ public function track_add_to_cart_on_product_page() { } /** - * Track (non-Interactivity API, i.e. AJAX) add to cart events. - * - * @param string|int $product_id ID of the product added to the cart. + * Track add to cart actions by direct link, e.g. ?product_type=download&add-to-cart=1&quantity=1 * * @return void * - * @codeCoverageIgnore Because we can't test XHR requests here. + * @codeCoverageIgnore Because we can't test XHR 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, - ]; + public function track_direct_add_to_cart() { + if ( ! isset( $_REQUEST[ 'add-to-cart' ] ) || ! is_numeric( wp_unslash( $_REQUEST[ 'add-to-cart' ] ) ) ) { + return; + } - $this->track_add_to_cart( $product, $add_to_cart_data ); + $product_id = absint( wp_unslash( $_REQUEST[ 'add-to-cart' ] ) ); + $product = wc_get_product( $product_id ); + $quantity = isset( $_REQUEST[ 'quantity' ] ) ? absint( wp_unslash( $_REQUEST[ 'quantity' ] ) ) : 1; + + $this->track_add_to_cart( $product, [ 'id' => $product_id, 'quantity' => $quantity ] ); } /** @@ -202,6 +204,10 @@ public function track_ajax_add_to_cart( $product_id ) { * @codeCoverageIgnore Because we can't test XHR requests here. */ public function track_add_to_cart( $product, $add_to_cart_data ) { + if ( ! $product ) { + return; + } + $product_data = $this->clean_data( $product->get_data() ); $added_to_cart = $this->clean_data( $add_to_cart_data ); $cart = WC()->cart; @@ -241,6 +247,25 @@ private function clean_data( $product ) { return $product; } + /** + * 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. * diff --git a/src/Plugin.php b/src/Plugin.php index 2bbaa97e..5a893d4e 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -16,11 +16,22 @@ final class Plugin { * @return void */ public function register() { + $this->setup(); + // Register services used throughout the plugin. (WP Rocket runs at priority 10) add_action( 'plugins_loaded', [ $this, 'register_services' ], 9 ); // Load text domain. - add_action( 'init', [ $this, 'load_plugin_textdomain' ] ); + add_action( 'init', [ $this, 'load_plugin_textdomain' ], 1000 ); + } + + /** + * Register plugin (de)activation hooks and cron job. + * + * @return void + */ + public function setup() { + new Setup(); } /** @@ -31,23 +42,57 @@ public function register() { * @return void */ public function register_services() { + if ( is_admin() ) { + add_action( 'init', [ $this, 'load_settings' ] ); + add_action( 'init', [ $this, 'load_provisioning' ] ); + new Admin\Upgrades(); - new Admin\Settings\Page(); new Admin\Filters(); new Admin\Actions(); new Admin\Module(); - new Admin\Provisioning(); - new Admin\Provisioning\Integrations(); } - new Integrations(); + add_action( 'init', [ $this, 'load_integrations' ] ); new Actions(); new Ajax(); new Compatibility(); new Filters(); new Proxy(); - new Setup(); + } + + /** + * Load @see Admin\Settings\Page() + * + * @return void + * + * @codeCoverageIgnore + */ + public function load_settings() { + new Admin\Settings\Page(); + } + + /** + * Load @see Admin\Provisioning() + * + * @return void + * + * @codeCoverageIgnore + */ + public function load_provisioning() { + new Admin\Provisioning(); + new Admin\Provisioning\Integrations(); + } + + /** + * Load @see Integrations() + * + * @return void + * + * @codeCoverageIgnore + */ + public function load_integrations() { + new Integrations(); } /** diff --git a/src/Proxy.php b/src/Proxy.php index dd1c7dde..1012cbde 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -112,6 +112,11 @@ public function do_request( $name = 'pageview', $domain = '', $url = '', $props 'u' => $url ?: wp_get_referer(), ]; + // URL is required, so if no $url was set and no referer was found, attempt to create it from the REQUEST_URI server variable. + if ( empty( $body[ 'u' ] ) ) { + $body[ 'u' ] = $this->generate_event_url(); // @codeCoverageIgnore + } + // Revenue events use a different approach. if ( isset( $props[ 'revenue' ] ) ) { $body[ 'revenue' ] = reset( $props ); // @codeCoverageIgnore @@ -124,6 +129,23 @@ public function do_request( $name = 'pageview', $domain = '', $url = '', $props return $this->send_event( $request ); } + /** + * Attempts to generate the Event URL from available resources. + * + * @return string + */ + public function generate_event_url() { + $url = ''; + $parts = parse_url( $_SERVER[ 'REQUEST_URI' ] ); + $home_url_parts = parse_url( get_home_url() ); + + if ( isset( $home_url_parts[ 'scheme' ] ) && isset( $home_url_parts[ 'host' ] ) && isset( $parts[ 'path' ] ) ) { + $url = $home_url_parts[ 'scheme' ] . '://' . $home_url_parts [ 'host' ] . $parts[ 'path' ]; + } + + return $url; + } + /** * Formats and sends $request to the Plausible API. * diff --git a/src/Setup.php b/src/Setup.php index 45122b18..dd1f293a 100644 --- a/src/Setup.php +++ b/src/Setup.php @@ -10,13 +10,6 @@ namespace Plausible\Analytics\WP; class Setup { - /** - * Cron job handle - * - * @var string - */ - private $cron = 'plausible_analytics_update_js'; - /** * Filters and Hooks. * @@ -28,7 +21,7 @@ public function __construct() { register_deactivation_hook( PLAUSIBLE_ANALYTICS_PLUGIN_FILE, [ $this, 'deactivate_cron' ] ); // Attach the cron script to the cron action. - add_action( $this->cron, [ $this, 'load_cron_script' ] ); + add_action( Cron::TASK_NAME, [ $this, 'load_cron_script' ] ); // This assures that the local file is downloaded/updated when settings are saved. add_action( 'plausible_analytics_settings_saved', [ $this, 'load_cron_script' ] ); @@ -51,8 +44,8 @@ public function create_cache_dir() { * @codeCoverageIgnore */ public function activate_cron() { - if ( ! wp_next_scheduled( $this->cron ) ) { - wp_schedule_event( time(), 'daily', $this->cron ); + if ( ! wp_next_scheduled( Cron::TASK_NAME ) ) { + wp_schedule_event( time(), 'daily', Cron::TASK_NAME ); } } @@ -62,8 +55,8 @@ public function activate_cron() { * @codeCoverageIgnore */ public function deactivate_cron() { - if ( wp_next_scheduled( $this->cron ) ) { - wp_clear_scheduled_hook( $this->cron ); + if ( wp_next_scheduled( Cron::TASK_NAME ) ) { + wp_clear_scheduled_hook( Cron::TASK_NAME ); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 084027d1..83cf3c26 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -36,6 +36,19 @@ public function enableRevenue( $settings ) { return $settings; } + /** + * Enable Enhanced Measurements > IE Compatibility + * + * @param $settings + * + * @return mixed + */ + public function enableCompat( $settings ) { + $settings[ 'enhanced_measurements' ] = [ 'compat' ]; + + return $settings; + } + /** * Enable form completions by modifying the settings array. * diff --git a/tests/integration/Admin/ProvisioningTest.php b/tests/integration/Admin/ProvisioningTest.php index aeed673b..71db0c2a 100644 --- a/tests/integration/Admin/ProvisioningTest.php +++ b/tests/integration/Admin/ProvisioningTest.php @@ -123,4 +123,39 @@ public function testCreateGoalRequest() { $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestCustomEvent', $custom_event ); } + + /** + * @see Provisioning::maybe_enable_customer_user_roles() + * @return void + */ + public function testMaybeEnableCustomerUserRole() { + $class = new Provisioning( false ); + $settings = []; + $settings[ 'enhanced_measurements' ] = [ 'revenue' ]; + $settings[ 'tracked_user_roles' ] = []; + + add_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + + $new_settings = $class->maybe_enable_customer_user_roles( $settings ); + + remove_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + + $this->assertTrue( in_array( 'customer', $new_settings[ 'tracked_user_roles' ] ) ); + + add_filter( 'plausible_analytics_integrations_edd', '__return_true' ); + + $new_settings = $class->maybe_enable_customer_user_roles( $settings ); + + remove_filter( 'plausible_analytics_integrations_edd', '__return_true' ); + + $this->assertTrue( in_array( 'subscriber', $new_settings[ 'tracked_user_roles' ] ) ); + + add_filter( 'plausible_analytics_integrations_edd_recurring', '__return_true' ); + + $new_settings = $class->maybe_enable_customer_user_roles( $settings ); + + remove_filter( 'plausible_analytics_integrations_edd_recurring', '__return_true' ); + + $this->assertTrue( in_array( 'edd_subscriber', $new_settings[ 'tracked_user_roles' ] ) ); + } } diff --git a/tests/integration/Admin/UpgradesTest.php b/tests/integration/Admin/UpgradesTest.php index 183cee27..4227c976 100644 --- a/tests/integration/Admin/UpgradesTest.php +++ b/tests/integration/Admin/UpgradesTest.php @@ -7,6 +7,7 @@ use Plausible\Analytics\Tests\TestCase; use Plausible\Analytics\WP\Admin\Upgrades; +use Plausible\Analytics\WP\Cron; use Plausible\Analytics\WP\Helpers; class UpgradesTest extends TestCase { @@ -27,4 +28,15 @@ public function testUpgradeTo210() { $this->assertIsArray( $enhanced_measurements ); } + + /** + * @see Upgrades::upgrade_to_231() + * @return void + */ + public function testUpgradeTo231() { + $class = new Upgrades(); + $class->upgrade_to_231(); + + $this->assertNotEmpty( wp_next_scheduled( Cron::TASK_NAME ) ); + } } diff --git a/tests/unit/ClientFactoryTest.php b/tests/integration/ClientFactoryTest.php similarity index 92% rename from tests/unit/ClientFactoryTest.php rename to tests/integration/ClientFactoryTest.php index 74486046..01413679 100644 --- a/tests/unit/ClientFactoryTest.php +++ b/tests/integration/ClientFactoryTest.php @@ -3,7 +3,7 @@ * @package Plausible Analytics Unit Tests - ClientFactory */ -namespace Plausible\Analytics\Tests\Unit; +namespace Plausible\Analytics\Tests\Integration; use Plausible\Analytics\Tests\TestCase; use Plausible\Analytics\WP\Client; diff --git a/tests/integration/FiltersTest.php b/tests/integration/FiltersTest.php index ac99f5c4..e448126b 100644 --- a/tests/integration/FiltersTest.php +++ b/tests/integration/FiltersTest.php @@ -18,6 +18,15 @@ public function testAddPlausibleAttributes() { $this->assertStringContainsString( 'example.org', $tag ); $this->assertStringContainsString( 'plausible.io/api/event', $tag ); + $this->assertStringContainsString( 'plausible-analytics-js', $tag ); + + add_filter( 'plausible_analytics_settings', [ $this, 'enableCompat' ] ); + + $class = new Filters(); + $tag = $class->add_plausible_attributes( '