PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.5.2
LatePoint – Calendar Booking Plugin for Appointments and Events v5.5.2
5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / kit / bsf-analytics / class-bsf-analytics.php
latepoint / lib / kit / bsf-analytics Last commit date
assets 4 months ago classes 4 months ago modules 2 months ago class-bsf-analytics-events.php 2 months ago class-bsf-analytics-loader.php 4 months ago class-bsf-analytics-stats.php 2 months ago class-bsf-analytics.php 2 months ago version.json 2 months ago
class-bsf-analytics.php
690 lines
1 <?php
2 /**
3 * BSF analytics class file.
4 *
5 * @version 1.0.0
6 *
7 * @package bsf-analytics
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 if ( ! class_exists( 'BSF_Analytics' ) ) {
15
16 /**
17 * BSF analytics
18 */
19 class BSF_Analytics {
20
21 /**
22 * Member Variable
23 *
24 * @var array Entities data.
25 */
26 private $entities;
27
28 /**
29 * Member Variable
30 *
31 * @var string Usage tracking document URL
32 */
33 public $usage_doc_link = 'https://store.brainstormforce.com/usage-tracking/';
34
35 /**
36 * Setup actions, load files.
37 *
38 * @param array $args entity data for analytics.
39 * @param string $analytics_path directory path to analytics library.
40 * @param float $analytics_version analytics library version.
41 * @since 1.0.0
42 */
43 public function __construct( $args, $analytics_path, $analytics_version ) {
44
45 // Bail when no analytics entities are registered.
46 if ( empty( $args ) ) {
47 return;
48 }
49
50 $this->entities = $args;
51
52 // Run migration from old "analytics" option names to new "usage" names.
53 $this->maybe_migrate_options();
54
55 define( 'BSF_ANALYTICS_VERSION', $analytics_version );
56 define( 'BSF_ANALYTICS_URI', $this->get_analytics_url( $analytics_path ) );
57
58 add_action( 'admin_init', array( $this, 'handle_optin_optout' ) );
59 add_action( 'admin_init', array( $this, 'option_notice' ) );
60 add_action( 'init', array( $this, 'maybe_track_analytics' ), 99 );
61
62 $this->set_actions();
63
64 add_action( 'admin_init', array( $this, 'register_usage_tracking_setting' ) );
65
66 $this->includes();
67
68 $this->load_deactivation_survey_actions();
69 }
70
71 /**
72 * Function to load the deactivation survey form actions.
73 *
74 * @since 1.1.6
75 * @return void
76 */
77 public function load_deactivation_survey_actions() {
78
79 // If not in a admin area then return it.
80 if ( ! is_admin() ) {
81 return;
82 }
83
84 add_filter( 'uds_survey_vars', array( $this, 'add_slugs_to_uds_vars' ) );
85 add_action( 'admin_footer', array( $this, 'load_deactivation_survey_form' ) );
86 }
87
88 /**
89 * Setup actions for admin notice style and analytics cron event.
90 *
91 * @since 1.0.4
92 */
93 public function set_actions() {
94
95 foreach ( $this->entities as $key => $data ) {
96 add_action( 'astra_notice_before_markup_' . $key . '-optin-notice', array( $this, 'enqueue_assets' ) );
97 add_action( 'update_option_' . $key . '_usage_optin', array( $this, 'update_analytics_option_callback' ), 10, 3 );
98 add_action( 'add_option_' . $key . '_usage_optin', array( $this, 'add_analytics_option_callback' ), 10, 2 );
99 }
100 }
101
102 /**
103 * BSF Analytics URL
104 *
105 * @param string $analytics_path directory path to analytics library.
106 * @return String URL of bsf-analytics directory.
107 * @since 1.0.0
108 */
109 public function get_analytics_url( $analytics_path ) {
110
111 $content_dir_path = wp_normalize_path( WP_CONTENT_DIR );
112
113 $analytics_path = wp_normalize_path( $analytics_path );
114
115 return str_replace( $content_dir_path, content_url(), $analytics_path );
116 }
117
118 /**
119 * Enqueue Scripts.
120 *
121 * @since 1.0.0
122 * @return void
123 */
124 public function enqueue_assets() {
125
126 /**
127 * Load unminified if SCRIPT_DEBUG is true.
128 *
129 * Directory and Extensions.
130 */
131 $dir_name = ( SCRIPT_DEBUG ) ? 'unminified' : 'minified';
132 $file_rtl = ( is_rtl() ) ? '-rtl' : '';
133 $css_ext = ( SCRIPT_DEBUG ) ? '.css' : '.min.css';
134
135 $css_uri = BSF_ANALYTICS_URI . '/assets/css/' . $dir_name . '/style' . $file_rtl . $css_ext;
136
137 wp_enqueue_style( 'bsf-analytics-admin-style', $css_uri, false, BSF_ANALYTICS_VERSION, 'all' );
138 }
139
140 /**
141 * Send analytics API call.
142 *
143 * @since 1.0.0
144 */
145 public function send() {
146
147 $api_url = BSF_Analytics_Helper::get_api_url();
148
149 wp_remote_post(
150 $api_url . 'api/analytics/',
151 array(
152 'body' => BSF_Analytics_Stats::instance()->get_stats(),
153 'timeout' => 5,
154 'blocking' => false,
155 )
156 );
157 }
158
159 /**
160 * Check if usage tracking is enabled.
161 *
162 * @return bool
163 * @since 1.0.0
164 */
165 public function is_tracking_enabled() {
166
167 // Global kill switch — allows hosting providers, compliance plugins,
168 // or agency developers to disable all BSF tracking with one filter.
169 if ( ! apply_filters( 'bsf_usage_tracking_enabled', true ) ) {
170 return false;
171 }
172
173 foreach ( $this->entities as $key => $data ) {
174
175 $is_enabled = get_site_option( $key . '_usage_optin', false ) === 'yes' ? true : false;
176 $is_enabled = $this->is_white_label_enabled( $key ) ? false : $is_enabled;
177
178 if ( apply_filters( $key . '_tracking_enabled', $is_enabled ) ) {
179 return true;
180 }
181 }
182
183 return false;
184 }
185
186 /**
187 * Check if WHITE label is enabled for BSF products.
188 *
189 * @param string $source source of analytics.
190 * @return bool
191 * @since 1.0.0
192 */
193 public function is_white_label_enabled( $source ) {
194
195 $options = apply_filters( $source . '_white_label_options', array() );
196 $is_enabled = false;
197
198 if ( is_array( $options ) ) {
199 foreach ( $options as $option ) {
200 if ( true === $option ) {
201 $is_enabled = true;
202 break;
203 }
204 }
205 }
206
207 return $is_enabled;
208 }
209
210 /**
211 * Get usage doc link with UTM parameters.
212 *
213 * Appends product-specific UTM params to the default usage tracking URL
214 * so we can attribute which plugin's link was clicked.
215 *
216 * @param string $product_key Product key (e.g., 'spectra', 'surerank').
217 * @param string $context Where the link appears ('notice' or 'settings').
218 * @return string Full URL with UTM parameters.
219 * @since 1.1.23
220 */
221 public function get_usage_doc_link( $product_key, $context = 'notice' ) {
222 return add_query_arg(
223 array(
224 'utm_source' => $product_key,
225 'utm_medium' => $context,
226 'utm_campaign' => 'usage_tracking',
227 ),
228 $this->usage_doc_link
229 );
230 }
231
232 /**
233 * Display admin notice for usage tracking.
234 *
235 * @since 1.0.0
236 */
237 public function option_notice() {
238
239 if ( ! current_user_can( 'manage_options' ) ) {
240 return;
241 }
242
243 if( $this->is_tracking_enabled() ) {
244 return; // Don't need to display notice if any of our plugin already have the permission.
245 }
246
247 // If the user has opted out of tracking, don't show the notice till 7 days.
248 $last_displayed_time = get_site_option( 'bsf_usage_last_displayed_time', false );
249 if ( $last_displayed_time && $last_displayed_time > time() - ( 7 * DAY_IN_SECONDS ) ) {
250 return; // Don't display the notice if it was displayed recently.
251 }
252
253 foreach ( $this->entities as $key => $data ) {
254
255 $time_to_display = isset( $data['time_to_display'] ) ? $data['time_to_display'] : '+24 hours';
256 $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'notice' );
257
258 // Don't display the notice if tracking is disabled or White Label is enabled for any of our plugins.
259 if ( false !== get_site_option( $key . '_usage_optin', false ) || $this->is_white_label_enabled( $key ) ) {
260 continue;
261 }
262
263 // Show tracker consent notice after 24 hours from installed time.
264 if ( strtotime( $time_to_display, $this->get_analytics_install_time( $key ) ) > time() ) {
265 continue;
266 }
267
268 /* translators: %s product name */
269 $notice_string = sprintf(
270 __(
271 '<strong>Help shape the future of %1$s.</strong><br><br>Share how you use the plugin so we can build features that matter, fix issues faster, and make smarter decisions.'
272 ),
273 esc_html( $data['product_name'] )
274 );
275
276 if ( is_multisite() ) {
277 $notice_string .= __( 'This will be applicable for all sites from the network.' );
278 }
279
280 $language_dir = is_rtl() ? 'rtl' : 'ltr';
281
282 Astra_Notices::add_notice(
283 array(
284 'id' => $key . '-optin-notice',
285 'type' => '',
286 'message' => sprintf(
287 '<div class="notice-content">
288 <div class="notice-heading">
289 %1$s
290 </div>
291 <div class="astra-notices-container">
292 <a href="%2$s" class="astra-notices button-primary">
293 %3$s
294 </a>
295 <a href="%4$s" data-repeat-notice-after="%5$s" class="astra-notices button-secondary">
296 %6$s
297 </a>
298 </div>
299 </div>',
300 /* translators: %s usage doc link */
301 sprintf( $notice_string . '<span dir="%1s"> <a href="%2s" target="_blank" rel="noreferrer noopener">%3s</a><span><br><br>', $language_dir, esc_url( $usage_doc_link ), __( 'Learn more.' ) ),
302 esc_url(
303 add_query_arg(
304 array(
305 $key . '_analytics_optin' => 'yes',
306 $key . '_analytics_nonce' => wp_create_nonce( $key . '_analytics_optin' ),
307 'bsf_analytics_source' => $key,
308 )
309 )
310 ),
311 __( 'Happy to help!' ),
312 esc_url(
313 add_query_arg(
314 array(
315 $key . '_analytics_optin' => 'no',
316 $key . '_analytics_nonce' => wp_create_nonce( $key . '_analytics_optin' ),
317 'bsf_analytics_source' => $key,
318 )
319 )
320 ),
321 MONTH_IN_SECONDS,
322 __( 'Skip' )
323 ),
324 'show_if' => true,
325 'repeat-notice-after' => false,
326 'priority' => 18,
327 'display-with-other-notices' => true,
328 )
329 );
330
331 return;
332 }
333 }
334
335 /**
336 * Process usage tracking opt out.
337 *
338 * @since 1.0.0
339 */
340 public function handle_optin_optout() {
341
342 if ( ! current_user_can( 'manage_options' ) ) {
343 return;
344 }
345
346 // Verify nonce before accessing any $_GET data.
347 // The nonce key is dynamic per entity, so iterate to find a valid one.
348 $source = '';
349 foreach ( $this->entities as $key => $data ) {
350 $nonce_key = $key . '_analytics_nonce';
351 if ( isset( $_GET[ $nonce_key ] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ $nonce_key ] ) ), $key . '_analytics_optin' ) ) {
352 $source = $key;
353 break;
354 }
355 }
356
357 if ( empty( $source ) ) {
358 return;
359 }
360
361 $optin_status = isset( $_GET[ $source . '_analytics_optin' ] ) ? sanitize_text_field( wp_unslash( $_GET[ $source . '_analytics_optin' ] ) ) : '';
362
363 if ( 'yes' === $optin_status ) {
364 $this->optin( $source );
365 } elseif ( 'no' === $optin_status ) {
366 $this->optout( $source );
367 }
368
369 wp_safe_redirect(
370 esc_url_raw(
371 remove_query_arg(
372 array(
373 $source . '_analytics_optin',
374 $source . '_analytics_nonce',
375 'bsf_analytics_source',
376 )
377 )
378 )
379 );
380 exit;
381 }
382
383 /**
384 * Opt in to usage tracking.
385 *
386 * @param string $source source of analytics.
387 * @since 1.0.0
388 */
389 private function optin( $source ) {
390 update_site_option( $source . '_usage_optin', 'yes' );
391 }
392
393 /**
394 * Opt out of usage tracking.
395 *
396 * @param string $source source of analytics.
397 * @since 1.0.0
398 */
399 private function optout( $source ) {
400 update_site_option( $source . '_usage_optin', 'no' );
401 update_site_option( 'bsf_usage_last_displayed_time', time() );
402
403 // Clear tracking transient immediately so opt-out takes effect right away.
404 delete_site_transient( 'bsf_usage_track' );
405 }
406
407 /**
408 * Load analytics stat class.
409 *
410 * @since 1.0.0
411 */
412 private function includes() {
413 require_once __DIR__ . '/classes/class-bsf-analytics-helper.php';
414 require_once __DIR__ . '/class-bsf-analytics-stats.php';
415 require_once __DIR__ . '/class-bsf-analytics-events.php';
416
417 // Loads all the modules.
418 require_once __DIR__ . '/modules/deactivation-survey/classes/class-deactivation-survey-feedback.php';
419 require_once __DIR__ . '/modules/utm-analytics.php';
420 }
421
422 /**
423 * Migrate old "analytics" options to new "usage" naming.
424 * Copies values to new options and deletes old options.
425 *
426 * @since 1.1.17
427 */
428 private function maybe_migrate_options() {
429 if ( get_site_option( 'bsf_usage_migrated' ) ) {
430 return;
431 }
432
433 // Migrate global options.
434 $old_last_displayed = get_site_option( 'bsf_analytics_last_displayed_time' );
435 if ( false !== $old_last_displayed ) {
436 update_site_option( 'bsf_usage_last_displayed_time', $old_last_displayed );
437 delete_site_option( 'bsf_analytics_last_displayed_time' );
438 }
439
440 // Migrate per-product options.
441 foreach ( $this->entities as $key => $data ) {
442 $old_optin = get_site_option( $key . '_analytics_optin' );
443 if ( false !== $old_optin ) {
444 update_site_option( $key . '_usage_optin', $old_optin );
445 delete_site_option( $key . '_analytics_optin' );
446 }
447
448 $old_install_time = get_site_option( $key . '_analytics_installed_time' );
449 if ( false !== $old_install_time ) {
450 update_site_option( $key . '_usage_installed_time', $old_install_time );
451 delete_site_option( $key . '_analytics_installed_time' );
452 }
453 }
454
455 // Migrate transient.
456 $old_track = get_site_transient( 'bsf_analytics_track' );
457 if ( false !== $old_track ) {
458 set_site_transient( 'bsf_usage_track', $old_track, 2 * DAY_IN_SECONDS );
459 delete_site_transient( 'bsf_analytics_track' );
460 }
461
462 update_site_option( 'bsf_usage_migrated', true );
463 }
464
465 /**
466 * Register usage tracking option in General settings page.
467 *
468 * @since 1.0.0
469 */
470 public function register_usage_tracking_setting() {
471
472 foreach ( $this->entities as $key => $data ) {
473
474 if ( ! apply_filters( $key . '_tracking_enabled', true ) || $this->is_white_label_enabled( $key ) ) {
475 return;
476 }
477
478 /**
479 * Introducing a new key 'hide_optin_checkbox, which allows individual plugin to hide optin checkbox
480 * If they are providing providing in-plugin option to manage this option.
481 * from General > Settings page.
482 *
483 * @since 1.1.14
484 */
485 if( ! empty( $data['hide_optin_checkbox'] ) && true === $data['hide_optin_checkbox'] ) {
486 continue;
487 }
488
489 $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'settings' );
490 $author = isset( $data['author'] ) ? $data['author'] : 'Brainstorm Force';
491
492 register_setting(
493 'general', // Options group.
494 $key . '_usage_optin', // Option name/database.
495 array( 'sanitize_callback' => array( $this, 'sanitize_option' ) ) // sanitize callback function.
496 );
497
498 add_settings_field(
499 $key . '-usage-optin', // Field ID.
500 __( 'Usage Tracking' ), // Field title.
501 array( $this, 'render_settings_field_html' ), // Field callback function.
502 'general',
503 'default', // Settings page slug.
504 array(
505 'type' => 'checkbox',
506 'title' => $author,
507 'name' => $key . '_usage_optin',
508 'label_for' => $key . '-usage-optin',
509 'id' => $key . '-usage-optin',
510 'usage_doc_link' => $usage_doc_link,
511 )
512 );
513 }
514 }
515
516 /**
517 * Sanitize Callback Function
518 *
519 * @param bool $input Option value.
520 * @since 1.0.0
521 */
522 public function sanitize_option( $input ) {
523
524 if ( ! $input || 'no' === $input ) {
525 return 'no';
526 }
527
528 return 'yes';
529 }
530
531 /**
532 * Print settings field HTML.
533 *
534 * @param array $args arguments to field.
535 * @since 1.0.0
536 */
537 public function render_settings_field_html( $args ) {
538 $is_checked = ( 'yes' === get_site_option( $args['name'], false ) );
539 ?>
540 <fieldset>
541 <label for="<?php echo esc_attr( $args['label_for'] ); ?>">
542 <input id="<?php echo esc_attr( $args['id'] ); ?>" type="checkbox" value="1" name="<?php echo esc_attr( $args['name'] ); ?>" <?php checked( $is_checked ); ?>>
543 <?php
544 /* translators: %s Product title */
545 echo esc_html( sprintf( __( 'Help improve %s by sharing non-sensitive usage data — like PHP version and features used.' ), $args['title'] ) );// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
546
547 if ( is_multisite() ) {
548 esc_html_e( ' This will be applicable for all sites from the network.' );
549 }
550 ?>
551 </label>
552 <?php
553 echo wp_kses_post( sprintf( '<a href="%1s" target="_blank" rel="noreferrer noopener">%2s</a>', esc_url( $args['usage_doc_link'] ), __( 'Learn More.' ) ) );
554 ?>
555 </fieldset>
556 <?php
557 }
558
559 /**
560 * Get analytics installed time from option.
561 *
562 * @param string $source source of analytics.
563 * @return string $time analytics installed time.
564 * @since 1.0.0
565 */
566 private function get_analytics_install_time( $source ) {
567
568 $time = get_site_option( $source . '_usage_installed_time' );
569
570 if ( ! $time ) {
571 $time = time();
572 update_site_option( $source . '_usage_installed_time', $time );
573 }
574
575 return $time;
576 }
577
578 /**
579 * Schedule/unschedule cron event on updation of option.
580 *
581 * @param string $old_value old value of option.
582 * @param string $value value of option.
583 * @param string $option Option name.
584 * @since 1.0.0
585 */
586 public function update_analytics_option_callback( $old_value, $value, $option ) {
587 if ( is_multisite() ) {
588 $this->add_option_to_network( $option, $value );
589 }
590 }
591
592 /**
593 * Analytics option add callback.
594 *
595 * @param string $option Option name.
596 * @param string $value value of option.
597 * @since 1.0.0
598 */
599 public function add_analytics_option_callback( $option, $value ) {
600 if ( is_multisite() ) {
601 $this->add_option_to_network( $option, $value );
602 }
603 }
604
605 /**
606 * Send analytics track event if tracking is enabled.
607 *
608 * @since 1.0.0
609 */
610 public function maybe_track_analytics() {
611
612 if ( ! $this->is_tracking_enabled() ) {
613 return;
614 }
615
616 $analytics_track = get_site_transient( 'bsf_usage_track' );
617
618 // If the last data sent is 2 days old i.e. transient is expired.
619 if ( ! $analytics_track ) {
620 $this->send();
621 set_site_transient( 'bsf_usage_track', true, 2 * DAY_IN_SECONDS );
622 }
623 }
624
625 /**
626 * Save analytics option to network.
627 *
628 * @param string $option name of option.
629 * @param string $value value of option.
630 * @since 1.0.0
631 */
632 public function add_option_to_network( $option, $value ) {
633
634 // If action coming from general settings page.
635 if ( isset( $_POST['option_page'] ) && 'general' === sanitize_text_field( wp_unslash( $_POST['option_page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
636
637 if ( get_site_option( $option ) ) {
638 update_site_option( $option, $value );
639 } else {
640 add_site_option( $option, $value );
641 }
642 }
643 }
644
645 /**
646 * Function to load the deactivation survey form on the admin footer.
647 *
648 * This function checks if the Deactivation_Survey_Feedback class exists and if so, it loads the deactivation survey form.
649 * The form is configured with specific settings for plugin. Example: For CartFlows, including the source, logo, plugin slug, title, support URL, description, and the screen on which to show the form.
650 *
651 * @since 1.1.6
652 * @return void
653 */
654 public function load_deactivation_survey_form() {
655
656 if ( class_exists( 'Deactivation_Survey_Feedback' ) ) {
657 foreach ( $this->entities as $key => $data ) {
658 // If the deactivation_survey info in available then only add the form.
659 if ( ! empty( $data['deactivation_survey'] ) && is_array( $data['deactivation_survey'] ) ) {
660 foreach ( $data['deactivation_survey'] as $key => $survey_args ) {
661 Deactivation_Survey_Feedback::show_feedback_form(
662 $survey_args
663 );
664 }
665 }
666 }
667 }
668 }
669
670 /**
671 * Function to add plugin slugs to Deactivation Survey vars for JS operations.
672 *
673 * @param array $vars UDS vars array.
674 * @return array Modified UDS vars array with plugin slugs.
675 * @since 1.1.6
676 */
677 public function add_slugs_to_uds_vars( $vars ) {
678 foreach ( $this->entities as $key => $data ) {
679 if ( ! empty( $data['deactivation_survey'] ) && is_array( $data['deactivation_survey'] ) ) {
680 foreach ( $data['deactivation_survey'] as $key => $survey_args ) {
681 $vars['_plugin_slug'] = isset( $vars['_plugin_slug'] ) ? array_merge( $vars['_plugin_slug'], array( $survey_args['plugin_slug'] ) ) : array( $survey_args['plugin_slug'] );
682 }
683 }
684 }
685
686 return $vars;
687 }
688 }
689 }
690