PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.6.3
LatePoint – Calendar Booking Plugin for Appointments and Events v5.6.3
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 1 week 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 1 week ago version.json 1 week ago
class-bsf-analytics.php
693 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( 'bsf_admin_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 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
239 return;
240 }
241
242 if ( ! current_user_can( 'manage_options' ) ) {
243 return;
244 }
245
246 if( $this->is_tracking_enabled() ) {
247 return; // Don't need to display notice if any of our plugin already have the permission.
248 }
249
250 // If the user has opted out of tracking, don't show the notice till 7 days.
251 $last_displayed_time = get_site_option( 'bsf_usage_last_displayed_time', false );
252 if ( $last_displayed_time && $last_displayed_time > time() - ( 7 * DAY_IN_SECONDS ) ) {
253 return; // Don't display the notice if it was displayed recently.
254 }
255
256 foreach ( $this->entities as $key => $data ) {
257
258 $time_to_display = isset( $data['time_to_display'] ) ? $data['time_to_display'] : '+24 hours';
259 $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'notice' );
260
261 // Don't display the notice if tracking is disabled or White Label is enabled for any of our plugins.
262 if ( false !== get_site_option( $key . '_usage_optin', false ) || $this->is_white_label_enabled( $key ) ) {
263 continue;
264 }
265
266 // Show tracker consent notice after 24 hours from installed time.
267 if ( strtotime( $time_to_display, $this->get_analytics_install_time( $key ) ) > time() ) {
268 continue;
269 }
270
271 /* translators: %s product name */
272 $notice_string = sprintf(
273 __(
274 '<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.'
275 ),
276 esc_html( $data['product_name'] )
277 );
278
279 if ( is_multisite() ) {
280 $notice_string .= __( 'This will be applicable for all sites from the network.' );
281 }
282
283 $language_dir = is_rtl() ? 'rtl' : 'ltr';
284
285 BSF_Admin_Notices::add_notice(
286 array(
287 'id' => $key . '-optin-notice',
288 'type' => '',
289 'message' => sprintf(
290 '<div class="notice-content">
291 <div class="notice-heading">
292 %1$s
293 </div>
294 <div class="astra-notices-container">
295 <a href="%2$s" class="astra-notices button-primary">
296 %3$s
297 </a>
298 <a href="%4$s" data-repeat-notice-after="%5$s" class="astra-notices button-secondary">
299 %6$s
300 </a>
301 </div>
302 </div>',
303 /* translators: %s usage doc link */
304 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.' ) ),
305 esc_url(
306 add_query_arg(
307 array(
308 $key . '_analytics_optin' => 'yes',
309 $key . '_analytics_nonce' => wp_create_nonce( $key . '_analytics_optin' ),
310 'bsf_analytics_source' => $key,
311 )
312 )
313 ),
314 __( 'Happy to help!' ),
315 esc_url(
316 add_query_arg(
317 array(
318 $key . '_analytics_optin' => 'no',
319 $key . '_analytics_nonce' => wp_create_nonce( $key . '_analytics_optin' ),
320 'bsf_analytics_source' => $key,
321 )
322 )
323 ),
324 MONTH_IN_SECONDS,
325 __( 'Skip' )
326 ),
327 'show_if' => true,
328 'repeat-notice-after' => false,
329 'priority' => 18,
330 'display-with-other-notices' => true,
331 )
332 );
333
334 return;
335 }
336 }
337
338 /**
339 * Process usage tracking opt out.
340 *
341 * @since 1.0.0
342 */
343 public function handle_optin_optout() {
344
345 if ( ! current_user_can( 'manage_options' ) ) {
346 return;
347 }
348
349 // Verify nonce before accessing any $_GET data.
350 // The nonce key is dynamic per entity, so iterate to find a valid one.
351 $source = '';
352 foreach ( $this->entities as $key => $data ) {
353 $nonce_key = $key . '_analytics_nonce';
354 if ( isset( $_GET[ $nonce_key ] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ $nonce_key ] ) ), $key . '_analytics_optin' ) ) {
355 $source = $key;
356 break;
357 }
358 }
359
360 if ( empty( $source ) ) {
361 return;
362 }
363
364 $optin_status = isset( $_GET[ $source . '_analytics_optin' ] ) ? sanitize_text_field( wp_unslash( $_GET[ $source . '_analytics_optin' ] ) ) : '';
365
366 if ( 'yes' === $optin_status ) {
367 $this->optin( $source );
368 } elseif ( 'no' === $optin_status ) {
369 $this->optout( $source );
370 }
371
372 wp_safe_redirect(
373 esc_url_raw(
374 remove_query_arg(
375 array(
376 $source . '_analytics_optin',
377 $source . '_analytics_nonce',
378 'bsf_analytics_source',
379 )
380 )
381 )
382 );
383 exit;
384 }
385
386 /**
387 * Opt in to usage tracking.
388 *
389 * @param string $source source of analytics.
390 * @since 1.0.0
391 */
392 private function optin( $source ) {
393 update_site_option( $source . '_usage_optin', 'yes' );
394 }
395
396 /**
397 * Opt out of usage tracking.
398 *
399 * @param string $source source of analytics.
400 * @since 1.0.0
401 */
402 private function optout( $source ) {
403 update_site_option( $source . '_usage_optin', 'no' );
404 update_site_option( 'bsf_usage_last_displayed_time', time() );
405
406 // Clear tracking transient immediately so opt-out takes effect right away.
407 delete_site_transient( 'bsf_usage_track' );
408 }
409
410 /**
411 * Load analytics stat class.
412 *
413 * @since 1.0.0
414 */
415 private function includes() {
416 require_once __DIR__ . '/classes/class-bsf-analytics-helper.php';
417 require_once __DIR__ . '/class-bsf-analytics-stats.php';
418 require_once __DIR__ . '/class-bsf-analytics-events.php';
419
420 // Loads all the modules.
421 require_once __DIR__ . '/modules/deactivation-survey/classes/class-deactivation-survey-feedback.php';
422 require_once __DIR__ . '/modules/utm-analytics.php';
423 }
424
425 /**
426 * Migrate old "analytics" options to new "usage" naming.
427 * Copies values to new options and deletes old options.
428 *
429 * @since 1.1.17
430 */
431 private function maybe_migrate_options() {
432 if ( get_site_option( 'bsf_usage_migrated' ) ) {
433 return;
434 }
435
436 // Migrate global options.
437 $old_last_displayed = get_site_option( 'bsf_analytics_last_displayed_time' );
438 if ( false !== $old_last_displayed ) {
439 update_site_option( 'bsf_usage_last_displayed_time', $old_last_displayed );
440 delete_site_option( 'bsf_analytics_last_displayed_time' );
441 }
442
443 // Migrate per-product options.
444 foreach ( $this->entities as $key => $data ) {
445 $old_optin = get_site_option( $key . '_analytics_optin' );
446 if ( false !== $old_optin ) {
447 update_site_option( $key . '_usage_optin', $old_optin );
448 delete_site_option( $key . '_analytics_optin' );
449 }
450
451 $old_install_time = get_site_option( $key . '_analytics_installed_time' );
452 if ( false !== $old_install_time ) {
453 update_site_option( $key . '_usage_installed_time', $old_install_time );
454 delete_site_option( $key . '_analytics_installed_time' );
455 }
456 }
457
458 // Migrate transient.
459 $old_track = get_site_transient( 'bsf_analytics_track' );
460 if ( false !== $old_track ) {
461 set_site_transient( 'bsf_usage_track', $old_track, 2 * DAY_IN_SECONDS );
462 delete_site_transient( 'bsf_analytics_track' );
463 }
464
465 update_site_option( 'bsf_usage_migrated', true );
466 }
467
468 /**
469 * Register usage tracking option in General settings page.
470 *
471 * @since 1.0.0
472 */
473 public function register_usage_tracking_setting() {
474
475 foreach ( $this->entities as $key => $data ) {
476
477 if ( ! apply_filters( $key . '_tracking_enabled', true ) || $this->is_white_label_enabled( $key ) ) {
478 return;
479 }
480
481 /**
482 * Introducing a new key 'hide_optin_checkbox, which allows individual plugin to hide optin checkbox
483 * If they are providing providing in-plugin option to manage this option.
484 * from General > Settings page.
485 *
486 * @since 1.1.14
487 */
488 if( ! empty( $data['hide_optin_checkbox'] ) && true === $data['hide_optin_checkbox'] ) {
489 continue;
490 }
491
492 $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'settings' );
493 $author = isset( $data['author'] ) ? $data['author'] : 'Brainstorm Force';
494
495 register_setting(
496 'general', // Options group.
497 $key . '_usage_optin', // Option name/database.
498 array( 'sanitize_callback' => array( $this, 'sanitize_option' ) ) // sanitize callback function.
499 );
500
501 add_settings_field(
502 $key . '-usage-optin', // Field ID.
503 __( 'Usage Tracking' ), // Field title.
504 array( $this, 'render_settings_field_html' ), // Field callback function.
505 'general',
506 'default', // Settings page slug.
507 array(
508 'type' => 'checkbox',
509 'title' => $author,
510 'name' => $key . '_usage_optin',
511 'label_for' => $key . '-usage-optin',
512 'id' => $key . '-usage-optin',
513 'usage_doc_link' => $usage_doc_link,
514 )
515 );
516 }
517 }
518
519 /**
520 * Sanitize Callback Function
521 *
522 * @param bool $input Option value.
523 * @since 1.0.0
524 */
525 public function sanitize_option( $input ) {
526
527 if ( ! $input || 'no' === $input ) {
528 return 'no';
529 }
530
531 return 'yes';
532 }
533
534 /**
535 * Print settings field HTML.
536 *
537 * @param array $args arguments to field.
538 * @since 1.0.0
539 */
540 public function render_settings_field_html( $args ) {
541 $is_checked = ( 'yes' === get_site_option( $args['name'], false ) );
542 ?>
543 <fieldset>
544 <label for="<?php echo esc_attr( $args['label_for'] ); ?>">
545 <input id="<?php echo esc_attr( $args['id'] ); ?>" type="checkbox" value="1" name="<?php echo esc_attr( $args['name'] ); ?>" <?php checked( $is_checked ); ?>>
546 <?php
547 /* translators: %s Product title */
548 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
549
550 if ( is_multisite() ) {
551 esc_html_e( ' This will be applicable for all sites from the network.' );
552 }
553 ?>
554 </label>
555 <?php
556 echo wp_kses_post( sprintf( '<a href="%1s" target="_blank" rel="noreferrer noopener">%2s</a>', esc_url( $args['usage_doc_link'] ), __( 'Learn More.' ) ) );
557 ?>
558 </fieldset>
559 <?php
560 }
561
562 /**
563 * Get analytics installed time from option.
564 *
565 * @param string $source source of analytics.
566 * @return string $time analytics installed time.
567 * @since 1.0.0
568 */
569 private function get_analytics_install_time( $source ) {
570
571 $time = get_site_option( $source . '_usage_installed_time' );
572
573 if ( ! $time ) {
574 $time = time();
575 update_site_option( $source . '_usage_installed_time', $time );
576 }
577
578 return $time;
579 }
580
581 /**
582 * Schedule/unschedule cron event on updation of option.
583 *
584 * @param string $old_value old value of option.
585 * @param string $value value of option.
586 * @param string $option Option name.
587 * @since 1.0.0
588 */
589 public function update_analytics_option_callback( $old_value, $value, $option ) {
590 if ( is_multisite() ) {
591 $this->add_option_to_network( $option, $value );
592 }
593 }
594
595 /**
596 * Analytics option add callback.
597 *
598 * @param string $option Option name.
599 * @param string $value value of option.
600 * @since 1.0.0
601 */
602 public function add_analytics_option_callback( $option, $value ) {
603 if ( is_multisite() ) {
604 $this->add_option_to_network( $option, $value );
605 }
606 }
607
608 /**
609 * Send analytics track event if tracking is enabled.
610 *
611 * @since 1.0.0
612 */
613 public function maybe_track_analytics() {
614
615 if ( ! $this->is_tracking_enabled() ) {
616 return;
617 }
618
619 $analytics_track = get_site_transient( 'bsf_usage_track' );
620
621 // If the last data sent is 2 days old i.e. transient is expired.
622 if ( ! $analytics_track ) {
623 $this->send();
624 set_site_transient( 'bsf_usage_track', true, 2 * DAY_IN_SECONDS );
625 }
626 }
627
628 /**
629 * Save analytics option to network.
630 *
631 * @param string $option name of option.
632 * @param string $value value of option.
633 * @since 1.0.0
634 */
635 public function add_option_to_network( $option, $value ) {
636
637 // If action coming from general settings page.
638 if ( isset( $_POST['option_page'] ) && 'general' === sanitize_text_field( wp_unslash( $_POST['option_page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
639
640 if ( get_site_option( $option ) ) {
641 update_site_option( $option, $value );
642 } else {
643 add_site_option( $option, $value );
644 }
645 }
646 }
647
648 /**
649 * Function to load the deactivation survey form on the admin footer.
650 *
651 * This function checks if the Deactivation_Survey_Feedback class exists and if so, it loads the deactivation survey form.
652 * 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.
653 *
654 * @since 1.1.6
655 * @return void
656 */
657 public function load_deactivation_survey_form() {
658
659 if ( class_exists( 'Deactivation_Survey_Feedback' ) ) {
660 foreach ( $this->entities as $key => $data ) {
661 // If the deactivation_survey info in available then only add the form.
662 if ( ! empty( $data['deactivation_survey'] ) && is_array( $data['deactivation_survey'] ) ) {
663 foreach ( $data['deactivation_survey'] as $key => $survey_args ) {
664 Deactivation_Survey_Feedback::show_feedback_form(
665 $survey_args
666 );
667 }
668 }
669 }
670 }
671 }
672
673 /**
674 * Function to add plugin slugs to Deactivation Survey vars for JS operations.
675 *
676 * @param array $vars UDS vars array.
677 * @return array Modified UDS vars array with plugin slugs.
678 * @since 1.1.6
679 */
680 public function add_slugs_to_uds_vars( $vars ) {
681 foreach ( $this->entities as $key => $data ) {
682 if ( ! empty( $data['deactivation_survey'] ) && is_array( $data['deactivation_survey'] ) ) {
683 foreach ( $data['deactivation_survey'] as $key => $survey_args ) {
684 $vars['_plugin_slug'] = isset( $vars['_plugin_slug'] ) ? array_merge( $vars['_plugin_slug'], array( $survey_args['plugin_slug'] ) ) : array( $survey_args['plugin_slug'] );
685 }
686 }
687 }
688
689 return $vars;
690 }
691 }
692 }
693