PluginProbe
Plausible Analytics / trunk
Plausible Analytics vtrunk
2.6.1 2.6.0 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 52 releases
plausible-analytics / src / Helpers.php

Helpers.php in Plausible Analytics trunk, at src/Helpers.php

504 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plausible Analytics | Helpers
4 *
5 * @since 1.0.0
6 * @package WordPress
7 * @subpackage Plausible Analytics
8 */
9
10 namespace Plausible\Analytics\WP;
11
12 use Exception;
13
14 /**
15 * We use 'static' (late static binding) instead of 'self' (early binding), so we can mock (where needed) in our tests.
16 */
17 class Helpers {
18 /**
19 * Returns the API token.
20 *
21 * @return string
22 */
23 public static function get_api_token() {
24 $settings = static::get_settings();
25 $current = static::get_current_language_domain_key();
26
27 return $settings['api_token'][ $current ] ?? $settings['api_token']['default'] ?? '';
28 }
29
30 /**
31 * Get Settings.
32 *
33 * @since 1.0.0
34 * @access public
35 * @return array
36 */
37 public static function get_settings() {
38 $defaults = [
39 'domain_name' => [ 'default' => '' ],
40 'api_token' => [ 'default' => '' ],
41 'enhanced_measurements' => [
42 EnhancedMeasurements::FOUR_O_FOUR,
43 EnhancedMeasurements::FILE_DOWNLOADS,
44 EnhancedMeasurements::OUTBOUND_LINKS,
45 EnhancedMeasurements::FORM_COMPLETIONS,
46 EnhancedMeasurements::SEARCH_QUERIES,
47 ],
48 'affiliate_links' => [],
49 'query_params' => [],
50 'proxy_enabled' => '',
51 'enable_analytics_dashboard' => '',
52 'shared_link' => [ 'default' => '' ],
53 'excluded_pages' => '',
54 'tracked_user_roles' => [],
55 'expand_dashboard_access' => [],
56 'disable_toolbar_menu' => '',
57 'self_hosted_domain' => '',
58 'self_hosted_shared_link' => '',
59 ];
60
61 $settings = function_exists( 'get_option' ) ? get_option( 'plausible_analytics_settings', [] ) : [];
62 $settings = wp_parse_args( $settings, $defaults );
63
64 /**
65 * Normalization: Ensure domain_name and api_token are always arrays.
66 */
67 if ( ! is_array( $settings['domain_name'] ) ) {
68 $settings['domain_name'] = [ 'default' => $settings['domain_name'] ];
69 }
70
71 if ( ! is_array( $settings['api_token'] ) ) {
72 $settings['api_token'] = [ 'default' => $settings['api_token'] ];
73 }
74
75 if ( ! is_array( $settings['shared_link'] ) ) {
76 $settings['shared_link'] = [ 'default' => $settings['shared_link'] ];
77 }
78
79 return apply_filters( 'plausible_analytics_settings', $settings );
80 }
81
82 /**
83 * Returns the key of the currently used Language Domain.
84 *
85 * @since v2.6.0
86 *
87 * @return string
88 *
89 * @codeCoverageIgnore Because it depends on 3rd party plugins.
90 */
91 public static function get_current_language_domain_key() {
92 if ( ! static::is_language_per_domain_mode() ) {
93 return 'default';
94 }
95
96 $language_domains = apply_filters( 'wpml_setting', [], 'language_domains' );
97 $current_language = apply_filters( 'wpml_current_language', null );
98
99 if ( $current_language && isset( $language_domains[ $current_language ] ) ) {
100 return (string) apply_filters( 'plausible_analytics_current_language_domain_key', $current_language );
101 }
102
103 return (string) apply_filters( 'plausible_analytics_current_language_domain_key', 'default' );
104 }
105
106 /**
107 * Returns true only when WPML is active, its negotiation type is "different domain per language", AND at least one domain is configured.
108 *
109 * @since v2.6.0
110 *
111 * @return bool
112 *
113 * @codeCoverageIgnore Because it depends on 3rd party plugins.
114 */
115 public static function is_language_per_domain_mode() {
116 static $is_language_per_domain;
117
118 if ( defined( 'PLAUSIBLE_CI' ) && PLAUSIBLE_CI ) {
119 $is_language_per_domain = null;
120 }
121
122 if ( $is_language_per_domain !== null ) {
123 return $is_language_per_domain; // @codeCoverageIgnore
124 }
125
126 /**
127 * If WPML is not active, we can assume we're not in language per domain mode.
128 */
129 if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
130 return $is_language_per_domain = (bool) apply_filters( 'plausible_analytics_language_per_domain_mode', false );
131 }
132
133 $negotiation_type = (int) apply_filters( 'wpml_setting', 0, 'language_negotiation_type' );
134 $domains = apply_filters( 'wpml_setting', [], 'language_domains' );
135 $value = $negotiation_type === 2 && ! empty( $domains );
136
137 return $is_language_per_domain = (bool) apply_filters( 'plausible_analytics_language_per_domain_mode', $value );
138 }
139
140 /**
141 * Returns the name of the current Plausible domain.
142 *
143 * @since v2.6.0 This is now mapped to language domains to provide compatibility with multilang plugins, like WPML.
144 *
145 * @return string
146 */
147 public static function get_domain() {
148 $settings = static::get_settings();
149 $current_key = static::get_current_language_domain_key();
150 $domain_name = $settings['domain_name'][ $current_key ] ?? '';
151
152 if ( ! empty( $domain_name ) ) {
153 return $domain_name;
154 }
155
156 if ( ! empty( $settings['domain_name']['default'] ) ) {
157 return $settings['domain_name']['default'];
158 }
159
160 $url = home_url();
161
162 return preg_replace( '/^http(s?):\/\/(www\.)?/i', '', $url );
163 }
164
165 /**
166 * Get Data API URL.
167 *
168 * @since 1.2.2
169 * @access public
170 * @return string
171 * @throws Exception
172 */
173 public static function get_endpoint_url() {
174 if ( static::proxy_enabled() ) {
175 // This will make sure the API endpoint is properly registered when we're testing.
176 $append = isset( $_GET['plausible_proxy'] ) ? '?plausible_proxy=1' : '';
177
178 return static::get_rest_endpoint() . $append;
179 }
180
181 return esc_url( static::get_hosted_domain_url() . '/api/event' );
182 }
183
184 /**
185 * Is the proxy enabled?
186 *
187 * @param array $settings Allows passing a current settings object.
188 *
189 * @return bool
190 */
191 public static function proxy_enabled( $settings = [] ) {
192 if ( empty( $settings ) ) {
193 $settings = static::get_settings();
194 }
195
196 return ! empty( $settings['proxy_enabled'] ) || isset( $_GET['plausible_proxy'] );
197 }
198
199 /**
200 * Returns the Proxy's REST endpoint.
201 *
202 * @return string
203 * @throws Exception
204 */
205 public static function get_rest_endpoint( $abs_url = true ) {
206 $namespace = static::get_proxy_resource( 'namespace' );
207 $base = static::get_proxy_resource( 'base' );
208 $endpoint = static::get_proxy_resource( 'endpoint' );
209
210 $uri = "$namespace/v1/$base/$endpoint";
211
212 if ( $abs_url ) {
213 return get_rest_url( null, $uri );
214 }
215
216 return '/' . rest_get_url_prefix() . '/' . $uri;
217 }
218
219 /**
220 * Get a proxy resource by name.
221 *
222 * @param string $resource_name
223 *
224 * @return string Value of resource from DB or empty string if Bypass ad blockers option is disabled.
225 * @throws Exception
226 */
227 public static function get_proxy_resource( $resource_name = '' ) {
228 $resources = static::get_proxy_resources();
229
230 /**
231 * Create the cache directory if it doesn't exist.
232 */
233 if ( ( $resource_name === 'cache_dir' || $resource_name === 'cache_url' ) && ! is_dir( $resources['cache_dir'] ) ) {
234 wp_mkdir_p( $resources[ $resource_name ] );
235 }
236
237 return $resources[ $resource_name ] ?? '';
238 }
239
240 /**
241 * Get (and generate/store if non-existent) proxy resources.
242 *
243 * @return array
244 * @throws Exception
245 *
246 * @codeCoverageIgnore
247 */
248 public static function get_proxy_resources() {
249 static $resources;
250
251 if ( $resources === null ) {
252 $resources = get_option( 'plausible_analytics_proxy_resources', [] );
253 }
254
255 /**
256 * Force a refresh of our resources if the user recently switched to SSL and we still have non-SSL resources stored.
257 */
258 if ( ! empty( $resources ) && is_ssl() && isset( $resources['cache_url'] ) && ( strpos( $resources['cache_url'], 'http:' ) !== false ) ) {
259 $resources = [];
260 }
261
262 if ( empty( $resources ) ) {
263 $cache_dir = bin2hex( random_bytes( 5 ) );
264 $upload_dir = wp_get_upload_dir();
265 $resources = [
266 'namespace' => bin2hex( random_bytes( 3 ) ),
267 'base' => bin2hex( random_bytes( 2 ) ),
268 'endpoint' => bin2hex( random_bytes( 4 ) ),
269 'cache_dir' => trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $cache_dir ),
270 'cache_url' => trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $cache_dir ),
271 ];
272
273 update_option( 'plausible_analytics_proxy_resources', $resources );
274 }
275
276 return $resources;
277 }
278
279 /**
280 * Returns the URL of the domain where Plausible Analytics is hosted: self-hosted or cloud.
281 *
282 * @return string
283 */
284 public static function get_hosted_domain_url() {
285 $settings = static::get_settings();
286
287 if ( defined( 'PLAUSIBLE_SELF_HOSTED_DOMAIN' ) ) {
288 return esc_url( 'https://' . PLAUSIBLE_SELF_HOSTED_DOMAIN ); // @codeCoverageIgnore
289 }
290
291 if ( ! empty( $settings['self_hosted_domain'] ) ) {
292 /**
293 * Until proven otherwise, let's just assume people are all on SSL.
294 */
295 return esc_url( 'https://' . $settings['self_hosted_domain'] );
296 }
297
298 return esc_url( 'https://plausible.io' );
299 }
300
301 /**
302 * A convenient way to retrieve the absolute path to the local JS file. Proxy should be enabled when this method is called!
303 *
304 * @return string
305 * @throws Exception
306 */
307 public static function get_js_path() {
308 $filename = static::get_filename();
309
310 if ( empty( $filename ) ) {
311 return ''; // @codeCoverageIgnore
312 }
313
314 return static::get_proxy_resource( 'cache_dir' ) . $filename . '.js';
315 }
316
317 /**
318 * Get filename (without file extension)
319 *
320 * @since 1.3.0
321 * @return string
322 * @throws Exception
323 *
324 * @codeCoverageIgnore
325 */
326 public static function get_filename() {
327 $client = static::get_client();
328
329 if ( $client instanceof Client ) {
330 return $client->get_tracker_id( static::get_current_language_domain_key() );
331 }
332
333 return '';
334 }
335
336 /**
337 * Build the API client.
338 *
339 * @param string $domain_key
340 *
341 * @return false|Client
342 *
343 * @codeCoverageIgnore This seam's only function is to keep our code testable.
344 */
345 protected static function get_client( $domain_key = '' ) {
346 $client = new ClientFactory( '', $domain_key );
347
348 return $client->build();
349 }
350
351 /**
352 * Get Analytics URL. Returns an empty string if @see self::get_filename() returns empty.
353 *
354 * @since 1.0.0
355 *
356 * @return string
357 * @throws Exception
358 */
359 public static function get_js_url( $local = false ) {
360 $file_name = static::get_filename();
361
362 if ( empty( $file_name ) ) {
363 return ''; // @codeCoverageIgnore
364 }
365
366 /**
367 * If the Avoid Ad Blockers option is enabled, return URL pointing to the local file.
368 */
369 if ( $local && static::proxy_enabled() ) {
370 return esc_url( static::get_proxy_resource( 'cache_url' ) . $file_name . '.js' );
371 }
372
373 return esc_url( static::get_hosted_domain_url() . "/js/$file_name.js" );
374 }
375
376 /**
377 * @since v2.6.0 Provide compatibility with multilang plugins, like WPML.
378 *
379 * @return array
380 *
381 * @codeCoverageIgnore Because it depends on 3rd party plugins.
382 */
383 public static function get_language_domains() {
384 $domains = apply_filters( 'wpml_setting', [], 'language_domains' );
385 $main = wp_parse_url( home_url(), PHP_URL_HOST ) ?: home_url();
386
387 // WPML's language_domains omits the default language; prepend the main WP domain.
388 if ( ! in_array( $main, $domains, true ) ) {
389 $domains = array_merge( [ 'default' => $main ], $domains );
390 }
391
392 return apply_filters( 'plausible_analytics_language_domains', $domains );
393 }
394
395 /**
396 * Get the name of the active multilang plugin.
397 *
398 * @return string
399 *
400 * @codeCoverageIgnore Because it depends on 3rd party plugins.
401 */
402 public static function get_multilang_plugin_name() {
403 $name = '';
404
405 if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
406 $name = 'WPML';
407 }
408
409 return apply_filters( 'plausible_analytics_multilang_plugin_name', $name );
410 }
411
412 /**
413 * Get the user role for the logged-in user.
414 *
415 * @since 1.3.0
416 * @access public
417 * @return string
418 */
419 public static function get_user_role() {
420 global $current_user;
421
422 $user_roles = $current_user->roles;
423
424 return array_shift( $user_roles );
425 }
426
427 /**
428 * Wrapper to check if the Cloaked Affiliate Links option contains any values.
429 *
430 * @param array $settings Allows passing a current settings object.
431 *
432 * @return bool
433 */
434 public static function is_cloaked_affiliate_links_enabled( $settings = [] ) {
435 if ( empty( $settings ) ) {
436 $settings = static::get_settings();
437 }
438
439 return static::setting_has_values( $settings, 'affiliate_links' );
440 }
441
442 /**
443 * Checks if a given array-type setting, within the given settings array, contains any non-empty values.
444 * Unlike a "get or default" helper, this never falls back to fetching live settings, so it's safe to
445 * reuse with partial or intentionally empty settings arrays (e.g., old vs. new option values).
446 *
447 * @param array $settings The settings array to check against.
448 * @param string $key The settings key to check.
449 *
450 * @return bool
451 */
452 public static function setting_has_values( array $settings, $key ) {
453 $value = $settings[ $key ] ?? [];
454
455 // Assume it's an empty string.
456 if ( ! is_array( $value ) ) {
457 $value = []; // @codeCoverageIgnore
458 }
459
460 return ! empty( array_filter( $value ) );
461 }
462
463 /**
464 * Wrapper to check if the Query Params option contains any values.
465 *
466 * @return bool
467 */
468 public static function is_query_params_enabled( $settings = [] ) {
469 if ( empty( $settings ) ) {
470 $settings = static::get_settings();
471 }
472
473 return static::setting_has_values( $settings, 'query_params' );
474 }
475
476 /**
477 * Checks if the main Plausible Analytics script is registered.
478 *
479 * @return bool
480 */
481 public static function main_script_is_registered() {
482 return wp_script_is( 'plausible-analytics', 'registered' );
483 }
484
485 /**
486 * @param string $option_name
487 * @param array|string|int $option_value
488 * @param string $key
489 *
490 * @return void
491 */
492 public static function update_setting( $option_name, $option_value, $key = '' ) {
493 $settings = static::get_settings();
494
495 if ( ! empty( $key ) && is_array( $settings[ $option_name ] ) ) {
496 $settings[ $option_name ][ $key ] = $option_value;
497 } else {
498 $settings[ $option_name ] = $option_value;
499 }
500
501 update_option( 'plausible_analytics_settings', $settings );
502 }
503 }
504