' . __(
'Create a secure and private shared link in your Plausible account.',
'plausible-analytics'
@@ -444,7 +431,7 @@ public function __construct() {
'type' => 'text',
'value' => $settings['self_hosted_shared_link'],
'placeholder' => sprintf(
- // translators: 1: Plausible hosted domain URL, 2: Site domain name.
+ // translators: 1: Plausible hosted domain URL, 2: Site domain name.
wp_kses( __( 'E.g. %1$s/share/%2$s?auth=XXXXXXXXXXXX', 'plausible-analytics' ), 'post' ),
Helpers::get_hosted_domain_url(),
Helpers::get_domain()
@@ -474,7 +461,8 @@ public function __construct() {
}
/**
- * If proxy is enabled, or self-hosted domain has a value, display warning box.
+ * If the proxy is enabled, or the self-hosted domain option has a value, display a warning box.
+ *
* @see self::proxy_warning()
*/
if ( Helpers::proxy_enabled() || ! empty( $settings['self_hosted_domain'] ) ) {
@@ -492,17 +480,44 @@ public function __construct() {
/**
* No Plugin Token is entered.
*/
- if ( empty( $settings['api_token'] ) ) {
+ if ( empty( Helpers::get_settings()['api_token'][ Helpers::get_current_language_domain_key() ] ) ) {
$this->fields['general'][0]['fields'][] = self::API_TOKEN_MISSING_HOOK;
$this->fields['general'][3]['fields'][] = self::OPTION_DISABLED_BY_MISSING_API_TOKEN_HOOK;
}
/**
- * If View Stats is enabled, display notice.
+ * If View Stats is enabled, display a notice.
*/
- if ( ! empty( $settings['api_token'] ) && ! empty( $settings['enable_analytics_dashboard'] ) ) {
+ if ( ! empty( Helpers::get_settings()['api_token'][ Helpers::get_current_language_domain_key() ] ) && ! empty( $settings['enable_analytics_dashboard'] ) ) {
$this->fields['general'][3]['fields'][] = self::ENABLE_ANALYTICS_DASH_NOTICE;
}
+
+ $this->wizard_fields = [
+ 'domain_name' => [
+ 'label' => esc_html__( 'Domain name', 'plausible-analytics' ),
+ 'slug' => 'domain_name[default]',
+ 'type' => 'text',
+ 'value' => Helpers::get_domain(),
+ ],
+ 'api_token' => [
+ 'label' => sprintf(
+ '%s - %s',
+ esc_html__( 'Plugin Token', 'plausible-analytics' ),
+ __( 'Create Token', 'plausible-analytics' )
+ ),
+ 'slug' => 'api_token[default]',
+ 'type' => 'text',
+ 'value' => Helpers::get_api_token(),
+ ],
+ 'connect_plausible_analytics' => [
+ 'label' => empty( Helpers::get_domain() ) || empty( Helpers::get_api_token() )
+ ? esc_html__( 'Connect', 'plausible-analytics' )
+ : esc_html__( 'Connected', 'plausible-analytics' ),
+ 'slug' => 'connect_plausible_analytics',
+ 'type' => 'button',
+ 'disabled' => empty( Helpers::get_domain() ) || empty( Helpers::get_api_token() ),
+ ],
+ ];
}
/**
@@ -555,6 +570,16 @@ private function build_user_roles_array( $slug, $disable_elements = [] ) {
return $roles_array;
}
+ /**
+ * A little hack to add some classes to the core #wpcontent div.
+ * @return void
+ */
+ public function add_background_color() {
+ if ( array_key_exists( 'page', $_GET ) && $_GET['page'] == 'plausible_analytics' ) {
+ echo "";
+ }
+ }
+
/**
* Register Menu.
* @since 1.0.0
@@ -623,16 +648,6 @@ public function register_menu() {
);
}
- /**
- * A little hack to add some classes to the core #wpcontent div.
- * @return void
- */
- public function add_background_color() {
- if ( array_key_exists( 'page', $_GET ) && $_GET['page'] == 'plausible_analytics' ) {
- echo "";
- }
- }
-
/**
* Statistics Page via Embed feature.
* @since 1.2.0
@@ -642,20 +657,18 @@ public function add_background_color() {
public function render_analytics_dashboard() {
global $current_user;
- $settings = Helpers::get_settings();
- $analytics_enabled = $settings['enable_analytics_dashboard'];
- $shared_link = $settings['shared_link'] ?: '';
- $self_hosted = ! empty( $settings ['self_hosted_domain'] );
+ $settings = Helpers::get_settings();
+ $language_domain_key = $_GET['domain'] ?? 'default';
+ $analytics_enabled = $settings['enable_analytics_dashboard'];
+ $shared_link = $settings['shared_link'][ $language_domain_key ] ?? $settings['shared_link']['default'] ?? '';
+ $self_hosted = ! empty( $settings ['self_hosted_domain'] );
if ( $self_hosted ) {
$shared_link = $settings['self_hosted_shared_link'];
}
$has_access = false;
- $user_roles_have_access = ! empty( $settings['expand_dashboard_access'] ) ? array_merge(
- [ 'administrator' ],
- $settings['expand_dashboard_access']
- ) : [ 'administrator' ];
+ $user_roles_have_access = ! empty( $settings['expand_dashboard_access'] ) ? array_merge( [ 'administrator' ], $settings['expand_dashboard_access'] ) : [ 'administrator' ];
foreach ( $current_user->roles as $role ) {
if ( in_array( $role, $user_roles_have_access, true ) ) {
@@ -683,19 +696,18 @@ public function render_analytics_dashboard() {
endif;
/**
- * Prior to this version, the default value would contain an example "auth" key, i.e. XXXXXXXXX.
- * When this option was saved to the database, underlying code would fail, throwing a CORS related error in browsers.
- * Now, we explicitly check for the existence of this example "auth" key, and display a human-readable error message to
+ * @since v1.2.5 Prior to this version, the default value would contain an example "auth" key, i.e., XXXXXXXXX.
+ * When this option was saved to the database, underlying code would fail, throwing a CORS-related error in browsers.
+ * Now, we explicitly check for the existence of this example "auth" key and display a human-readable error message to
* those who haven't properly set it up.
- * @since v1.2.5
- * For self-hosters the View Stats option doesn't need to be enabled, if a Shared Link is entered, we can assume they want to View Stats.
- * For regular users, the shared link is provisioned by the API, so it shouldn't be empty.
- * @since v2.0.3
+ *
+ * @since v2.0.3 For self-hosters the View Stats option doesn't need to be enabled, if a Shared Link is entered, we can assume they want to View Stats.
+ * For regular users, the API provisions the shared link, so it shouldn't be empty.
*/
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.
+ // Append an individual page URL if it exists.
if ( $shared_link && $page_url ) {
$shared_link .= "&page={$page_url}";
}
@@ -740,7 +752,7 @@ public function render_analytics_dashboard() {
Shared Link under Self-Hosted Settings.',
'plausible-analytics'
@@ -749,7 +761,7 @@ public function render_analytics_dashboard() {
); ?>
click here to enable View Stats in WordPress.',
'plausible-analytics'
diff --git a/src/AdminBar.php b/src/AdminBar.php
index 18300f4e..fb6e372f 100644
--- a/src/AdminBar.php
+++ b/src/AdminBar.php
@@ -28,10 +28,11 @@ private function init() {
/**
* Create admin bar nodes.
*
+ * @since 1.3.0
+ *
* @param \WP_Admin_Bar $admin_bar Admin bar object.
*
* @return void
- * @since 1.3.0
*/
public function admin_bar_node( $admin_bar ) {
$disable = ! empty( Helpers::get_settings()['disable_toolbar_menu'] );
@@ -65,7 +66,7 @@ public function admin_bar_node( $admin_bar ) {
[
'id' => 'plausible-analytics',
'title' => 'Plausible Analytics',
- ]
+ ],
], $settings );
foreach ( $args as $arg ) {
@@ -77,17 +78,34 @@ public function admin_bar_node( $admin_bar ) {
* Adds the View Analytics link to the Admin Bar Menu if any of the related settings are enabled.
*
* @param $args
+ * @param $settings
*
* @return mixed
*/
public function maybe_add_analytics( $args, $settings ) {
if ( ! empty( $settings['enable_analytics_dashboard'] ) || ( ! empty( $settings['self_hosted_domain'] ) && ! empty( $settings['self_hosted_shared_link'] ) ) ) {
- $args[] = [
- 'id' => 'view-analytics',
- 'title' => esc_html__( 'View Analytics', 'plausible-analytics' ),
- 'href' => admin_url( 'index.php?page=plausible_analytics_statistics' ),
- 'parent' => 'plausible-analytics',
- ];
+ $language_domains = Helpers::get_language_domains();
+
+ foreach ( $language_domains as $key => $language_domain ) {
+ $domain_key = $key !== 'default' ? $key : '';
+
+ if ( $domain_key && empty( $settings['shared_link'][ $domain_key ] ) ) {
+ continue; // @codeCoverageIgnore
+ }
+
+ $href = add_query_arg( 'page', 'plausible_analytics_statistics', admin_url( 'index.php' ) );
+
+ if ( $domain_key ) {
+ $href = add_query_arg( 'domain', $domain_key, $href );
+ }
+
+ $args[] = [
+ 'id' => "view-analytics-$key",
+ 'title' => sprintf( esc_html__( 'View analytics for %s', 'plausible-analytics' ), $language_domain ),
+ 'href' => $href,
+ 'parent' => 'plausible-analytics',
+ ];
+ }
// Add a link to individual page stats.
if ( $this->is_singular() ) {
@@ -108,7 +126,7 @@ public function maybe_add_analytics( $args, $settings ) {
}
}
- return $args;
+ return apply_filters( 'plausible_analytics_admin_bar_view_analytics', $args );
}
/**
diff --git a/src/Ajax.php b/src/Ajax.php
index 2f05eea0..dd270803 100644
--- a/src/Ajax.php
+++ b/src/Ajax.php
@@ -11,6 +11,7 @@
use Plausible\Analytics\WP\Admin\Messages;
use Plausible\Analytics\WP\Admin\Settings\Hooks;
+use Plausible\Analytics\WP\Admin\Settings\OptionsParser;
use Plausible\Analytics\WP\Admin\Settings\Page;
use Plausible\Analytics\WP\Client\ApiException;
@@ -37,57 +38,48 @@ private function init() {
add_action( 'wp_ajax_plausible_analytics_toggle_option', [ $this, 'toggle_option' ] );
add_action( 'wp_ajax_plausible_analytics_save_options', [ $this, 'save_options' ] );
add_action( 'wp_ajax_plausible_analytics_bulk_toggle', [ $this, 'bulk_toggle_options' ] );
+ add_action( 'wp_ajax_plausible_analytics_dismiss_multilang_notice', [ $this, 'dismiss_multilang_notice' ] );
}
- /**
- * Returns an array of messages fetched from transients for display by JS.
- */
- public function fetch_messages() {
- $notice = get_transient( Messages::NOTICE_TRANSIENT );
- $error = get_transient( Messages::ERROR_TRANSIENT );
- $success = get_transient( Messages::SUCCESS_TRANSIENT );
- $additional = get_transient( Messages::ADDITIONAL_TRANSIENT ) ?: [];
- $additional_message = [];
+ public function bulk_toggle_options() {
+ $post_data = $this->clean( $_POST );
+ $settings = Helpers::get_settings();
- if ( ! empty( $additional ) ) {
- $additional_message = [
- 'id' => array_key_first( $additional ),
- 'message' => $additional[ array_key_first( $additional ) ],
- ];
+ if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data['_nonce'], 'plausible_analytics_toggle_option' ) < 1 ) {
+ wp_send_json_error( __( 'Not allowed.', 'plausible-analytics' ), 403 );
}
- $messages = apply_filters(
- 'plausible_analytics_messages',
- [
- 'notice' => $notice,
- 'error' => $error,
- 'success' => $success,
- 'additional' => $additional_message,
- ]
- );
+ $options = json_decode( $post_data['options'], true );
- wp_send_json_success( $messages, 200 );
- }
+ if ( empty( $options ) ) {
+ wp_send_json_error( __( 'No options found.', 'plausible-analytics' ), 400 );
+ }
- /**
- * Mark the wizard as finished, so it won't appear again, and optionally redirect.
- *
- * @return void
- */
- public function quit_wizard() {
- $request_data = $this->clean( $_REQUEST );
+ foreach ( $options as $option ) {
+ $name = sanitize_text_field( $option['name'] );
+ $value = sanitize_text_field( $option['value'] );
+ $status = sanitize_text_field( $option['status'] );
- if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_quit_wizard' ) < 1 ) {
- Messages::set_error( __( 'Not allowed', 'plausible-analytics' ) );
+ if ( ! isset( $settings[ $name ] ) || ! is_array( $settings[ $name ] ) ) {
+ continue;
+ }
- wp_send_json_error( null, 403 );
+ if ( $status === 'on' ) {
+ if ( ! in_array( $value, $settings[ $name ] ) ) {
+ $settings[ $name ][] = $value;
+ }
+ } else {
+ if ( ( $key = array_search( $value, $settings[ $name ] ) ) !== false ) {
+ unset( $settings[ $name ][ $key ] );
+ }
+ }
}
- update_option( 'plausible_analytics_wizard_done', true );
+ update_option( 'plausible_analytics_settings', $settings );
- $this->maybe_handle_redirect( $request_data['redirect'] );
+ Messages::set_success( __( 'Settings saved.', 'plausible-analytics' ) );
- wp_send_json_success();
+ wp_send_json_success( null, 200 );
}
/**
@@ -140,181 +132,97 @@ private function clean( $var, $key = '' ) {
}
/**
- * Makes the AJAX request redirect instead of e.g. return JSON.
- *
- * @param $direction
+ * Dismiss Multilang Notice
*
* @return void
- *
- * @codeCoverageIgnore
*/
- private function maybe_handle_redirect( $direction ) {
- if ( ! empty( $direction ) ) {
- $url = admin_url( 'options-general.php?page=plausible_analytics' );
+ public function dismiss_multilang_notice() {
+ $request_data = $this->clean( $_REQUEST );
- // Redirect param points to a specific option.
- if ( strpos( $direction, 'self-hosted' ) !== false ) {
- $url .= '&tab=' . $direction;
- } elseif ( $direction !== '1' ) {
- $url .= '#' . $direction;
- }
+ if ( ! current_user_can( 'manage_options' ) || empty( $request_data['_nonce'] ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_dismiss_multilang_notice' ) < 1 ) {
+ wp_send_json_error( __( 'Not allowed.', 'plausible-analytics' ), 403 );
+ }
- wp_redirect( $url );
+ update_option( 'plausible_analytics_multilang_notice_dismissed', true, false );
- exit;
- }
+ wp_send_json_success();
}
/**
- * Removes the plausible_analytics_wizard_done row from the wp_options table, effectively displaying the wizard on next page load.
- *
- * @return void
+ * Returns an array of messages fetched from transients for display by JS.
*/
- public function show_wizard() {
- $request_data = $this->clean( $_REQUEST );
-
- if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_show_wizard' ) < 1 ) {
- Messages::set_error( __( 'Not allowed.', 'plausible-analytics' ) );
+ public function fetch_messages() {
+ $notice = get_transient( Messages::NOTICE_TRANSIENT );
+ $error = get_transient( Messages::ERROR_TRANSIENT );
+ $success = get_transient( Messages::SUCCESS_TRANSIENT );
+ $additional = get_transient( Messages::ADDITIONAL_TRANSIENT ) ?: [];
+ $additional_message = [];
- wp_send_json_error( null, 403 );
+ if ( ! empty( $additional ) ) {
+ $additional_message = [
+ 'id' => array_key_first( $additional ),
+ 'message' => $additional[ array_key_first( $additional ) ],
+ ];
}
- delete_option( 'plausible_analytics_wizard_done' );
-
- $this->maybe_handle_redirect( $request_data['redirect'] );
+ $messages = apply_filters(
+ 'plausible_analytics_messages',
+ [
+ 'notice' => $notice,
+ 'error' => $error,
+ 'success' => $success,
+ 'additional' => $additional_message,
+ ]
+ );
- wp_send_json_success();
+ wp_send_json_success( $messages, 200 );
}
/**
- * Save Admin Settings
+ * Mark the wizard as finished, so it won't appear again and optionally redirect.
*
- * @since 1.0.0
* @return void
*/
- public function toggle_option() {
- // Sanitize all the post data before using.
- $post_data = $this->clean( $_POST );
- $settings = Helpers::get_settings();
+ public function quit_wizard() {
+ $request_data = $this->clean( $_REQUEST );
- if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data['_nonce'], 'plausible_analytics_toggle_option' ) < 1 ) {
- wp_send_json_error( __( 'Not allowed.', 'plausible-analytics' ), 403 );
- }
+ if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_quit_wizard' ) < 1 ) {
+ Messages::set_error( __( 'Not allowed', 'plausible-analytics' ) );
- if ( $post_data['is_list'] ) {
- /**
- * 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 ] );
- }
- }
- } else {
- /**
- * Single toggles.
- */
- $settings[ $post_data['option_name'] ] = $post_data['toggle_status'];
+ wp_send_json_error( null, 403 );
}
- // Update all the options to plausible settings.
- update_option( 'plausible_analytics_settings', $settings );
-
- /**
- * Allow devs to perform additional actions.
- */
- do_action( 'plausible_analytics_settings_saved', $settings, $post_data['option_name'], $post_data['toggle_status'] );
-
- $option_label = $post_data['option_label'];
- $toggle_status = $post_data['toggle_status'] === 'on' ? __( 'enabled', 'plausible-analytics' ) : __( 'disabled', 'plausible-analytics' );
- $message = apply_filters(
- 'plausible_analytics_toggle_option_success_message',
- sprintf( '%s %s.', $option_label, $toggle_status ),
- $post_data['option_name'],
- $post_data['toggle_status']
- );
-
- Messages::set_success( $message );
-
- $additional = $this->maybe_render_additional_message( $post_data['option_name'], $post_data['toggle_status'] );
+ update_option( 'plausible_analytics_wizard_done', true );
- Messages::set_additional( $additional, $post_data['option_name'] );
+ $this->maybe_handle_redirect( $request_data['redirect'] );
- wp_send_json_success( null, 200 );
+ wp_send_json_success();
}
/**
- * Adds the 'additional' array element to $message if applicable.
+ * Makes the AJAX request redirect instead of e.g. return JSON.
*
- * @param $option_name
- * @param $option_value
+ * @param $direction
*
- * @return string
+ * @return void
+ *
+ * @codeCoverageIgnore
*/
- private function maybe_render_additional_message( $option_name, $option_value ) {
- $additional_message_html = '';
- $hooks = new Hooks( false );
-
- if ( $option_name === 'proxy_enabled' && $option_value !== '' ) {
- $additional_message_html = $hooks->render_hook_field( Page::PROXY_WARNING_HOOK );
- }
-
- if ( $option_name === 'enable_analytics_dashboard' && $option_value !== '' ) {
- $additional_message_html = $hooks->render_hook_field( Page::ENABLE_ANALYTICS_DASH_NOTICE );
- }
-
- if ( $option_name === 'api_token' && $option_value === '' ) {
- $additional_message_html = $hooks->render_hook_field( Page::API_TOKEN_MISSING_HOOK );
- }
-
- return $additional_message_html;
- }
-
- public function bulk_toggle_options() {
- $post_data = $this->clean( $_POST );
- $settings = Helpers::get_settings();
-
- if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data['_nonce'], 'plausible_analytics_toggle_option' ) < 1 ) {
- wp_send_json_error( __( 'Not allowed.', 'plausible-analytics' ), 403 );
- }
-
- $options = json_decode( $post_data['options'], true );
-
- if ( empty( $options ) ) {
- wp_send_json_error( __( 'No options found.', 'plausible-analytics' ), 400 );
- }
-
- foreach ( $options as $option ) {
- $name = sanitize_text_field( $option['name'] );
- $value = sanitize_text_field( $option['value'] );
- $status = sanitize_text_field( $option['status'] );
-
- if ( ! isset( $settings[ $name ] ) || ! is_array( $settings[ $name ] ) ) {
- continue;
- }
+ private function maybe_handle_redirect( $direction ) {
+ if ( ! empty( $direction ) ) {
+ $url = admin_url( 'options-general.php?page=plausible_analytics' );
- if ( $status === 'on' ) {
- if ( ! in_array( $value, $settings[ $name ] ) ) {
- $settings[ $name ][] = $value;
- }
- } else {
- if ( ( $key = array_search( $value, $settings[ $name ] ) ) !== false ) {
- unset( $settings[ $name ][ $key ] );
- }
+ // Redirect param points to a specific option.
+ if ( strpos( $direction, 'self-hosted' ) !== false ) {
+ $url .= '&tab=' . $direction;
+ } elseif ( $direction !== '1' ) {
+ $url .= '#' . $direction;
}
- }
-
- update_option( 'plausible_analytics_settings', $settings );
- Messages::set_success( __( 'Settings saved.', 'plausible-analytics' ) );
+ wp_redirect( $url );
- wp_send_json_success( null, 200 );
+ exit;
+ }
}
/**
@@ -347,34 +255,22 @@ public function save_options() {
}
/**
- * If we're dealing with an array of inputs (e.g. item[0], item[1], etc.), we need to convert $options , before storing it in the database.
- *
- * @since 2.4.0
+ * Convert the object to array before parsing.
*/
- $input_array_elements = array_filter(
- $options,
- function ( $option ) {
- return preg_match( '/\[[0-9]+]/', $option->name );
- }
- );
-
- if ( count( $input_array_elements ) > 0 ) {
- $options = [];
- $array_name = preg_replace( '/\[[0-9]+]/', '', $input_array_elements[0]->name );
- $options[0] = (object) [];
- $options[0]->name = $array_name;
- $options[0]->value = [];
+ $options_to_parse = [];
- foreach ( $input_array_elements as $input_array_element ) {
- if ( $input_array_element->value ) {
- $options[0]->value[] = $input_array_element->value;
- }
- }
+ foreach ( $options as $option ) {
+ $options_to_parse[] = (array) $option;
}
+ $parsed_options = OptionsParser::parse_keyed_options( $options_to_parse, $settings );
+ $options = $parsed_options['options'];
+ $posted_values = $parsed_options['posted_values'];
+ $posted_keys = $parsed_options['posted_keys'];
+
foreach ( $options as $option ) {
- $name = sanitize_text_field( $option->name );
- $value = $this->clean( $option->value );
+ $name = sanitize_text_field( $option['name'] );
+ $value = $this->clean( $option['value'] );
// Clean spaces
if ( is_string( $value ) ) {
@@ -385,14 +281,46 @@ function ( $option ) {
// Validate Plugin Token if this is the Plugin Token field.
if ( $name === 'api_token' ) {
- $this->validate_api_token( $value );
+ if ( isset( $posted_values['api_token'] ) ) {
+ /**
+ * Multilingual plugin compatibility.
+ */
+ $language_domain_key = $posted_keys['api_token'] ?? 'default';
+ $posted_domain = $settings['domain_name'][ $language_domain_key ] ?? '';
+
+ if ( isset( $posted_keys['domain_name'], $posted_values['domain_name'] ) && $posted_keys['domain_name'] === $language_domain_key ) {
+ $posted_domain = $this->clean( $posted_values['domain_name'] );
+ }
+
+ // 1. Force get_current_multilang_key() to return the key we're saving for.
+ $force_key = function () use ( $language_domain_key ) {
+ return $language_domain_key;
+ };
+
+ // 2. Force get_domain() to find the not-yet-persisted domain name under that key.
+ $force_domain = function ( $s ) use ( $posted_domain, $language_domain_key ) {
+ $s['domain_name'][ $language_domain_key ] = $posted_domain;
- $additional = $this->maybe_render_additional_message( $name, $value );
+ return $s;
+ };
- Messages::set_additional( $additional, $name );
+ add_filter( 'plausible_analytics_current_language_domain_key', $force_key );
+ add_filter( 'plausible_analytics_settings', $force_domain );
+
+ $this->validate_api_token( $this->clean( $posted_values['api_token'] ), $language_domain_key );
+
+ remove_filter( 'plausible_analytics_current_language_domain_key', $force_key );
+ remove_filter( 'plausible_analytics_settings', $force_domain );
+ } else {
+ $this->validate_api_token( $value );
+
+ $additional = $this->maybe_render_additional_message( $name, $value );
+
+ Messages::set_additional( $additional, $name );
+ }
}
- // Refresh Tracker ID if Domain Name has changed (e.g. after migration from staging to production)
+ // Refresh Tracker ID if Domain Name has changed (e.g., after migration from staging to production)
if ( $name === 'domain_name' ) {
delete_option( 'plausible_analytics_tracker_id' );
}
@@ -402,6 +330,11 @@ function ( $option ) {
Messages::set_success( __( 'Settings saved.', 'plausible-analytics' ) );
+ /**
+ * Allow devs to perform additional actions.
+ */
+ do_action( 'plausible_analytics_settings_saved', $settings );
+
if ( ! defined( 'PLAUSIBLE_CI' ) ) {
wp_send_json_success( null, 200 );
}
@@ -411,12 +344,13 @@ function ( $option ) {
* Validate the entered Plugin Token, before storing it to the DB. wp_send_json_error() ensures that code execution stops.
*
* @param string $token
+ * @param string $domain_key
*
* @return void
* @throws ApiException
*/
- private function validate_api_token( $token = '' ) {
- $client_factory = new ClientFactory( $token );
+ private function validate_api_token( $token = '', $domain_key = '' ) {
+ $client_factory = new ClientFactory( $token, $domain_key );
$client = $client_factory->build();
if ( $client instanceof Client && ! $client->validate_api_token() ) {
@@ -425,7 +359,7 @@ private function validate_api_token( $token = '' ) {
Messages::set_error(
sprintf(
- // translators: 1: URL to create a new plugin token, 2: URL to read more.
+ // translators: 1: URL to create a new plugin token, 2: URL to read more.
__(
'Oops! The Plugin Token you used is invalid. Please create a new token. Read more',
'plausible-analytics'
@@ -438,4 +372,115 @@ private function validate_api_token( $token = '' ) {
wp_send_json_error( 'invalid_api_token', 400 );
}
}
+
+ /**
+ * Adds the 'additional' array element to $message if applicable.
+ *
+ * @param $option_name
+ * @param $option_value
+ *
+ * @return string
+ */
+ private function maybe_render_additional_message( $option_name, $option_value ) {
+ $additional_message_html = '';
+ $hooks = new Hooks( false );
+
+ if ( $option_name === 'proxy_enabled' && $option_value !== '' ) {
+ $additional_message_html = $hooks->render_hook_field( Page::PROXY_WARNING_HOOK );
+ }
+
+ if ( $option_name === 'enable_analytics_dashboard' && $option_value !== '' ) {
+ $additional_message_html = $hooks->render_hook_field( Page::ENABLE_ANALYTICS_DASH_NOTICE );
+ }
+
+ if ( $option_name === 'api_token' && $option_value === '' ) {
+ $additional_message_html = $hooks->render_hook_field( Page::API_TOKEN_MISSING_HOOK );
+ }
+
+ return $additional_message_html;
+ }
+
+ /**
+ * Removes the plausible_analytics_wizard_done row from the wp_options table, effectively displaying the wizard on next page load.
+ *
+ * @return void
+ */
+ public function show_wizard() {
+ $request_data = $this->clean( $_REQUEST );
+
+ if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $request_data['_nonce'], 'plausible_analytics_show_wizard' ) < 1 ) {
+ Messages::set_error( __( 'Not allowed.', 'plausible-analytics' ) );
+
+ wp_send_json_error( null, 403 );
+ }
+
+ delete_option( 'plausible_analytics_wizard_done' );
+
+ $this->maybe_handle_redirect( $request_data['redirect'] );
+
+ wp_send_json_success();
+ }
+
+ /**
+ * Save Admin Settings
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public function toggle_option() {
+ // Sanitize all the post-data before using.
+ $post_data = $this->clean( $_POST );
+ $settings = Helpers::get_settings();
+
+ if ( ! current_user_can( 'manage_options' ) || wp_verify_nonce( $post_data['_nonce'], 'plausible_analytics_toggle_option' ) < 1 ) {
+ wp_send_json_error( __( 'Not allowed.', 'plausible-analytics' ), 403 );
+ }
+
+ if ( $post_data['is_list'] ) {
+ /**
+ * Toggle lists.
+ */
+ if ( $post_data['toggle_status'] === 'on' ) {
+ // If the 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 the 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 ] );
+ }
+ }
+ } else {
+ /**
+ * Single toggles.
+ */
+ $settings[ $post_data['option_name'] ] = $post_data['toggle_status'];
+ }
+
+ // Update all the options to plausible settings.
+ update_option( 'plausible_analytics_settings', $settings );
+
+ /**
+ * Allow devs to perform additional actions.
+ */
+ do_action( 'plausible_analytics_settings_saved', $settings );
+
+ $option_label = $post_data['option_label'];
+ $toggle_status = $post_data['toggle_status'] === 'on' ? __( 'enabled', 'plausible-analytics' ) : __( 'disabled', 'plausible-analytics' );
+ $message = apply_filters(
+ 'plausible_analytics_toggle_option_success_message',
+ sprintf( '%s %s.', $option_label, $toggle_status ),
+ $post_data['option_name'],
+ $post_data['toggle_status']
+ );
+
+ Messages::set_success( $message );
+
+ $additional = $this->maybe_render_additional_message( $post_data['option_name'], $post_data['toggle_status'] );
+
+ Messages::set_additional( $additional, $post_data['option_name'] );
+
+ wp_send_json_success( null, 200 );
+ }
}
diff --git a/src/Assets.php b/src/Assets.php
index 81e5a17f..a55b806e 100644
--- a/src/Assets.php
+++ b/src/Assets.php
@@ -99,7 +99,7 @@ public function maybe_enqueue_main_script() {
}
/**
- * This is a dummy script that will allow us to attach inline scripts further down the line.
+ * Enqueue the JS container for this domain.
*/
wp_register_script( 'plausible-analytics', $url, [], null, apply_filters( 'plausible_load_js_in_footer', false ) );
diff --git a/src/Client.php b/src/Client.php
index f639d076..fef556e8 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -31,123 +31,111 @@ class Client {
private $api_instance;
/**
- * Setup basic authorization, basic_auth.
- *
- * @param string $token Allows specifying the token, e.g., when it's not stored in the DB yet.
- */
- public function __construct( $token = '' ) {
- $config = Configuration::getDefaultConfiguration()
- ->setUsername( 'WordPress' )
- ->setPassword( $token )
- ->setHost( Helpers::get_hosted_domain_url() );
- $timeout = (float) apply_filters( 'plausible_analytics_api_timeout', 10.0 );
- $connect_timeout = (float) apply_filters( 'plausible_analytics_api_connect_timeout', 5.0 );
- $this->api_instance = new DefaultApi( new GuzzleClient( [ 'timeout' => $timeout, 'connect_timeout' => $connect_timeout ] ), $config );
- }
-
- /**
- * Validates the Plugin Token (password) set in the current instance and caches the state to a transient valid for 1 day.
- *
- * @return bool
- * @throws ApiException
+ * @var string $domain_key
*/
- public function validate_api_token() {
- if ( $this->is_api_token_valid() ) {
- return true; // @codeCoverageIgnore
- }
-
- $features = $this->get_features();
-
- if ( ! $features instanceof CapabilitiesFeatures ) {
- return false; // @codeCoverageIgnore
- }
-
- $data_domain = $this->get_data_domain();
- $token = $this->api_instance->getConfig()->getPassword();
- $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;
- }
+ private $domain_key;
/**
- * Is currently stored token valid?
+ * Set up basic authorization, basic_auth.
*
- * @return bool
+ * @param string $token Allows specifying the token, e.g., when it's not stored in the DB yet.
+ * @param string $domain_key The domain key to use for this client.
*/
- public function is_api_token_valid() {
- $token = $this->api_instance->getConfig()->getPassword();
- $valid_tokens = get_transient( 'plausible_analytics_valid_token' );
-
- return isset( $valid_tokens[ $token ] ) && $valid_tokens[ $token ] === true;
+ public function __construct( $token = '', $domain_key = 'default' ) {
+ $this->domain_key = $domain_key;
+ $timeout = (float) apply_filters( 'plausible_analytics_api_timeout', 10.0 );
+ $connect_timeout = (float) apply_filters( 'plausible_analytics_api_connect_timeout', 5.0 );
+ $config = new Configuration();
+ $config->setUsername( 'WordPress' )
+ ->setPassword( $token )
+ ->setHost( Helpers::get_hosted_domain_url() );
+ $this->api_instance = new DefaultApi( new GuzzleClient( [ 'timeout' => $timeout, 'connect_timeout' => $connect_timeout ] ), $config );
}
/**
- * Retrieve Features from Capabilities object.
+ * Allows creating Funnels in bulk.
*
- * @return false|Client\Model\CapabilitiesFeatures
- */
- public function get_features() {
- $capabilities = $this->get_capabilities();
-
- if ( $capabilities instanceof Capabilities ) {
- return $capabilities->getFeatures();
- }
-
- return false; // @codeCoverageIgnore
- }
-
- /**
- * Retrieve all capabilities assigned to configured Plugin Token.
+ * @param FunnelCreateRequest $funnel
*
- * @return bool|Client\Model\Capabilities
+ * @return Client\Model\Funnel|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void
*
* @codeCoverageIgnore
*/
- private function get_capabilities() {
+ public function create_funnel( $funnel ) {
try {
- return $this->api_instance->plausibleWebPluginsAPIControllersCapabilitiesIndex();
- } catch ( \Exception $e ) {
- return false;
+ return $this->api_instance->funnelGetOrCreate( $funnel );
+ } catch ( Exception $e ) {
+ // translators: %s: Error message.
+ $this->send_json_error( $e, __( 'Something went wrong while creating Funnel: %s', 'plausible-analytics' ) );
}
}
/**
- * Retrieve Data Domain property from Capabilities object.
+ * @param Exception $e
+ * @param string $error_message The human-readable part of the error message, requires a %s at the end!
*
- * @return false|string
+ * @return void
*
* @codeCoverageIgnore
*/
- private function get_data_domain() {
- $capabilities = $this->get_capabilities();
+ private function send_json_error( $e, $error_message ) {
+ if ( ! wp_doing_ajax() ) {
+ return;
+ }
- if ( $capabilities instanceof Capabilities ) {
- return $capabilities->getDataDomain();
+ $code = $e->getCode();
+
+ // Any error codes outside the 4xx range should show a generic error.
+ if ( $code <= 399 || $code >= 500 ) {
+ Messages::set_error( __( 'Something went wrong, try again later.', 'plausible-analytics' ) );
+
+ wp_send_json_error( null, $code );
}
- return false;
+ $message = $e->getMessage();
+ $response_body = $e->getResponseBody();
+
+ if ( $response_body !== null ) {
+ $response_json = json_decode( $response_body );
+
+ if ( ! empty( $response_json->errors ) ) {
+ $message = '';
+
+ foreach ( $response_json->errors as $error_no => $error ) {
+ $message .= $error->detail;
+
+ if ( $error_no + 1 === count( $response_json->errors ) ) {
+ $message .= '.';
+ } elseif ( count( $response_json->errors ) > 1 ) {
+ $message .= ', ';
+ }
+ }
+ }
+ }
+
+ Messages::set_error( sprintf( $error_message, $message ) );
+
+ $caps = $this->update_capabilities( '', $this->domain_key );
+
+ wp_send_json_error( [ 'capabilities' => $caps ], $code );
}
/**
* Stores the capabilities for the currently entered API token in the DB for later use.
*
- * @param $token
+ * @param string $token
+ * @param string $domain_key
*
* @return false|array
*
* @codeCoverageIgnore
*/
- private function update_capabilities( $token = '' ) {
- $client_factory = new ClientFactory( $token );
+ private function update_capabilities( $token = '', $domain_key = '' ) {
+ if ( empty( $domain_key ) ) {
+ $domain_key = $this->domain_key;
+ }
+
+ $client_factory = new ClientFactory( $token, $domain_key );
/** @var Client $client */
$client = $client_factory->build();
@@ -170,125 +158,68 @@ private function update_capabilities( $token = '' ) {
WPCapabilities::STATS => $features->getStatsApi(),
];
- update_option( 'plausible_analytics_api_token_caps', $caps );
+ $all_caps = get_option( 'plausible_analytics_api_token_caps', [] );
+
+ /**
+ * @since v2.6.0 Normalize @var $all_caps if the plugin has been configured prior to this version.
+ */
+ if ( ! empty( $all_caps ) && ! is_array( reset( $all_caps ) ) ) {
+ $all_caps = [ 'default' => $all_caps ];
+ }
+
+ $all_caps[ $domain_key ] = $caps;
+
+ update_option( 'plausible_analytics_api_token_caps', $all_caps );
return $caps;
}
/**
- * Retrieve the configured Tracker ID and stores it in the options table.
- *
- * @return string
+ * Retrieve Features from the Capabilities object.
*
- * @codeCoverageIgnore Because we don't want to test WordPress core functionality.
+ * @return false|Client\Model\CapabilitiesFeatures
*/
- public function get_tracker_id() {
- $id = get_option( 'plausible_analytics_tracker_id' );
-
- if ( ! $id ) {
- $tracker_configuration = $this->get_configuration();
-
- if ( ! $tracker_configuration instanceof Client\Model\TrackerScriptConfigurationTrackerScriptConfiguration ) {
- return '';
- }
-
- $id = $tracker_configuration->getId();
+ public function get_features() {
+ $capabilities = $this->get_capabilities();
- update_option( 'plausible_analytics_tracker_id', $id );
+ if ( $capabilities instanceof Capabilities ) {
+ return $capabilities->getFeatures();
}
- return $id;
+ return false; // @codeCoverageIgnore
}
/**
- * Retrieve the configured Tracker Script Configuration.
+ * Retrieve all capabilities assigned to configured Plugin Token.
*
- * @return false|Client\Model\TrackerScriptConfigurationTrackerScriptConfiguration
+ * @return bool|Client\Model\Capabilities
*
- * @codeCoverageIgnore Because we don't want to test the API's response.
+ * @codeCoverageIgnore
*/
- private function get_configuration() {
+ private function get_capabilities() {
try {
- $configuration = $this->api_instance->plausibleWebPluginsAPIControllersTrackerScriptConfigurationGet();
-
- return $configuration->getTrackerScriptConfiguration();
+ return $this->api_instance->plausibleWebPluginsAPIControllersCapabilitiesIndex();
} catch ( \Exception $e ) {
return false;
}
}
/**
- * Update the configured Tracker Script Configuration.
+ * Allows creating Custom Event Goals in bulk.
*
- * @param \Plausible\Analytics\WP\Client\Model\TrackerScriptConfigurationUpdateRequest $tracker_script_config_update_request
+ * @param GoalCreateRequestBulkGetOrCreate $goals
+ *
+ * @return GoalListResponse|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void
*
* @codeCoverageIgnore
*/
- public function update_tracker_script_configuration( $tracker_script_config_update_request ) {
+ public function create_goals( $goals ) {
try {
- $this->api_instance->plausibleWebPluginsAPIControllersTrackerScriptConfigurationUpdate(
- $tracker_script_config_update_request
- );
+ return $this->api_instance->goalGetOrCreate( $goals );
} catch ( Exception $e ) {
- $this->send_json_error(
- $e,
- // translators: %s: Error message.
- __(
- 'Something went wrong while updating tracker script configuration: %s',
- 'plausible-analytics'
- )
- );
- }
- }
-
- /**
- * @param Exception $e
- * @param string $error_message The human-readable part of the error message, requires a %s at the end!
- *
- * @return void
- *
- * @codeCoverageIgnore
- */
- private function send_json_error( $e, $error_message ) {
- if ( ! wp_doing_ajax() ) {
- return;
- }
-
- $code = $e->getCode();
-
- // Any error codes outside the 4xx range should show a generic error.
- if ( $code <= 399 || $code >= 500 ) {
- Messages::set_error( __( 'Something went wrong, try again later.', 'plausible-analytics' ) );
-
- wp_send_json_error( null, $code );
- }
-
- $message = $e->getMessage();
- $response_body = $e->getResponseBody();
-
- if ( $response_body !== null ) {
- $response_json = json_decode( $response_body );
-
- if ( ! empty( $response_json->errors ) ) {
- $message = '';
-
- foreach ( $response_json->errors as $error_no => $error ) {
- $message .= $error->detail;
-
- if ( $error_no + 1 === count( $response_json->errors ) ) {
- $message .= '.';
- } elseif ( count( $response_json->errors ) > 1 ) {
- $message .= ', ';
- }
- }
- }
+ // translators: %s: Error message.
+ $this->send_json_error( $e, __( 'Something went wrong while creating Custom Event Goal: %s', 'plausible-analytics' ) );
}
-
- Messages::set_error( sprintf( $error_message, $message ) );
-
- $caps = $this->update_capabilities();
-
- wp_send_json_error( [ 'capabilities' => $caps ], $code );
}
/**
@@ -296,7 +227,7 @@ private function send_json_error( $e, $error_message ) {
*
* @return void
*/
- public function create_shared_link() {
+ public function create_shared_link( $key = 'default' ) {
$shared_link = (object) [];
$result = (object) [];
@@ -314,7 +245,7 @@ public function create_shared_link() {
}
if ( ! empty( $shared_link->getHref() ) ) {
- Helpers::update_setting( 'shared_link', $shared_link->getHref() );
+ Helpers::update_setting( 'shared_link', $shared_link->getHref(), $key );
}
}
@@ -331,57 +262,116 @@ public function bulk_create_shared_links() {
}
/**
- * Allows creating Custom Event Goals in bulk.
- *
- * @param GoalCreateRequestBulkGetOrCreate $goals
+ * Delete a Custom Event Goal by ID.
*
- * @return GoalListResponse|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void
+ * @param int $id
*
* @codeCoverageIgnore
*/
- public function create_goals( $goals ) {
+ public function delete_goal( $id ) {
try {
- return $this->api_instance->goalGetOrCreate( $goals );
+ $this->api_instance->plausibleWebPluginsAPIControllersGoalsDelete( $id );
} catch ( Exception $e ) {
- // translators: %s: Error message.
- $this->send_json_error( $e, __( 'Something went wrong while creating Custom Event Goal: %s', 'plausible-analytics' ) );
+ $this->send_json_error(
+ $e,
+ // translators: %s: Error message.
+ __(
+ 'Something went wrong while deleting a Custom Event Goal: %s',
+ 'plausible-analytics'
+ )
+ );
}
}
/**
- * Allows creating Funnels in bulk.
+ * Enable (or get) a custom property.
*
- * @param FunnelCreateRequest $funnel
+ * @param CustomPropEnableRequestBulkEnable $enable_request
*
- * @return Client\Model\Funnel|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void
+ * @throws PaymentRequiredError|UnauthorizedError|UnprocessableEntityError
*
* @codeCoverageIgnore
*/
- public function create_funnel( $funnel ) {
+ public function enable_custom_property( $enable_request ) {
try {
- return $this->api_instance->funnelGetOrCreate( $funnel );
+ $this->api_instance->customPropGetOrEnable( $enable_request );
} catch ( Exception $e ) {
- // translators: %s: Error message.
- $this->send_json_error( $e, __( 'Something went wrong while creating Funnel: %s', 'plausible-analytics' ) );
+ $this->send_json_error(
+ $e,
+ // translators: %s: Error message.
+ __(
+ 'Something went wrong while enabling Pageview Properties: %s',
+ 'plausible-analytics'
+ )
+ );
}
}
/**
- * Delete a Custom Event Goal by ID.
+ * Retrieve the configured Tracker ID and stores it in WP's options table.
*
- * @param int $id
+ * @return string
+ *
+ * @codeCoverageIgnore Because we don't want to test WordPress core functionality.
+ */
+ public function get_tracker_id( $key = 'default' ) {
+ $ids = get_option( 'plausible_analytics_tracker_id', [] );
+
+ if ( ! is_array( $ids ) ) {
+ /** @since v2.6.0 normalization for earlier versions. */
+ $ids = [ 'default' => $ids ];
+ }
+
+ if ( empty( $ids[ $key ] ) ) {
+ $tracker_configuration = $this->get_configuration();
+
+ if ( ! $tracker_configuration instanceof Client\Model\TrackerScriptConfigurationTrackerScriptConfiguration ) {
+ return '';
+ }
+
+ $ids[ $key ] = $tracker_configuration->getId();
+
+ update_option( 'plausible_analytics_tracker_id', $ids );
+ }
+
+ return $ids[ $key ];
+ }
+
+ /**
+ * Retrieve the configured Tracker Script Configuration.
+ *
+ * @return false|Client\Model\TrackerScriptConfigurationTrackerScriptConfiguration
+ *
+ * @codeCoverageIgnore Because we don't want to test the API's response.
+ */
+ private function get_configuration() {
+ try {
+ $configuration = $this->api_instance->plausibleWebPluginsAPIControllersTrackerScriptConfigurationGet();
+
+ return $configuration->getTrackerScriptConfiguration();
+ } catch ( \Exception $e ) {
+ return false;
+ }
+ }
+
+ /**
+ * Update the configured Tracker Script Configuration.
+ *
+ * @param \Plausible\Analytics\WP\Client\Model\TrackerScriptConfigurationUpdateRequest $tracker_script_config_update_request
*
* @codeCoverageIgnore
*/
- public function delete_goal( $id ) {
+ public function update_tracker_script_configuration( $tracker_script_config_update_request ) {
try {
- $this->api_instance->plausibleWebPluginsAPIControllersGoalsDelete( $id );
+ $this->api_instance->plausibleWebPluginsAPIControllersTrackerScriptConfigurationUpdate(
+ $tracker_script_config_update_request
+ );
} catch ( Exception $e ) {
$this->send_json_error(
$e,
// translators: %s: Error message.
__(
- 'Something went wrong while deleting a Custom Event Goal: %s',
+ 'Something went wrong while updating tracker script configuration: %s',
'plausible-analytics'
)
);
@@ -389,26 +379,75 @@ public function delete_goal( $id ) {
}
/**
- * Enable (or get) a custom property.
+ * Validates the Plugin Token (password) set in the current instance and caches the state to a transient valid for 1 day.
*
- * @param CustomPropEnableRequestBulkEnable $enable_request
+ * @return bool
+ */
+ public function validate_api_token() {
+ if ( $this->is_api_token_valid() ) {
+ return true; // @codeCoverageIgnore
+ }
+
+ $features = $this->get_features();
+
+ if ( ! $features instanceof CapabilitiesFeatures ) {
+ return false; // @codeCoverageIgnore
+ }
+
+ $data_domain = $this->get_data_domain();
+ $token = $this->api_instance->getConfig()->getPassword();
+ $is_valid = str_contains( $token, 'plausible-plugin' ) && ! empty( $features->getGoals() ) && $data_domain === Helpers::get_domain();
+
+ /**
+ * Don't cache invalid API tokens.
+ */
+ if ( $is_valid ) {
+ $valid_tokens = get_transient( 'plausible_analytics_valid_token' );
+
+ if ( ! is_array( $valid_tokens ) ) {
+ $valid_tokens = [];
+ }
+
+ $valid_tokens[ $token ] = true;
+
+ set_transient( 'plausible_analytics_valid_token', $valid_tokens, 86400 ); // @codeCoverageIgnore
+
+ $this->update_capabilities( $token ); // @codeCoverageIgnore
+ }
+
+ return $is_valid;
+ }
+
+ /**
+ * Is the currently stored token valid?
*
- * @throws PaymentRequiredError|UnauthorizedError|UnprocessableEntityError
+ * @return bool
+ */
+ public function is_api_token_valid() {
+ $token = $this->api_instance->getConfig()->getPassword();
+ $valid_tokens = get_transient( 'plausible_analytics_valid_token' );
+
+ if ( ! is_array( $valid_tokens ) ) {
+ return false;
+ }
+
+ return isset( $valid_tokens[ $token ] ) && $valid_tokens[ $token ] === true;
+ }
+
+ /**
+ * Retrieve Data Domain property from Capabilities object.
+ *
+ * @return false|string
*
* @codeCoverageIgnore
*/
- public function enable_custom_property( $enable_request ) {
- try {
- $this->api_instance->customPropGetOrEnable( $enable_request );
- } catch ( Exception $e ) {
- $this->send_json_error(
- $e,
- // translators: %s: Error message.
- __(
- 'Something went wrong while enabling Pageview Properties: %s',
- 'plausible-analytics'
- )
- );
+ public function get_data_domain() {
+ $capabilities = $this->get_capabilities();
+
+ if ( $capabilities instanceof Capabilities ) {
+ return $capabilities->getDataDomain();
}
+
+ return false;
}
}
diff --git a/src/ClientFactory.php b/src/ClientFactory.php
index 6d17509d..aa0d0f99 100644
--- a/src/ClientFactory.php
+++ b/src/ClientFactory.php
@@ -10,13 +10,36 @@ class ClientFactory {
*/
private $token;
+ /**
+ * @var string $domain_key
+ */
+ private $domain_key;
+
/**
* Setup basic authorization.
*
- * @param string $token Allows to specify the token, e.g. when it's not stored in the DB yet.
+ * @param string $token Allows specifying the token, e.g., when it's not stored in the DB yet.
+ * @param string $domain_key The domain key to use for the client.
*/
- public function __construct( $token = '' ) {
- $this->token = $token;
+ public function __construct( $token = '', $domain_key = '' ) {
+ $this->token = $token;
+ $this->domain_key = $domain_key ?: Helpers::get_current_language_domain_key();
+ }
+
+ /**
+ * Show an error on the settings screen if cURL isn't enabled on this machine.
+ *
+ * @return void
+ *
+ * @codeCoverageIgnore
+ */
+ public function add_curl_error() {
+ Messages::set_error(
+ __(
+ 'cURL is not enabled on this server, which means API provisioning will not work. Please contact your hosting provider to enable the cURL module or allow_url_fopen.',
+ 'plausible-analytics'
+ )
+ );
}
/**
@@ -35,29 +58,13 @@ public function build() {
}
if ( ! $this->token ) {
- $this->token = Helpers::get_settings()['api_token'];
+ $this->token = Helpers::get_api_token();
}
if ( ! $this->token ) {
return false;
}
- return new Client( $this->token );
- }
-
- /**
- * Show an error on the settings screen if cURL isn't enabled on this machine.
- *
- * @return void
- *
- * @codeCoverageIgnore
- */
- public function add_curl_error() {
- Messages::set_error(
- __(
- 'cURL is not enabled on this server, which means API provisioning will not work. Please contact your hosting provider to enable the cURL module or allow_url_fopen.',
- 'plausible-analytics'
- )
- );
+ return new Client( $this->token, $this->domain_key );
}
}
diff --git a/src/Cron.php b/src/Cron.php
index f1742c44..8c79c405 100644
--- a/src/Cron.php
+++ b/src/Cron.php
@@ -54,14 +54,35 @@ private function maybe_download() {
return false;
}
- $remote = Helpers::get_js_url();
- $local = Helpers::get_js_path();
+ $settings = Helpers::get_settings();
+ $tokens = $settings['api_token'];
+ $success = true;
- if ( ! $remote || ! $local ) {
- return false;
+ foreach ( $tokens as $key => $token ) {
+ if ( empty( $token ) ) {
+ continue;
+ }
+
+ // Force the language domain key for this iteration so get_filename() / get_api_token() resolve correctly.
+ $force_key = function () use ( $key ) {
+ return $key;
+ };
+
+ add_filter( 'plausible_analytics_current_language_domain_key', $force_key );
+
+ $remote = Helpers::get_js_url();
+ $local = Helpers::get_js_path();
+
+ if ( ! $remote || ! $local ) {
+ $success = false;
+ } elseif ( ! $this->download_file( $remote, $local ) ) {
+ $success = false;
+ }
+
+ remove_filter( 'plausible_analytics_current_language_domain_key', $force_key );
}
- return $this->download_file( $remote, $local );
+ return $success;
}
/**
diff --git a/src/EnhancedMeasurements.php b/src/EnhancedMeasurements.php
index 68a2aa66..bec4deb9 100644
--- a/src/EnhancedMeasurements.php
+++ b/src/EnhancedMeasurements.php
@@ -52,8 +52,8 @@ final class EnhancedMeasurements {
*
* @TODO: Refactor $name to enum (introduced in PHP 8.1) when WordPress drops support for PHP 8.0 and lower.
*
- * @param string $name Name of the option to check, valid values are defined in @var self::AVAILABLE_OPTIONS
- * @param array $enhanced_measurements Allows checking against a different set of options.
+ * @param string $name Name of the option to check, valid values are defined in @var self::AVAILABLE_OPTIONS
+ * @param array $enhanced_measurements Allows checking against a different set of options.
*
* @return bool
*/
@@ -61,7 +61,9 @@ public static function is_enabled( $name, $enhanced_measurements = [] ) {
self::is_valid( $name );
if ( empty( $enhanced_measurements ) ) {
- $enhanced_measurements = Helpers::get_settings()['enhanced_measurements'];
+ $settings = Helpers::get_settings();
+
+ $enhanced_measurements = $settings['enhanced_measurements'] ?? [];
}
if ( ! is_array( $enhanced_measurements ) ) {
diff --git a/src/Helpers.php b/src/Helpers.php
index 257b4d8e..6d998ad5 100644
--- a/src/Helpers.php
+++ b/src/Helpers.php
@@ -16,22 +16,15 @@
*/
class Helpers {
/**
- * Get entered Domain Name or provide an alternative if not entered.
+ * Returns the API token.
*
- * @since 1.0.0
- * @access public
* @return string
*/
- public static function get_domain() {
+ public static function get_api_token() {
$settings = static::get_settings();
+ $current = static::get_current_language_domain_key();
- if ( ! empty( $settings['domain_name'] ) ) {
- return $settings['domain_name'];
- }
-
- $url = home_url();
-
- return preg_replace( '/^http(s?):\/\/(www\.)?/i', '', $url );
+ return $settings['api_token'][ $current ] ?? $settings['api_token']['default'] ?? '';
}
/**
@@ -43,8 +36,8 @@ public static function get_domain() {
*/
public static function get_settings() {
$defaults = [
- 'domain_name' => '',
- 'api_token' => '',
+ 'domain_name' => [ 'default' => '' ],
+ 'api_token' => [ 'default' => '' ],
'enhanced_measurements' => [
EnhancedMeasurements::FOUR_O_FOUR,
EnhancedMeasurements::FILE_DOWNLOADS,
@@ -56,7 +49,7 @@ public static function get_settings() {
'query_params' => [],
'proxy_enabled' => '',
'enable_analytics_dashboard' => '',
- 'shared_link' => '',
+ 'shared_link' => [ 'default' => '' ],
'excluded_pages' => '',
'tracked_user_roles' => [],
'expand_dashboard_access' => [],
@@ -65,9 +58,108 @@ public static function get_settings() {
'self_hosted_shared_link' => '',
];
- $settings = get_option( 'plausible_analytics_settings', [] );
+ $settings = function_exists( 'get_option' ) ? get_option( 'plausible_analytics_settings', [] ) : [];
+ $settings = wp_parse_args( $settings, $defaults );
+
+ /**
+ * Normalization: Ensure domain_name and api_token are always arrays.
+ */
+ if ( ! is_array( $settings['domain_name'] ) ) {
+ $settings['domain_name'] = [ 'default' => $settings['domain_name'] ];
+ }
+
+ if ( ! is_array( $settings['api_token'] ) ) {
+ $settings['api_token'] = [ 'default' => $settings['api_token'] ];
+ }
+
+ if ( ! is_array( $settings['shared_link'] ) ) {
+ $settings['shared_link'] = [ 'default' => $settings['shared_link'] ];
+ }
+
+ return apply_filters( 'plausible_analytics_settings', $settings );
+ }
+
+ /**
+ * Returns the key of the currently used Language Domain.
+ *
+ * @since v2.6.0
+ *
+ * @return string
+ *
+ * @codeCoverageIgnore Because it depends on 3rd party plugins.
+ */
+ public static function get_current_language_domain_key() {
+ if ( ! static::is_language_per_domain_mode() ) {
+ return 'default';
+ }
+
+ $language_domains = apply_filters( 'wpml_setting', [], 'language_domains' );
+ $current_language = apply_filters( 'wpml_current_language', null );
- return apply_filters( 'plausible_analytics_settings', wp_parse_args( $settings, $defaults ) );
+ if ( $current_language && isset( $language_domains[ $current_language ] ) ) {
+ return (string) apply_filters( 'plausible_analytics_current_language_domain_key', $current_language );
+ }
+
+ return (string) apply_filters( 'plausible_analytics_current_language_domain_key', 'default' );
+ }
+
+ /**
+ * Returns true only when WPML is active, its negotiation type is "different domain per language", AND at least one domain is configured.
+ *
+ * @since v2.6.0
+ *
+ * @return bool
+ *
+ * @codeCoverageIgnore Because it depends on 3rd party plugins.
+ */
+ public static function is_language_per_domain_mode() {
+ static $is_language_per_domain;
+
+ if ( defined( 'PLAUSIBLE_CI' ) && PLAUSIBLE_CI ) {
+ $is_language_per_domain = null;
+ }
+
+ if ( $is_language_per_domain !== null ) {
+ return $is_language_per_domain; // @codeCoverageIgnore
+ }
+
+ /**
+ * If WPML is not active, we can assume we're not in language per domain mode.
+ */
+ if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
+ return $is_language_per_domain = (bool) apply_filters( 'plausible_analytics_language_per_domain_mode', false );
+ }
+
+ $negotiation_type = (int) apply_filters( 'wpml_setting', 0, 'language_negotiation_type' );
+ $domains = apply_filters( 'wpml_setting', [], 'language_domains' );
+ $value = $negotiation_type === 2 && ! empty( $domains );
+
+ return $is_language_per_domain = (bool) apply_filters( 'plausible_analytics_language_per_domain_mode', $value );
+ }
+
+ /**
+ * Returns the name of the current Plausible domain.
+ *
+ * @since v2.6.0 This is now mapped to language domains to provide compatibility with multilang plugins, like WPML.
+ *
+ * @return string
+ */
+ public static function get_domain() {
+ $settings = static::get_settings();
+ $current_key = static::get_current_language_domain_key();
+ $domain_name = $settings['domain_name'][ $current_key ] ?? '';
+
+ if ( ! empty( $domain_name ) ) {
+ return $domain_name;
+ }
+
+ if ( ! empty( $settings['domain_name']['default'] ) ) {
+ return $settings['domain_name']['default'];
+ }
+
+ $url = home_url();
+
+ return preg_replace( '/^http(s?):\/\/(www\.)?/i', '', $url );
}
/**
@@ -235,7 +327,7 @@ public static function get_filename() {
$client = static::get_client();
if ( $client instanceof Client ) {
- return $client->get_tracker_id();
+ return $client->get_tracker_id( static::get_current_language_domain_key() );
}
return '';
@@ -244,12 +336,14 @@ public static function get_filename() {
/**
* Build the API client.
*
+ * @param string $domain_key
+ *
* @return false|Client
*
* @codeCoverageIgnore This seam's only function is to keep our code testable.
*/
- protected static function get_client() {
- $client = new ClientFactory();
+ protected static function get_client( $domain_key = '' ) {
+ $client = new ClientFactory( '', $domain_key );
return $client->build();
}
@@ -280,7 +374,43 @@ public static function get_js_url( $local = false ) {
}
/**
- * Get user role for the logged-in user.
+ * @since v2.6.0 Provide compatibility with multilang plugins, like WPML.
+ *
+ * @return array
+ *
+ * @codeCoverageIgnore Because it depends on 3rd party plugins.
+ */
+ public static function get_language_domains() {
+ $domains = apply_filters( 'wpml_setting', [], 'language_domains' );
+ $main = wp_parse_url( home_url(), PHP_URL_HOST ) ?: home_url();
+
+ // WPML's language_domains omits the default language; prepend the main WP domain.
+ if ( ! in_array( $main, $domains, true ) ) {
+ $domains = array_merge( [ 'default' => $main ], $domains );
+ }
+
+ return apply_filters( 'plausible_analytics_language_domains', $domains );
+ }
+
+ /**
+ * Get the name of the active multilang plugin.
+ *
+ * @return string
+ *
+ * @codeCoverageIgnore Because it depends on 3rd party plugins.
+ */
+ public static function get_multilang_plugin_name() {
+ $name = '';
+
+ if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
+ $name = 'WPML';
+ }
+
+ return apply_filters( 'plausible_analytics_multilang_plugin_name', $name );
+ }
+
+ /**
+ * Get the user role for the logged-in user.
*
* @since 1.3.0
* @access public
@@ -309,9 +439,14 @@ public static function main_script_is_registered() {
*
* @return void
*/
- public static function update_setting( $option_name, $option_value ) {
- $settings = static::get_settings();
- $settings[ $option_name ] = $option_value;
+ public static function update_setting( $option_name, $option_value, $key = '' ) {
+ $settings = static::get_settings();
+
+ if ( ! empty( $key ) && is_array( $settings[ $option_name ] ) ) {
+ $settings[ $option_name ][ $key ] = $option_value;
+ } else {
+ $settings[ $option_name ] = $option_value;
+ }
update_option( 'plausible_analytics_settings', $settings );
}
diff --git a/tests/TestableHelpers.php b/tests/TestableHelpers.php
index f2287e89..3eabd1ac 100644
--- a/tests/TestableHelpers.php
+++ b/tests/TestableHelpers.php
@@ -15,9 +15,9 @@ class TestableHelpers extends Helpers {
/**
* @return
*/
- protected static function get_client() {
+ protected static function get_client( $domain_key = '' ) {
return new class extends Client {
- public function get_tracker_id() {
+ public function get_tracker_id( $key = 'default' ) {
return 'pa-test-tracker-id';
}
};
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 40f6122a..43e26517 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -34,5 +34,17 @@ function _manually_load_plugin() {
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
+/**
+ * The function the_block_template_skip_link() is deprecated since WP 6.4.0.
+ * It is still hooked to wp_footer in wp-includes/default-filters.php, which causes a deprecation warning.
+ * We unhook it here to prevent the warning.
+ */
+tests_add_filter(
+ 'init',
+ function () {
+ remove_action( 'wp_footer', 'the_block_template_skip_link' );
+ }
+);
+
// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";
diff --git a/tests/integration/Admin/ProvisioningTest.php b/tests/integration/Admin/ProvisioningTest.php
index 13971cc9..f0342db2 100644
--- a/tests/integration/Admin/ProvisioningTest.php
+++ b/tests/integration/Admin/ProvisioningTest.php
@@ -17,38 +17,28 @@
class ProvisioningTest extends TestCase {
/**
- * @throws ApiException
- * @see Provisioning::maybe_create_shared_link()
+ * @see Provisioning::create_goal_request()
+ * @return void
*/
- public function testCreateSharedLink() {
- $settings = [];
- $settings['enable_analytics_dashboard'] = 1;
- $mock = $this->getMockBuilder( Client::class )->onlyMethods( [ 'bulk_create_shared_links' ] )->getMock();
- $sharedLinkObject = new Client\Model\SharedLinkSharedLink(
- [
- 'id' => 'test',
- 'name' => 'Test',
- 'href' => 'http://example.org/test',
- 'password_protected' => false,
- ]
- );
- $sharedLink = new Client\Model\SharedLink();
+ public function testCreateGoalRequest() {
+ $class = new Provisioning( false );
- $sharedLink->setSharedLink( $sharedLinkObject );
- $mock->method( 'bulk_create_shared_links' )->willReturn( $sharedLink );
+ $pageview = $class->create_goal_request( 'Test Pageview', 'Pageview', null, '/test' );
- $class = new Provisioning( $mock );
+ $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestPageview', $pageview );
- $class->maybe_create_shared_link( [], $settings );
+ $revenue = $class->create_goal_request( 'Test Revenue', 'Revenue', 'EUR' );
- $sharedLink = Helpers::get_settings()['shared_link'];
+ $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestRevenue', $revenue );
- $this->assertEquals( 'http://example.org/test', $sharedLink );
+ $custom_event = $class->create_goal_request( 'Test Custom Event' );
+
+ $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestCustomEvent', $custom_event );
}
/**
- * @throws ApiException
* @see Provisioning::maybe_create_goals()
+ * @throws ApiException
*/
public function testCreateGoals() {
$settings['enhanced_measurements'] = [
@@ -64,7 +54,7 @@ public function testCreateGoals() {
'goal' => new GoalPageviewAllOfGoal( [
'display_name' => '404',
'id' => 111,
- 'path' => null
+ 'path' => null,
] ),
'goal_type' => 'Goal.CustomEvent',
]
@@ -74,7 +64,7 @@ public function testCreateGoals() {
'goal' => new GoalPageviewAllOfGoal( [
'display_name' => 'Outbound Link: Click',
'id' => 222,
- 'path' => null
+ 'path' => null,
] ),
'goal_type' => 'Goal.CustomEvent',
]
@@ -84,7 +74,7 @@ public function testCreateGoals() {
'goal' => new GoalPageviewAllOfGoal( [
'display_name' => 'File Downloads',
'id' => 333,
- 'path' => null
+ 'path' => null,
] ),
'goal_type' => 'Goal.CustomEvent',
]
@@ -94,7 +84,7 @@ public function testCreateGoals() {
'goal' => new GoalPageviewAllOfGoal( [
'display_name' => 'Search',
'id' => 444,
- 'path' => null
+ 'path' => null,
] ),
'goal_type' => 'Goal.Pageview',
]
@@ -112,38 +102,49 @@ public function testCreateGoals() {
$goal_ids = get_option( 'plausible_analytics_enhanced_measurements_goal_ids' );
- $this->assertCount( 4, $goal_ids );
- $this->assertArrayHasKey( 111, $goal_ids );
- $this->assertArrayHasKey( 222, $goal_ids );
- $this->assertArrayHasKey( 333, $goal_ids );
- $this->assertArrayHasKey( 444, $goal_ids );
+ $this->assertCount( 1, $goal_ids );
+ $this->assertCount( 4, $goal_ids['default'] );
+ $this->assertArrayHasKey( 111, $goal_ids['default'] );
+ $this->assertArrayHasKey( 222, $goal_ids['default'] );
+ $this->assertArrayHasKey( 333, $goal_ids['default'] );
+ $this->assertArrayHasKey( 444, $goal_ids['default'] );
delete_option( 'plausible_analytics_enhanced_measurements_goal_ids' );
}
/**
- * @return void
- * @see Provisioning::create_goal_request()
+ * @see Provisioning::maybe_create_shared_link()
+ * @throws ApiException
*/
- public function testCreateGoalRequest() {
- $class = new Provisioning( false );
-
- $pageview = $class->create_goal_request( 'Test Pageview', 'Pageview', null, '/test' );
+ public function testCreateSharedLink() {
+ $settings = [];
+ $settings['enable_analytics_dashboard'] = 1;
+ $mock = $this->getMockBuilder( Client::class )->onlyMethods( [ 'bulk_create_shared_links' ] )->getMock();
+ $sharedLinkObject = new Client\Model\SharedLinkSharedLink(
+ [
+ 'id' => 'test',
+ 'name' => 'Test',
+ 'href' => 'http://example.org/test',
+ 'password_protected' => false,
+ ]
+ );
+ $sharedLink = new Client\Model\SharedLink();
- $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestPageview', $pageview );
+ $sharedLink->setSharedLink( $sharedLinkObject );
+ $mock->method( 'bulk_create_shared_links' )->willReturn( $sharedLink );
- $revenue = $class->create_goal_request( 'Test Revenue', 'Revenue', 'EUR' );
+ $class = new Provisioning( $mock );
- $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestRevenue', $revenue );
+ $class->maybe_create_shared_link( [], $settings );
- $custom_event = $class->create_goal_request( 'Test Custom Event' );
+ $sharedLink = Helpers::get_settings()['shared_link']['default'];
- $this->assertInstanceOf( 'Plausible\Analytics\WP\Client\Model\GoalCreateRequestCustomEvent', $custom_event );
+ $this->assertEquals( 'http://example.org/test', $sharedLink );
}
/**
- * @return void
* @see Provisioning::maybe_enable_customer_user_roles()
+ * @return void
*/
public function testMaybeEnableCustomerUserRole() {
try {
@@ -183,8 +184,76 @@ public function testMaybeEnableCustomerUserRole() {
}
/**
- * @throws ApiException
+ * Test multi-domain iteration.
+ */
+ public function testMultiDomainIteration() {
+ // Mock settings to have two domains with tokens.
+ $settings = Helpers::get_settings();
+ $settings['api_token'] = [
+ 'default' => 'token-a',
+ 'nl' => 'token-b',
+ ];
+ update_option( 'plausible_analytics_settings', $settings );
+
+ $mock_a = $this->getMockBuilder( Client::class )->disableOriginalConstructor()->onlyMethods( [ 'validate_api_token', 'create_shared_link' ] )->getMock();
+ $mock_a->method( 'validate_api_token' )->willReturn( true );
+ $mock_a->expects( $this->once() )->method( 'create_shared_link' );
+
+ $mock_b = $this->getMockBuilder( Client::class )->disableOriginalConstructor()->onlyMethods( [ 'validate_api_token', 'create_shared_link' ] )->getMock();
+ $mock_b->method( 'validate_api_token' )->willReturn( true );
+ $mock_b->expects( $this->once() )->method( 'create_shared_link' );
+
+ // We need a way to return these mocks. Since we can't easily mock ClientFactory::build() without more effort,
+ // we'll mock get_clients_per_domain directly in a partial mock of Provisioning.
+ $provisioning = $this->getMockBuilder( Provisioning::class )
+ ->onlyMethods( [ 'get_clients' ] )
+ ->getMock();
+
+ $provisioning->method( 'get_clients' )->willReturn( [
+ 'default' => $mock_a,
+ 'nl' => $mock_b,
+ ] );
+
+ $provisioning->maybe_create_shared_link( [], [ 'enable_analytics_dashboard' => 1 ] );
+
+ delete_option( 'plausible_analytics_settings' );
+ }
+
+ /**
+ * Test legacy data normalization.
+ */
+ public function testNormalizePerDomainOption() {
+ $provisioning = new Provisioning();
+
+ // Flat format (legacy)
+ $legacy_goals = [ 123 => 'Goal Name' ];
+ $normalized = $this->callMethod( $provisioning, 'normalize_option', [ $legacy_goals ] );
+ $this->assertEquals( [ 'default' => $legacy_goals ], $normalized );
+
+ $legacy_caps = [ 'goals' => true, 'stats' => false ];
+ $normalized = $this->callMethod( $provisioning, 'normalize_option', [ $legacy_caps ] );
+ $this->assertEquals( [ 'default' => $legacy_caps ], $normalized );
+
+ // New format
+ $new_format = [ 'default' => [ 123 => 'Goal Name' ], 'fr' => [ 456 => 'Goal FR' ] ];
+ $normalized = $this->callMethod( $provisioning, 'normalize_option', [ $new_format ] );
+ $this->assertEquals( $new_format, $normalized );
+ }
+
+ /**
+ * Helper to call private/protected methods.
+ */
+ protected function callMethod( $obj, $name, array $args ) {
+ $class = new \ReflectionClass( $obj );
+ $method = $class->getMethod( $name );
+ $method->setAccessible( true );
+
+ return $method->invokeArgs( $obj, $args );
+ }
+
+ /**
* @see Provisioning::update_tracker_script_config()
+ * @throws ApiException
*/
public function testUpdateTrackerScriptConfig() {
$mock = $this->getMockBuilder( Client::class )->onlyMethods( [ 'update_tracker_script_configuration' ] )->getMock();
diff --git a/tests/integration/AdminBarTest.php b/tests/integration/AdminBarTest.php
index 82b0bb8b..118ea857 100644
--- a/tests/integration/AdminBarTest.php
+++ b/tests/integration/AdminBarTest.php
@@ -10,29 +10,6 @@
use WP_Admin_Bar;
class AdminBarTest extends TestCase {
- /**
- * @see AdminBar::admin_bar_node()
- */
- public function testAdminBarNode() {
- $class = new AdminBar();
-
- if ( ! class_exists( 'WP_Admin_Bar' ) ) {
- require_once( ABSPATH . 'wp-includes/class-wp-admin-bar.php' );
- }
-
- wp_set_current_user( 1 );
- $user = wp_get_current_user();
- $user->add_role( 'administrator' );
- $admin_bar = new WP_Admin_Bar();
- $class->admin_bar_node( $admin_bar );
- $this->assertNotEmpty( $admin_bar->get_node( 'plausible-analytics' ) );
-
- wp_set_current_user( null );
- $admin_bar = new WP_Admin_Bar();
- $class->admin_bar_node( $admin_bar );
- $this->assertEmpty( $admin_bar->get_node( 'plausible-analytics' ) );
- }
-
public function testAddAnalyticsNode() {
try {
add_filter( 'plausible_analytics_settings', [ $this, 'enableAnalyticsDashboard' ] );
@@ -64,12 +41,64 @@ public function testAddAnalyticsNode() {
$args = $class->maybe_add_analytics( [], [ 'enable_analytics_dashboard' => 'on' ] );
$this->assertNotEmpty( $args );
$this->assertCount( 2, $args );
- $this->assertTrue( in_array( 'view-analytics', $args[0] ) );
- $this->assertTrue( in_array( 'view-page-analytics', $args[1] ) );
+ $this->assertEquals( 'view-analytics-default', $args[0]['id'] );
+ $this->assertEquals( 'view-page-analytics', $args[1]['id'] );
} finally {
remove_filter( 'plausible_analytics_settings', [ $this, 'enableAnalyticsDashboard' ] );
wp_set_current_user( null );
unset( $post );
}
}
+
+ /**
+ * @see AdminBar::admin_bar_node()
+ */
+ public function testAdminBarNode() {
+ $class = new AdminBar();
+
+ if ( ! class_exists( 'WP_Admin_Bar' ) ) {
+ require_once( ABSPATH . 'wp-includes/class-wp-admin-bar.php' );
+ }
+
+ wp_set_current_user( 1 );
+ $user = wp_get_current_user();
+ $user->add_role( 'administrator' );
+ $admin_bar = new WP_Admin_Bar();
+ $class->admin_bar_node( $admin_bar );
+ $this->assertNotEmpty( $admin_bar->get_node( 'plausible-analytics' ) );
+
+ wp_set_current_user( null );
+ $admin_bar = new WP_Admin_Bar();
+ $class->admin_bar_node( $admin_bar );
+ $this->assertEmpty( $admin_bar->get_node( 'plausible-analytics' ) );
+ }
+
+ public function testMaybeAddAnalyticsWithMultipleDomains() {
+ $class = new AdminBar();
+
+ $language_domains = [
+ 'default' => 'example.com',
+ 'nl' => 'nl.example.com',
+ ];
+ $callback = function () use ( $language_domains ) {
+ return $language_domains;
+ };
+ add_filter( 'plausible_analytics_language_domains', $callback );
+
+ $settings = [
+ 'enable_analytics_dashboard' => 'on',
+ 'shared_link' => [
+ 'nl' => 'https://plausible.io/share/nl.example.com?auth=token',
+ ],
+ ];
+
+ $args = $class->maybe_add_analytics( [], $settings );
+
+ remove_filter( 'plausible_analytics_language_domains', $callback );
+
+ $this->assertCount( 2, $args );
+ $this->assertEquals( 'view-analytics-default', $args[0]['id'] );
+ $this->assertEquals( 'view-analytics-nl', $args[1]['id'] );
+ $this->assertStringContainsString( 'domain=nl', $args[1]['href'] );
+ }
}
diff --git a/tests/integration/AjaxTest.php b/tests/integration/AjaxTest.php
index a8870c94..e249962c 100644
--- a/tests/integration/AjaxTest.php
+++ b/tests/integration/AjaxTest.php
@@ -42,6 +42,54 @@ public function tearDown(): void {
remove_filter( 'nonce_user_logged_out', '__return_true' );
}
+ /**
+ * Test save_options with keyed/multilang data.
+ */
+ public function testSaveKeyedOptionsSuccess() {
+ $language_domain = 'german.dev.local';
+ $options = [
+ [ 'name' => "domain_name[$language_domain]", 'value' => 'german.example.com' ],
+ [ 'name' => "api_token[$language_domain]", 'value' => 'plausible-plugin-german-token' ],
+ ];
+
+ $_POST['_nonce'] = wp_create_nonce( 'plausible_analytics_toggle_option' );
+ $_POST['options'] = wp_json_encode( $options );
+
+ // Mock the API response to avoid real API calls and handle the validation in Ajax::save_options
+ set_transient( 'plausible_analytics_valid_token', [ 'plausible-plugin-german-token' => true ] );
+
+ try {
+ $this->ajax->save_options();
+ } catch ( \Exception $e ) {
+ }
+
+ $settings = Helpers::get_settings();
+
+ $this->assertArrayHasKey( $language_domain, $settings['domain_name'] );
+ $this->assertEquals( 'german.example.com', $settings['domain_name'][ $language_domain ] );
+ $this->assertArrayHasKey( $language_domain, $settings['api_token'] );
+ $this->assertEquals( 'plausible-plugin-german-token', $settings['api_token'][ $language_domain ] );
+ }
+
+ /**
+ * Test save_options with invalid JSON.
+ */
+ public function testSaveOptionsInvalidJson() {
+ $_POST['_nonce'] = wp_create_nonce( 'plausible_analytics_toggle_option' );
+ $_POST['options'] = 'invalid-json';
+
+ // wp_send_json_error will be called, which we expect.
+ // In a real WP environment it would exit.
+ try {
+ $this->ajax->save_options();
+ } catch ( \Exception $e ) {
+ }
+
+ // Verify that settings were NOT updated to something weird.
+ $settings = Helpers::get_settings();
+ $this->assertNotEquals( 'invalid-json', $settings['domain_name']['default'] );
+ }
+
/**
* Test save_options with normal JSON data.
*/
@@ -62,7 +110,7 @@ public function testSaveOptionsSuccess() {
}
$settings = Helpers::get_settings();
- $this->assertEquals( 'example.com', $settings['domain_name'] );
+ $this->assertEquals( 'example.com', $settings['domain_name']['default'] );
$this->assertEquals( 'on', $settings['proxy_enabled'] );
}
@@ -87,25 +135,6 @@ public function testSaveOptionsWithEscapedJson() {
}
$settings = Helpers::get_settings();
- $this->assertEquals( 'escaped.com', $settings['domain_name'] );
- }
-
- /**
- * Test save_options with invalid JSON.
- */
- public function testSaveOptionsInvalidJson() {
- $_POST['_nonce'] = wp_create_nonce( 'plausible_analytics_toggle_option' );
- $_POST['options'] = 'invalid-json';
-
- // wp_send_json_error will be called, which we expect.
- // In a real WP environment it would exit.
- try {
- $this->ajax->save_options();
- } catch ( \Exception $e ) {
- }
-
- // Verify that settings were NOT updated to something weird.
- $settings = Helpers::get_settings();
- $this->assertNotEquals( 'invalid-json', $settings['domain_name'] );
+ $this->assertEquals( 'escaped.com', $settings['domain_name']['default'] );
}
}
diff --git a/tests/integration/ClientFactoryTest.php b/tests/integration/ClientFactoryTest.php
index 01413679..9c06af09 100644
--- a/tests/integration/ClientFactoryTest.php
+++ b/tests/integration/ClientFactoryTest.php
@@ -14,6 +14,8 @@ class ClientFactoryTest extends TestCase {
* @see ClientFactory::build()
*/
public function testBuild() {
+ delete_option( 'plausible_analytics_settings' );
+
$clientFactory = new ClientFactory();
$client = $clientFactory->build();
diff --git a/tests/integration/ClientTest.php b/tests/integration/ClientTest.php
new file mode 100644
index 00000000..f742aad2
--- /dev/null
+++ b/tests/integration/ClientTest.php
@@ -0,0 +1,82 @@
+assertFalse( $client->is_api_token_valid() );
+
+ // Test with null
+ delete_transient( 'plausible_analytics_valid_token' );
+ $this->assertFalse( $client->is_api_token_valid() );
+ }
+
+ /**
+ * @see Client::validate_api_token()
+ * @see Client::is_api_token_valid()
+ */
+ public function test_validate_api_token_caching() {
+ // Mock Client to avoid real API calls
+ $token1 = 'plausible-plugin-token1';
+ $token2 = 'plausible-plugin-token2';
+ $domain = 'example.com';
+
+ // We need to mock get_features and get_data_domain which are used in validate_api_token
+ // But they are private/protected or use api_instance.
+ // Since we want to test the caching logic in validate_api_token, let's see if we can mock the api_instance.
+
+ // However, it might be easier to just test is_api_token_valid and the transient logic directly
+ // if we can't easily mock the API response here.
+
+ // Let's try to mock Client and only the parts that hit the API.
+ $client1 = $this->getMockBuilder( Client::class )
+ ->setConstructorArgs( [ $token1 ] )
+ ->onlyMethods( [ 'get_features', 'get_data_domain' ] )
+ ->getMock();
+
+ $features = new CapabilitiesFeatures();
+ $features->setGoals( [ 'goal1' ] );
+
+ $client1->method( 'get_features' )->willReturn( $features );
+ $client1->method( 'get_data_domain' )->willReturn( Helpers::get_domain() );
+
+ // Clear transient
+ delete_transient( 'plausible_analytics_valid_token' );
+
+ // Validate first token
+ $this->assertTrue( $client1->validate_api_token() );
+ $this->assertTrue( $client1->is_api_token_valid() );
+
+ $cached = get_transient( 'plausible_analytics_valid_token' );
+ $this->assertArrayHasKey( $token1, $cached );
+
+ // Validate second token
+ $client2 = $this->getMockBuilder( Client::class )
+ ->setConstructorArgs( [ $token2 ] )
+ ->onlyMethods( [ 'get_features', 'get_data_domain' ] )
+ ->getMock();
+ $client2->method( 'get_features' )->willReturn( $features );
+ $client2->method( 'get_data_domain' )->willReturn( Helpers::get_domain() );
+
+ $this->assertTrue( $client2->validate_api_token() );
+ $this->assertTrue( $client2->is_api_token_valid() );
+
+ $cached = get_transient( 'plausible_analytics_valid_token' );
+
+ $this->assertArrayHasKey( $token1, $cached );
+ $this->assertArrayHasKey( $token2, $cached );
+ }
+}
diff --git a/tests/integration/HelpersTest.php b/tests/integration/HelpersTest.php
index 96acdf25..39ebe982 100644
--- a/tests/integration/HelpersTest.php
+++ b/tests/integration/HelpersTest.php
@@ -12,144 +12,211 @@
class HelpersTest extends TestCase {
/**
- * @see Helpers::get_js_url()
+ * Enable excluded pages option.
+ *
+ * @param $settings
+ *
+ * @return mixed
*/
- public function testGetJsUrl() {
- $url = TestableHelpers::get_js_url();
-
- $this->assertEquals( 'https://plausible.io/js/pa-test-tracker-id.js', $url );
-
- try {
- add_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
-
- $url = TestableHelpers::get_js_url( true );
-
- $this->assertMatchesRegularExpression( '~http://example.org/wp-content/uploads/.*?/.*?.js~', $url );
- } finally {
- remove_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
- }
-
- try {
- add_filter( 'plausible_analytics_settings', [ $this, 'enableSelfHostedDomain' ] );
-
- $url = TestableHelpers::get_js_url();
+ public function addExcludedPages( $settings ) {
+ $settings['excluded_pages'] = 'test';
- $this->assertEquals( 'https://self-hosted-test.org/js/pa-test-tracker-id.js', $url );
- } finally {
- remove_filter( 'plausible_analytics_settings', [ $this, 'enableSelfHostedDomain' ] );
- }
+ return $settings;
}
/**
- * Enable Self Hosted domain.
+ * Enable Enhanced Measurements > Outbound Links.
*
* @param $settings
*
* @return mixed
*/
- public function enableSelfHostedDomain( $settings ) {
- $settings['self_hosted_domain'] = 'self-hosted-test.org';
+ public function enableOutboundLinks( $settings ) {
+ $settings['enhanced_measurements'] = [ 'outbound-links' ];
return $settings;
}
/**
- * Enable excluded pages option.
+ * Enable Enhanced Measurements > Search Queries
*
* @param $settings
*
* @return mixed
*/
- public function addExcludedPages( $settings ) {
- $settings['excluded_pages'] = 'test';
+ public function enableSearch( $settings ) {
+ $settings['enhanced_measurements'] = [ 'search' ];
return $settings;
}
/**
- * Enable Enhanced Measurements > Outbound Links.
+ * Enable Self Hosted domain.
*
* @param $settings
*
* @return mixed
*/
- public function enableOutboundLinks( $settings ) {
- $settings['enhanced_measurements'] = [ 'outbound-links' ];
+ public function enableSelfHostedDomain( $settings ) {
+ $settings['self_hosted_domain'] = 'self-hosted-test.org';
return $settings;
}
/**
- * Enable Enhanced Measurements > Search Queries
+ * Set domain.
*
* @param $settings
*
* @return mixed
*/
- public function enableSearch( $settings ) {
- $settings['enhanced_measurements'] = [ 'search' ];
+ public function setDomain( $settings ) {
+ $settings['domain_name'] = [ 'default' => 'test.dev' ];
return $settings;
}
/**
+ * @see Helpers::get_endpoint_url()
* @return void
- * @see Helpers::get_settings()
- *
*/
- public function testGetPostSettings() {
- $_POST['action'] = 'plausible_analytics_save_options';
- $_POST['options'] = wp_json_encode( [ [ 'name' => 'post_test', 'value' => 'post_test' ] ] );
+ public function testGetDataApiUrl() {
+ delete_option( 'plausible_analytics_settings' );
+ $url = Helpers::get_endpoint_url();
+ $this->assertEquals( 'https://plausible.io/api/event', $url );
- $settings = Helpers::get_settings();
+ try {
+ add_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
- $this->assertArrayNotHasKey( 'post_test', $settings );
+ $url = Helpers::get_endpoint_url();
+
+ $this->assertMatchesRegularExpression( '~http://example.org/index.php\?rest_route=/[0-9a-z]{6}/v1/[0-9a-z]{4}/[0-9a-z]{8}~', $url );
+ } finally {
+ remove_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
+ }
+
+ try {
+ add_filter( 'plausible_analytics_settings', [ $this, 'enableSelfHostedDomain' ] );
+
+ $url = Helpers::get_endpoint_url();
+
+ $this->assertEquals( 'https://self-hosted-test.org/api/event', $url );
+ } finally {
+ remove_filter( 'plausible_analytics_settings', [ $this, 'enableSelfHostedDomain' ] );
+ }
}
/**
+ * @see Helpers::get_domain()
* @return void
- * @throws Exception
- * @see Helpers::get_proxy_resource()
*/
- public function testGetProxyResource() {
- $namespace = Helpers::get_proxy_resource( 'namespace' );
-
- $this->assertMatchesRegularExpression( '/[a-z0-9]{6}/', $namespace );
-
- $base = Helpers::get_proxy_resource( 'base' );
-
- $this->assertMatchesRegularExpression( '/[a-z0-9]{4}/', $base );
+ public function testGetDomain() {
+ try {
+ update_option( 'plausible_analytics_settings', [ 'domain_name' => [ 'default' => 'example.org' ] ] );
+ $domain = Helpers::get_domain();
- $endpoint = Helpers::get_proxy_resource( 'endpoint' );
+ $this->assertEquals( 'example.org', $domain );
- $this->assertMatchesRegularExpression( '/[a-z0-9]{8}/', $endpoint );
+ add_filter( 'plausible_analytics_settings', [ $this, 'setDomain' ] );
- $cache_dir = Helpers::get_proxy_resource( 'cache_dir' );
- $upload_dir = wp_get_upload_dir()['basedir'];
+ $domain = Helpers::get_domain();
- $this->assertMatchesRegularExpression( "~$upload_dir/[a-z0-9]{10}/~", $cache_dir );
- $this->assertTrue( is_dir( $cache_dir ) );
+ $this->assertEquals( 'test.dev', $domain );
+ } finally {
+ remove_filter( 'plausible_analytics_settings', [ $this, 'setDomain' ] );
+ }
+ }
- $cache_url = Helpers::get_proxy_resource( 'cache_url' );
- $upload_url = wp_get_upload_dir()['baseurl'];
+ /**
+ * @see Helpers::get_domain()
+ * @return void
+ */
+ public function testGetDomainWithDefaultOnly() {
+ $settings = [
+ 'domain_name' => [
+ 'default' => 'example.com',
+ 'fr' => '',
+ ],
+ ];
+
+ update_option( 'plausible_analytics_settings', $settings );
+
+ $filter_mode = function () {
+ return true;
+ };
+ $filter_domains = function () {
+ return [ 'fr' => 'example.fr' ];
+ };
+ $filter_lang = function () {
+ return 'fr';
+ };
+ $filter_key = function () {
+ return 'fr';
+ };
+
+ add_filter( 'plausible_analytics_language_per_domain_mode', $filter_mode );
+ add_filter( 'wpml_setting', $filter_domains, 10, 2 );
+ add_filter( 'wpml_current_language', $filter_lang );
+ add_filter( 'plausible_analytics_current_language_domain_key', $filter_key );
- $this->assertMatchesRegularExpression( "~$upload_url/[a-z0-9]{10}/~", $cache_url );
+ try {
+ $domain = Helpers::get_domain();
+ $this->assertEquals( 'example.com', $domain );
+ } finally {
+ remove_filter( 'plausible_analytics_language_per_domain_mode', $filter_mode );
+ remove_filter( 'wpml_setting', $filter_domains );
+ remove_filter( 'wpml_current_language', $filter_lang );
+ remove_filter( 'plausible_analytics_current_language_domain_key', $filter_key );
+ }
}
/**
+ * @see Helpers::get_domain()
* @return void
- * @see Helpers::update_setting()
*/
- public function testUpdateSetting() {
- Helpers::update_setting( 'test', true );
+ public function testGetDomainWithLanguageKey() {
+ $settings = [
+ 'domain_name' => [
+ 'default' => 'example.com',
+ 'fr' => 'example.fr',
+ ],
+ ];
+
+ update_option( 'plausible_analytics_settings', $settings );
+
+ $filter_mode = function () {
+ return true;
+ };
+ $filter_domains = function () {
+ return [ 'fr' => 'example.fr' ];
+ };
+ $filter_lang = function () {
+ return 'fr';
+ };
+ $filter_key = function () {
+ return 'fr';
+ };
+
+ add_filter( 'plausible_analytics_language_per_domain_mode', $filter_mode );
+ add_filter( 'wpml_setting', $filter_domains, 10, 2 );
+ add_filter( 'wpml_current_language', $filter_lang );
+ add_filter( 'plausible_analytics_current_language_domain_key', $filter_key );
- $this->assertTrue( Helpers::get_settings()['test'] );
+ try {
+ $domain = Helpers::get_domain();
+ $this->assertEquals( 'example.fr', $domain );
+ } finally {
+ remove_filter( 'plausible_analytics_language_per_domain_mode', $filter_mode );
+ remove_filter( 'wpml_setting', $filter_domains );
+ remove_filter( 'wpml_current_language', $filter_lang );
+ remove_filter( 'plausible_analytics_current_language_domain_key', $filter_key );
+ }
}
/**
+ * @see Helpers::get_js_path()
* @return void
* @throws Exception
- * @see Helpers::get_js_path()
*/
public function testGetJsPath() {
$path = TestableHelpers::get_js_path();
@@ -159,41 +226,19 @@ public function testGetJsPath() {
}
/**
- * @return void
- * @see Helpers::get_domain()
+ * @see Helpers::get_js_url()
*/
- public function testGetDomain() {
- try {
- delete_option( 'plausible_analytics_settings' );
- $domain = Helpers::get_domain();
-
- $this->assertEquals( 'example.org', $domain );
-
- add_filter( 'plausible_analytics_settings', [ $this, 'setDomain' ] );
-
- $domain = Helpers::get_domain();
-
- $this->assertEquals( 'test.dev', $domain );
- } finally {
- remove_filter( 'plausible_analytics_settings', [ $this, 'setDomain' ] );
- }
- }
+ public function testGetJsUrl() {
+ $url = TestableHelpers::get_js_url();
- /**
- * @return void
- * @see Helpers::get_endpoint_url()
- */
- public function testGetDataApiUrl() {
- delete_option( 'plausible_analytics_settings' );
- $url = Helpers::get_endpoint_url();
- $this->assertEquals( 'https://plausible.io/api/event', $url );
+ $this->assertEquals( 'https://plausible.io/js/pa-test-tracker-id.js', $url );
try {
add_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
- $url = Helpers::get_endpoint_url();
+ $url = TestableHelpers::get_js_url( true );
- $this->assertMatchesRegularExpression( '~http://example.org/index.php\?rest_route=/[0-9a-z]{6}/v1/[0-9a-z]{4}/[0-9a-z]{8}~', $url );
+ $this->assertMatchesRegularExpression( '~http://example.org/wp-content/uploads/.*?/.*?.js~', $url );
} finally {
remove_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
}
@@ -201,18 +246,62 @@ public function testGetDataApiUrl() {
try {
add_filter( 'plausible_analytics_settings', [ $this, 'enableSelfHostedDomain' ] );
- $url = Helpers::get_endpoint_url();
+ $url = TestableHelpers::get_js_url();
- $this->assertEquals( 'https://self-hosted-test.org/api/event', $url );
+ $this->assertEquals( 'https://self-hosted-test.org/js/pa-test-tracker-id.js', $url );
} finally {
remove_filter( 'plausible_analytics_settings', [ $this, 'enableSelfHostedDomain' ] );
}
}
/**
+ * @see Helpers::get_settings()
+ *
+ * @return void
+ */
+ public function testGetPostSettings() {
+ $_POST['action'] = 'plausible_analytics_save_options';
+ $_POST['options'] = wp_json_encode( [ [ 'name' => 'post_test', 'value' => 'post_test' ] ] );
+
+ $settings = Helpers::get_settings();
+
+ $this->assertArrayNotHasKey( 'post_test', $settings );
+ }
+
+ /**
+ * @see Helpers::get_proxy_resource()
* @return void
* @throws Exception
+ */
+ public function testGetProxyResource() {
+ $namespace = Helpers::get_proxy_resource( 'namespace' );
+
+ $this->assertMatchesRegularExpression( '/[a-z0-9]{6}/', $namespace );
+
+ $base = Helpers::get_proxy_resource( 'base' );
+
+ $this->assertMatchesRegularExpression( '/[a-z0-9]{4}/', $base );
+
+ $endpoint = Helpers::get_proxy_resource( 'endpoint' );
+
+ $this->assertMatchesRegularExpression( '/[a-z0-9]{8}/', $endpoint );
+
+ $cache_dir = Helpers::get_proxy_resource( 'cache_dir' );
+ $upload_dir = wp_get_upload_dir()['basedir'];
+
+ $this->assertMatchesRegularExpression( "~$upload_dir/[a-z0-9]{10}/~", $cache_dir );
+ $this->assertTrue( is_dir( $cache_dir ) );
+
+ $cache_url = Helpers::get_proxy_resource( 'cache_url' );
+ $upload_url = wp_get_upload_dir()['baseurl'];
+
+ $this->assertMatchesRegularExpression( "~$upload_url/[a-z0-9]{10}/~", $cache_url );
+ }
+
+ /**
* @see Helpers::get_rest_endpoint()
+ * @return void
+ * @throws Exception
*/
public function testGetRestEndpoint() {
$endpoint = Helpers::get_rest_endpoint( false );
@@ -223,4 +312,36 @@ public function testGetRestEndpoint() {
$this->assertMatchesRegularExpression( '~http://example.org/index.php\?rest_route=/[0-9a-z]{6}/v1/[0-9a-z]{4}/[0-9a-z]{8}~', $endpoint );
}
+
+ /**
+ * @see Helpers::get_settings()
+ * @return void
+ */
+ public function testGetSettingsNormalization() {
+ $settings = [
+ 'domain_name' => 'example.com',
+ 'api_token' => 'test-token',
+ 'shared_link' => 'https://plausible.io/share/example.com',
+ ];
+
+ update_option( 'plausible_analytics_settings', $settings );
+
+ $normalized_settings = Helpers::get_settings();
+
+ $this->assertIsArray( $normalized_settings['api_token'] );
+ $this->assertEquals( [ 'default' => 'test-token' ], $normalized_settings['api_token'] );
+
+ $this->assertIsArray( $normalized_settings['shared_link'] );
+ $this->assertEquals( [ 'default' => 'https://plausible.io/share/example.com' ], $normalized_settings['shared_link'] );
+ }
+
+ /**
+ * @see Helpers::update_setting()
+ * @return void
+ */
+ public function testUpdateSetting() {
+ Helpers::update_setting( 'test', true );
+
+ $this->assertTrue( Helpers::get_settings()['test'] );
+ }
}
diff --git a/webpack.config.js b/webpack.config.js
index 9a418057..ced7077f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -13,6 +13,7 @@ const config = {
mode,
entry: {
'plausible-admin': ['./assets/src/css/admin/main.css', './assets/src/js/admin/main.js'],
+ 'plausible-admin-notice': ['./assets/src/js/admin/notice.js'],
'plausible-affiliate-links': ['./assets/src/js/affiliate-links.js'],
'plausible-woocommerce-integration': ['./assets/src/js/integrations/woocommerce.js'],
'plausible-form-submit-integration': ['./assets/src/js/integrations/form-submit.js']