PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.7.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.7.0
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / admin / analytics.php

analytics.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.7.0, at admin/analytics.php

979 lines 29.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Analytics class helps to connect BSFAnalytics.
4 *
5 * @package sureforms.
6 */
7
8 namespace SRFM\Admin;
9
10 use SRFM\Inc\Database\Tables\Entries;
11 use SRFM\Inc\Helper;
12 use SRFM\Inc\Traits\Get_Instance;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17 /**
18 * Analytics class.
19 *
20 * @since 1.4.0
21 */
22 class Analytics {
23 use Get_Instance;
24
25 /**
26 * BSF_Analytics_Events instance for one-time event tracking.
27 *
28 * @var \BSF_Analytics_Events|null
29 */
30 private static $events = null;
31
32 /**
33 * Class constructor.
34 *
35 * @return void
36 * @since 1.4.0
37 */
38 public function __construct() {
39 /*
40 * BSF Analytics.
41 */
42 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
43 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
44 }
45
46 if ( ! class_exists( 'Astra_Notices' ) ) {
47 require_once SRFM_DIR . 'inc/lib/astra-notices/class-astra-notices.php';
48 }
49
50 add_filter(
51 'uds_survey_allowed_screens',
52 static function () {
53 return [ 'plugins' ];
54 }
55 );
56
57 $srfm_bsf_analytics = \BSF_Analytics_Loader::get_instance();
58
59 $srfm_bsf_analytics->set_entity(
60 [
61 'sureforms' => [
62 'product_name' => 'SureForms',
63 'path' => SRFM_DIR . 'inc/lib/bsf-analytics',
64 'author' => 'SureForms',
65 'time_to_display' => '+24 hours',
66 'deactivation_survey' => apply_filters(
67 'srfm_deactivation_survey_data',
68 [
69 [
70 'id' => 'deactivation-survey-sureforms',
71 'popup_logo' => SRFM_URL . 'admin/assets/sureforms-logo.png',
72 'plugin_slug' => 'sureforms',
73 'popup_title' => 'Quick Feedback',
74 'support_url' => 'https://sureforms.com/contact/',
75 'popup_description' => 'If you have a moment, please share why you are deactivating SureForms:',
76 'show_on_screens' => [ 'plugins' ],
77 'plugin_version' => SRFM_VER,
78 ],
79 ]
80 ),
81 'hide_optin_checkbox' => true,
82 ],
83 ]
84 );
85
86 add_filter( 'bsf_core_stats', [ $this, 'add_srfm_analytics_data' ] );
87
88 // Event tracking hooks.
89 add_action( 'current_screen', [ $this, 'track_first_editor_open' ] );
90 add_action( 'transition_post_status', [ $this, 'track_first_form_published' ], 10, 3 );
91 add_action( 'save_post', [ $this, 'track_embed_styling_configured' ], 10, 2 );
92
93 // Detect state-based events on admin load (dedup prevents repeat tracking).
94 $this->detect_state_events();
95 }
96
97 /**
98 * Get the shared BSF_Analytics_Events instance.
99 *
100 * Uses SureForms' Helper option methods so data stays in the
101 * existing srfm_options row — zero migration required.
102 *
103 * @since 2.7.0
104 * @return \BSF_Analytics_Events
105 */
106 public static function events() {
107 if ( null === self::$events ) {
108 if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
109 require_once SRFM_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-events.php';
110 }
111
112 self::$events = new \BSF_Analytics_Events(
113 'sureforms',
114 [
115 'get' => [ Helper::class, 'get_srfm_option' ],
116 'update' => [ Helper::class, 'update_srfm_option' ],
117 ]
118 );
119 }
120 return self::$events;
121 }
122
123 /**
124 * Callback function to add SureForms specific analytics data.
125 *
126 * @param array $stats_data existing stats_data.
127 * @since 1.4.0
128 * @return array
129 */
130 public function add_srfm_analytics_data( $stats_data ) {
131 $stats_data['plugin_data']['sureforms'] = [
132 'free_version' => SRFM_VER,
133 'site_language' => get_locale(),
134 'most_used_anti_spam' => $this->most_used_anti_spam(),
135 'user_status' => $this->user_status(),
136 'pointer_popup_clicked' => $this->pointer_popup_clicked(),
137 ];
138 $stats_data['plugin_data']['sureforms']['numeric_values'] = [
139 'total_forms' => wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0,
140 'instant_forms_enabled' => $this->instant_forms_enabled(),
141 'forms_using_custom_css' => $this->forms_using_custom_css(),
142 'ai_generated_forms' => $this->ai_generated_forms(),
143 'ai_generated_payment_forms' => $this->ai_generated_forms( 'payments' ),
144 'payment_forms' => $this->get_payment_forms_count(),
145 'total_entries' => Entries::get_total_entries_by_status(),
146 'restricted_forms' => $this->get_restricted_forms(),
147 'embed_styling_gb_default' => self::embed_styling_gutenberg_count( 'default' ),
148 'embed_styling_el_default' => self::embed_styling_elementor_count( 'default' ),
149 'embed_styling_br_default' => self::embed_styling_bricks_count( 'default' ),
150 ];
151
152 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->global_settings_data() );
153 // Add onboarding analytics data.
154 $stats_data['plugin_data']['sureforms'] = array_merge_recursive( $stats_data['plugin_data']['sureforms'], $this->onboarding_analytics_data() );
155
156 // Add KPI tracking data.
157 $kpi_data = $this->get_kpi_tracking_data();
158 if ( ! empty( $kpi_data ) ) {
159 $stats_data['plugin_data']['sureforms']['kpi_records'] = $kpi_data;
160 }
161
162 // Flush pending events into payload (only if any exist).
163 $pending_events = self::events()->flush_pending();
164 if ( ! empty( $pending_events ) ) {
165 $stats_data['plugin_data']['sureforms']['events_record'] = $pending_events;
166 }
167
168 return $stats_data;
169 }
170
171 /**
172 * Return total number of forms using instant forms.
173 *
174 * @since 1.4.0
175 * @return int
176 */
177 public function instant_forms_enabled() {
178 $meta_query = [
179 [
180 'key' => '_srfm_instant_form_settings',
181 'value' => '"enable_instant_form";b:1;',
182 'compare' => 'LIKE',
183 ],
184 ];
185
186 return $this->custom_wp_query_total_posts( $meta_query );
187 }
188
189 /**
190 * Return total number of ai generated forms.
191 *
192 * @param string $form_type Form type to check.
193 *
194 * @since 1.4.0
195 * @return int
196 */
197 public function ai_generated_forms( $form_type = '' ) {
198 $form_type = empty( $form_type ) || ! is_string( $form_type ) ? '' : $form_type;
199 $meta_query = [
200 [
201 'key' => '_srfm_is_ai_generated',
202 'value' => '',
203 'compare' => '!=', // Checks if the value is NOT empty.
204 ],
205 ];
206
207 if ( 'payments' === $form_type ) {
208 $search = 'wp:srfm/payment';
209 return $this->custom_wp_query_total_posts_with_search( $meta_query, $search );
210 }
211
212 return $this->custom_wp_query_total_posts( $meta_query );
213 }
214
215 /**
216 * Return most used anti-spam type on this site.
217 *
218 * @since 1.4.4
219 * @return int
220 */
221 public function most_used_anti_spam() {
222 global $wpdb;
223
224 // Attempt to get from cache first.
225 $cache_key = 'most_used_anti_spam';
226 $cached_result = wp_cache_get( $cache_key, 'sureforms' );
227
228 if ( false !== $cached_result ) {
229 return $cached_result;
230 }
231
232 $meta_key = '_srfm_captcha_security_type';
233
234 // Query to get the most used captcha type.
235 // PHPCS: Ignore direct database query warning, as there is no built-in alternative.
236 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
237 $result = $wpdb->get_row(
238 $wpdb->prepare(
239 "
240 SELECT meta_value, COUNT(meta_value) as count
241 FROM {$wpdb->postmeta}
242 WHERE meta_key = %s
243 AND meta_value != ''
244 GROUP BY meta_value
245 ORDER BY count DESC
246 LIMIT 1
247 ",
248 $meta_key
249 ),
250 ARRAY_A
251 );
252
253 $output = '';
254 if ( $result && ! empty( $result['meta_value'] ) ) {
255 switch ( $result['meta_value'] ) {
256 case 'g-recaptcha':
257 $output = 'Google reCAPTCHA';
258 break;
259
260 case 'cf-turnstile':
261 $output = 'CloudFlare Turnstile';
262 break;
263
264 case 'hcaptcha':
265 $output = 'hCaptcha';
266 break;
267
268 default:
269 $output = '';
270 break;
271 }
272 }
273
274 // Store result in cache for 1 hour.
275 wp_cache_set( $cache_key, $output, 'sureforms', HOUR_IN_SECONDS );
276
277 return $output;
278 }
279
280 /**
281 * Returns total number of forms using custom css.
282 *
283 * @since 1.4.0
284 * @return int
285 */
286 public function forms_using_custom_css() {
287 $meta_query = [
288 [
289 'key' => '_srfm_form_custom_css',
290 'value' => '',
291 'compare' => '!=', // Checks if the value is NOT empty.
292 ],
293 ];
294
295 return $this->custom_wp_query_total_posts( $meta_query );
296 }
297
298 /**
299 * Count Gutenberg srfm/form embed blocks using a specific formTheme.
300 *
301 * When formTheme is 'inherit' (the block.json default), WordPress does not
302 * serialize it in the block comment. So only 'default' and 'custom' appear.
303 *
304 * Counts individual embed blocks (not pages), since a single page can
305 * contain multiple form embeds each with different styling.
306 *
307 * @param string $theme Theme slug to count ('default' or 'custom').
308 * @since 2.7.0
309 * @return int
310 */
311 public static function embed_styling_gutenberg_count( $theme ) {
312 global $wpdb;
313
314 $cache_key = 'embed_styling_gb_' . $theme;
315 $cached_result = wp_cache_get( $cache_key, 'sureforms' );
316
317 if ( false !== $cached_result ) {
318 return (int) $cached_result;
319 }
320
321 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for cross-post-type content search with multiple LIKE conditions.
322 $posts = $wpdb->get_col(
323 $wpdb->prepare(
324 "SELECT post_content FROM {$wpdb->posts}
325 WHERE post_status = 'publish'
326 AND post_content LIKE %s
327 AND post_content LIKE %s",
328 '%' . $wpdb->esc_like( 'wp:srfm/form' ) . '%',
329 '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
330 )
331 );
332
333 $count = 0;
334 $escaped_theme = preg_quote( $theme, '/' );
335 foreach ( $posts as $content ) {
336 $count += preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $content );
337 }
338
339 wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
340
341 return $count;
342 }
343
344 /**
345 * Count Elementor sureforms_form widgets using a specific formTheme.
346 *
347 * Searches _elementor_data post meta for sureforms_form widgets with
348 * a non-inherit formTheme. Counts individual widgets (not pages).
349 *
350 * @param string $theme Theme slug to count ('default' or 'custom').
351 * @since 2.7.0
352 * @return int
353 */
354 public static function embed_styling_elementor_count( $theme ) {
355 global $wpdb;
356
357 $cache_key = 'embed_styling_el_' . $theme;
358 $cached_result = wp_cache_get( $cache_key, 'sureforms' );
359
360 if ( false !== $cached_result ) {
361 return (int) $cached_result;
362 }
363
364 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
365 $meta_values = $wpdb->get_col(
366 $wpdb->prepare(
367 "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
368 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
369 WHERE p.post_status = 'publish'
370 AND pm.meta_key = '_elementor_data'
371 AND pm.meta_value LIKE %s
372 AND pm.meta_value LIKE %s",
373 '%' . $wpdb->esc_like( 'sureforms_form' ) . '%',
374 '%' . $wpdb->esc_like( '"formTheme":"' . $theme . '"' ) . '%'
375 )
376 );
377
378 $count = 0;
379 $escaped_theme = preg_quote( $theme, '/' );
380 foreach ( $meta_values as $json ) {
381 // Count widget instances with the specific formTheme in the JSON.
382 $count += preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"' . $escaped_theme . '"/', $json );
383 }
384
385 wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
386
387 return $count;
388 }
389
390 /**
391 * Count Bricks sureforms elements using a specific formTheme.
392 *
393 * Searches _bricks_page_content_2 post meta (serialized PHP) for
394 * sureforms elements with a non-inherit formTheme. Counts individual elements.
395 *
396 * @param string $theme Theme slug to count ('default' or 'custom').
397 * @since 2.7.0
398 * @return int
399 */
400 public static function embed_styling_bricks_count( $theme ) {
401 global $wpdb;
402
403 $cache_key = 'embed_styling_br_' . $theme;
404 $cached_result = wp_cache_get( $cache_key, 'sureforms' );
405
406 if ( false !== $cached_result ) {
407 return (int) $cached_result;
408 }
409
410 // Bricks stores element data as serialized PHP in _bricks_page_content_2.
411 // In serialized format: s:9:"formTheme";s:7:"default" (for 'default' theme).
412 $serialized_theme = sprintf( '"formTheme";s:%d:"%s"', strlen( $theme ), $theme );
413
414 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- No WP_Query alternative for post meta content search with multiple LIKE conditions.
415 $meta_values = $wpdb->get_col(
416 $wpdb->prepare(
417 "SELECT pm.meta_value FROM {$wpdb->postmeta} pm
418 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
419 WHERE p.post_status = 'publish'
420 AND pm.meta_key = '_bricks_page_content_2'
421 AND pm.meta_value LIKE %s
422 AND pm.meta_value LIKE %s",
423 '%' . $wpdb->esc_like( '"sureforms"' ) . '%',
424 '%' . $wpdb->esc_like( $serialized_theme ) . '%'
425 )
426 );
427
428 $count = 0;
429 $escaped_serial_theme = preg_quote( $serialized_theme, '/' );
430 foreach ( $meta_values as $data ) {
431 $count += preg_match_all( '/' . $escaped_serial_theme . '/', $data );
432 }
433
434 wp_cache_set( $cache_key, $count, 'sureforms', HOUR_IN_SECONDS );
435
436 return $count;
437 }
438
439 /**
440 * Return total number of restricted forms.
441 *
442 * @since 1.10.1
443 * @return int
444 */
445 public function get_restricted_forms() {
446 $meta_query = [
447 [
448 'key' => '_srfm_form_restriction',
449 'value' => '"status":true',
450 'compare' => 'LIKE',
451 ],
452 ];
453
454 return $this->custom_wp_query_total_posts( $meta_query );
455 }
456
457 /**
458 * Generates global setting data for analytics
459 *
460 * @since 1.4.0
461 * @return array
462 */
463 public function global_settings_data() {
464 $global_data = [];
465
466 $security_settings = get_option( 'srfm_security_settings_options', [] );
467 $global_data['boolean_values']['honeypot_enabled'] = isset( $security_settings['srfm_honeypot'] ) && true === $security_settings['srfm_honeypot'];
468
469 $email_summary_data = get_option( 'srfm_email_summary_settings_options', [] );
470 $global_data['boolean_values']['email_summary_enabled'] = isset( $email_summary_data['srfm_email_summary'] ) && true === $email_summary_data['srfm_email_summary'];
471
472 $global_data['boolean_values']['suretriggers_active'] = is_plugin_active( 'suretriggers/suretriggers.php' );
473
474 $bsf_internal_referrer = get_option( 'bsf_product_referers', [] );
475 if ( ! empty( $bsf_internal_referrer['sureforms'] ) ) {
476 $global_data['internal_referer'] = $bsf_internal_referrer['sureforms'];
477 } else {
478 $global_data['internal_referer'] = '';
479 }
480
481 $general_settings = get_option( 'srfm_general_settings_options', [] );
482 $global_data['boolean_values']['ip_logging_enabled'] = ! empty( $general_settings['srfm_ip_log'] );
483
484 $validation_messages = get_option( 'srfm_default_dynamic_block_option', [] );
485 $global_data['boolean_values']['custom_validation_message'] = ! empty( $validation_messages ) && is_array( $validation_messages );
486
487 // Payment analytics - check if any payment method is enabled.
488 $global_data['boolean_values']['stripe_enabled'] = $this->is_stripe_enabled();
489
490 return $global_data;
491 }
492
493 /**
494 * Generates onboarding analytics data
495 *
496 * @since 1.9.1
497 * @return array
498 */
499 public function onboarding_analytics_data() {
500 $onboarding_data = [];
501 $analytics_option = Helper::get_srfm_option( 'onboarding_analytics', [] );
502
503 if ( empty( $analytics_option ) ) {
504 return $onboarding_data;
505 }
506
507 // Process skipped steps - store as an array.
508 if ( ! empty( $analytics_option['skippedSteps'] ) && is_array( $analytics_option['skippedSteps'] ) ) {
509 // Map step keys to more descriptive names.
510 $step_mapping = [
511 'welcome' => 'Welcome',
512 'connect' => 'Connect',
513 'emailDelivery' => 'SureMail',
514 'premiumFeatures' => 'Features',
515 'done' => 'Done',
516 ];
517
518 // Transform the step keys to their descriptive names.
519 $mapped_steps = array_map(
520 static function( $step ) use ( $step_mapping ) {
521 return $step_mapping[ $step ] ?? $step;
522 },
523 $analytics_option['skippedSteps']
524 );
525
526 // Store as an array.
527 $onboarding_data['onboarding_skipped_steps'] = $mapped_steps;
528 }
529
530 // SureMail Installation Status.
531 if ( isset( $analytics_option['suremailInstalled'] ) ) {
532 $onboarding_data['boolean_values']['onboarding_suremail_installed'] = (bool) $analytics_option['suremailInstalled'];
533 }
534
535 // Account Connection Status.
536 if ( isset( $analytics_option['accountConnected'] ) ) {
537 $onboarding_data['boolean_values']['onboarding_account_connected'] = (bool) $analytics_option['accountConnected'];
538 }
539
540 // Onboarding Completion Status.
541 if ( isset( $analytics_option['completed'] ) ) {
542 $onboarding_data['boolean_values']['onboarding_completed'] = (bool) $analytics_option['completed'];
543 }
544
545 // Onboarding Early Exit Status.
546 if ( isset( $analytics_option['exitedEarly'] ) ) {
547 $onboarding_data['boolean_values']['onboarding_exited_early'] = (bool) $analytics_option['exitedEarly'];
548 }
549
550 // Onboarding Selected Premium Features.
551 if ( ! empty( $analytics_option['premiumFeatures'] ) && ! empty( $analytics_option['premiumFeatures']['selectedFeatures'] ) ) {
552 // Map feature IDs to more descriptive names - exclude free features.
553 $feature_mapping = [
554 // Starter features.
555 'multi_step_form' => 'Multi-step Forms',
556 'conditional_logic' => 'Conditional Fields',
557 'webhooks' => 'Webhooks',
558 'advanced_fields' => 'Advanced Fields',
559
560 // Pro features.
561 'conversational_forms' => 'Conversational Forms',
562 'digital_signatures' => 'Digital Signatures',
563
564 // Business features.
565 'calculations' => 'Calculators',
566 'user_registration' => 'User Registration and Login',
567 'custom_app' => 'Custom App',
568 'pdf_generation' => 'PDF Generation',
569 ];
570
571 // Filter out any free features that might have been included.
572 $premium_features = array_filter(
573 $analytics_option['premiumFeatures']['selectedFeatures'],
574 static function( $feature ) {
575 // Exclude free features (ai-form-generation and entries).
576 return 'ai-form-generation' !== $feature && 'entries' !== $feature;
577 }
578 );
579
580 // Transform the feature IDs to their descriptive names.
581 $mapped_features = array_map(
582 static function( $feature ) use ( $feature_mapping ) {
583 return $feature_mapping[ $feature ] ?? $feature;
584 },
585 $premium_features
586 );
587
588 // Store as an array.
589 $onboarding_data['onboarding_selected_premium_features'] = $mapped_features;
590 }
591
592 return $onboarding_data;
593 }
594
595 /**
596 * Returns user status.
597 *
598 * @since 1.8.0
599 * @return string
600 */
601 public function user_status() {
602 // First, check if user_active is already set in srfm_options.
603 if ( Helper::get_srfm_option( 'user_active', false ) ) {
604 return 'active';
605 }
606 // Get up to 10 published SureForms.
607 $forms = get_posts(
608 [
609 'post_type' => SRFM_FORMS_POST_TYPE,
610 'posts_per_page' => 10,
611 'post_status' => 'publish',
612 ]
613 );
614 if ( empty( $forms ) ) {
615 return 'inactive';
616 }
617 foreach ( $forms as $form ) {
618 if ( ! get_post_meta( $form->ID, '_astra_sites_imported_post', true ) ) {
619 // Mark user as active in srfm_options.
620 Helper::update_srfm_option( 'user_active', true );
621 return 'active';
622 }
623 }
624 return 'inactive';
625 }
626
627 /**
628 * Return pointer popup clicked status.
629 *
630 * @return string pointer click status.
631 * @since 1.8.0
632 */
633 public function pointer_popup_clicked() {
634 // Get both values from srfm_options.
635 $accepted = Helper::get_srfm_option( 'pointer_popup_accepted', false );
636 $dismissed = Helper::get_srfm_option( 'pointer_popup_dismissed', false );
637 // If neither action has occurred.
638 if ( ! $accepted && ! $dismissed ) {
639 return '';
640 }
641
642 // If both are set, return the most recent one.
643 if ( $accepted && $dismissed ) {
644 return $accepted > $dismissed ? 'accepted' : 'dismissed';
645 }
646
647 // If only one is set, return it.
648 return $accepted ? 'accepted' : 'dismissed';
649 }
650
651 /**
652 * Runs a custom WP_Query to fetch the total number of posts matching the given meta query and optional search string.
653 *
654 * This function is used to count SureForms posts based on specific meta query conditions.
655 * Optionally, a search string can be included to further filter results by keyword match.
656 *
657 * @since 2.0.0
658 *
659 * @param array $meta_query Meta query array for WP_Query.
660 * @param string $search Optional. Search string for WP_Query. Default empty.
661 * @return int The number of matching posts.
662 */
663 public function custom_wp_query_total_posts_with_search( $meta_query = [], $search = '' ) {
664 $args = [
665 'post_type' => SRFM_FORMS_POST_TYPE,
666 'post_status' => 'publish',
667 'posts_per_page' => -1,
668 ];
669
670 if ( ! empty( $meta_query ) && is_array( $meta_query ) ) {
671 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
672 $args['meta_query'] = $meta_query;
673 }
674
675 // If search string is provided, add it to the query.
676 if ( ! empty( $search ) ) {
677 $args['s'] = sanitize_text_field( $search );
678 }
679
680 $query = new \WP_Query( $args );
681 $posts_count = $query->found_posts;
682
683 wp_reset_postdata();
684
685 return $posts_count;
686 }
687
688 /**
689 * Get the total number of forms that utilize payment blocks.
690 *
691 * This function searches for forms containing the payment block identifier
692 * ('wp:srfm/payment') to determine how many forms include payment capabilities.
693 *
694 * @since 2.0.0
695 * @return int The number of forms that contain payment blocks.
696 */
697 public function get_payment_forms_count() {
698 $search = 'wp:srfm/payment';
699 // Runs a custom WP_Query to find the count of forms with payment block.
700 return $this->custom_wp_query_total_posts_with_search( [], $search );
701 }
702
703 /**
704 * Track first time a user opens the form editor.
705 *
706 * @since 2.5.1
707 * @return void
708 */
709 public function track_first_editor_open() {
710 $screen = get_current_screen();
711 if ( $screen && 'sureforms_form' === $screen->id ) {
712 self::events()->track( 'first_form_editor_opened' );
713 }
714 }
715
716 /**
717 * Track first time a form is published.
718 *
719 * @param string $new_status New post status.
720 * @param string $old_status Old post status.
721 * @param \WP_Post $post Post object.
722 * @since 2.5.1
723 * @return void
724 */
725 public function track_first_form_published( $new_status, $old_status, $post ) {
726 if ( 'publish' !== $new_status || 'publish' === $old_status || SRFM_FORMS_POST_TYPE !== $post->post_type ) {
727 return;
728 }
729
730 $is_ai = ! empty( get_post_meta( $post->ID, '_srfm_is_ai_generated', true ) );
731 $block_count = substr_count( $post->post_content, '<!-- wp:srfm/' );
732
733 self::events()->track(
734 'first_form_published',
735 (string) $post->ID,
736 [
737 'is_ai_generated' => $is_ai,
738 'block_count' => $block_count,
739 ]
740 );
741 }
742
743 /**
744 * Track embed styling configuration when a post/page is saved.
745 *
746 * Detects custom formTheme in Gutenberg blocks (post_content),
747 * Elementor widgets (_elementor_data meta), and Bricks elements
748 * (_bricks_page_content_2 meta). Re-tracks on each save by flushing
749 * the dedup flag.
750 *
751 * @param int $post_id Post ID.
752 * @param \WP_Post $post Post object.
753 * @since 2.7.0
754 * @return void
755 */
756 public function track_embed_styling_configured( $post_id, $post ) {
757 if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
758 return;
759 }
760
761 if ( 'publish' !== $post->post_status ) {
762 return;
763 }
764
765 $themes = [];
766 $source = '';
767
768 // Check Gutenberg blocks in post_content.
769 if ( preg_match_all( '/<!--\s*wp:srfm\/form\s+\{[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $post->post_content, $matches ) ) {
770 $themes = array_count_values( $matches[1] );
771 $source = 'gutenberg';
772 }
773
774 // Check Elementor widgets in _elementor_data meta.
775 if ( empty( $themes ) ) {
776 $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
777 if ( is_string( $elementor_data )
778 && preg_match_all( '/"widgetType"\s*:\s*"sureforms_form"[^}]*"formTheme"\s*:\s*"(?!inherit")([^"]*)"/', $elementor_data, $el_matches )
779 ) {
780 $themes = array_count_values( $el_matches[1] );
781 $source = 'elementor';
782 }
783 }
784
785 // Check Bricks elements in _bricks_page_content_2 meta (serialized PHP).
786 if ( empty( $themes ) ) {
787 $bricks_data = get_post_meta( $post_id, '_bricks_page_content_2', true );
788 if ( is_array( $bricks_data ) ) {
789 $bricks_themes = self::extract_bricks_form_themes( $bricks_data );
790 if ( ! empty( $bricks_themes ) ) {
791 $themes = array_count_values( $bricks_themes );
792 $source = 'bricks';
793 }
794 }
795 }
796
797 if ( empty( $themes ) ) {
798 return;
799 }
800
801 // Flush dedup so event is re-tracked on each meaningful save.
802 self::events()->flush_pushed( [ 'embed_styling_configured' ] );
803
804 self::events()->track(
805 'embed_styling_configured',
806 (string) $post_id,
807 [
808 'themes' => $themes,
809 'block_count' => array_sum( $themes ),
810 'source' => $source,
811 ]
812 );
813 }
814
815 /**
816 * Extract non-inherit formTheme values from Bricks element data.
817 *
818 * Recursively walks the unserialized Bricks elements array looking for
819 * sureforms elements with custom formTheme settings.
820 *
821 * @param array<mixed> $elements Bricks elements array.
822 * @return array<string> List of formTheme values (non-inherit).
823 * @since 2.7.0
824 */
825 private static function extract_bricks_form_themes( $elements ) {
826 $themes = [];
827
828 foreach ( $elements as $element ) {
829 if ( ! is_array( $element ) ) {
830 continue;
831 }
832
833 $name = $element['name'] ?? '';
834 $settings = $element['settings'] ?? [];
835 $form_theme = $settings['formTheme'] ?? '';
836
837 if ( 'sureforms' === $name && ! empty( $form_theme ) && 'inherit' !== $form_theme ) {
838 $themes[] = sanitize_text_field( $form_theme );
839 }
840
841 // Recurse into nested children.
842 if ( ! empty( $element['children'] ) && is_array( $element['children'] ) ) {
843 $themes = array_merge( $themes, self::extract_bricks_form_themes( $element['children'] ) );
844 }
845 }
846
847 return $themes;
848 }
849
850 /**
851 * Check if any payment method is enabled.
852 *
853 * This function checks if any payment gateway is connected and enabled.
854 * Currently supports Stripe, but can be extended for other payment methods in the future.
855 *
856 * @since 2.0.0
857 * @return bool True if any payment method is enabled, false otherwise.
858 */
859 private function is_stripe_enabled() {
860 // Check if Stripe is connected.
861 return class_exists( 'SRFM\Inc\Payments\Stripe\Stripe_Helper' ) && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected();
862 }
863
864 /**
865 * Runs custom WP_Query to fetch data as per requirement
866 *
867 * @param array $meta_query meta query array for WP_Query.
868 * @since 1.4.0
869 * @return int
870 */
871 private function custom_wp_query_total_posts( $meta_query ) {
872
873 $args = [
874 'post_type' => SRFM_FORMS_POST_TYPE,
875 'post_status' => 'publish',
876 'posts_per_page' => -1,
877 'meta_query' => $meta_query, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query required as we need to fetch count of nested data.
878 ];
879
880 $query = new \WP_Query( $args );
881 $posts_count = $query->found_posts;
882
883 wp_reset_postdata();
884
885 return $posts_count;
886 }
887
888 /**
889 * Get KPI tracking data for the last 2 days (excluding today).
890 *
891 * @since 2.4.0
892 * @return array KPI data organized by date.
893 */
894 private function get_kpi_tracking_data() {
895 $kpi_data = [];
896 $today = current_time( 'Y-m-d' );
897
898 // Get data for yesterday and day before yesterday.
899 for ( $i = 1; $i <= 2; $i++ ) {
900 $date = gmdate( 'Y-m-d', strtotime( $today . ' -' . $i . ' days' ) );
901
902 $kpi_data[ $date ] = [
903 'numeric_values' => [
904 'submissions' => $this->get_daily_submissions_count( $date ),
905 ],
906 ];
907 }
908
909 return $kpi_data;
910 }
911
912 /**
913 * Get daily submissions count for a specific date.
914 *
915 * @param string $date Date in Y-m-d format.
916 * @since 2.4.0
917 * @return int Daily submissions count.
918 */
919 private function get_daily_submissions_count( $date ) {
920 $start_date = $date . ' 00:00:00';
921 $end_date = $date . ' 23:59:59';
922
923 $where_conditions = [
924 [
925 [
926 'key' => 'created_at',
927 'compare' => '>=',
928 'value' => $start_date,
929 ],
930 [
931 'key' => 'created_at',
932 'compare' => '<=',
933 'value' => $end_date,
934 ],
935 ],
936 ];
937
938 return Entries::get_instance()->get_total_count( $where_conditions );
939 }
940
941 /**
942 * Detect state-based events that can't use direct hooks.
943 * Uses dedup in self::events()->track() — safe to call repeatedly.
944 *
945 * @since 2.5.1
946 * @return void
947 */
948 private function detect_state_events() {
949 // plugin_activated: dedup in self::events()->track() ensures this fires only once.
950 $bsf_referrers = get_option( 'bsf_product_referers', [] );
951 $source = ! empty( $bsf_referrers['sureforms'] ) ? $bsf_referrers['sureforms'] : 'self';
952 self::events()->track( 'plugin_activated', SRFM_VER, [ 'source' => $source ] );
953
954 // onboarding_completed: detect completed state.
955 if ( \SRFM\Inc\Onboarding::get_instance()->get_onboarding_status() ) {
956 self::events()->track( 'onboarding_completed' );
957 }
958
959 // stripe_connected: detect connection state.
960 if ( class_exists( '\SRFM\Inc\Payments\Stripe\Stripe_Helper' )
961 && \SRFM\Inc\Payments\Stripe\Stripe_Helper::is_stripe_connected() ) {
962 $mode = \SRFM\Inc\Payments\Stripe\Stripe_Helper::get_stripe_mode();
963 self::events()->track( 'stripe_connected', ! empty( $mode ) ? $mode : 'live' );
964 }
965
966 // MCP / Abilities API first-enable events.
967 $mcp_settings = get_option( 'srfm_mcp_settings_options', [] );
968
969 if ( ! empty( $mcp_settings['srfm_abilities_api'] ) ) {
970 self::events()->track( 'abilities_api_enabled' );
971 }
972
973 if ( ! empty( $mcp_settings['srfm_mcp_server'] ) ) {
974 self::events()->track( 'mcp_server_enabled' );
975 }
976 }
977
978 }
979