PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.1
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / lib / bsf-analytics / class-bsf-analytics.php

class-bsf-analytics.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.1.1, at inc/lib/bsf-analytics/class-bsf-analytics.php

693 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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