PluginProbe
FunnelKit – Funnel Builder for WooCommerce Checkout / trunk
FunnelKit – Funnel Builder for WooCommerce Checkout vtrunk
3.16.0.5 3.16.0.4 3.16.0.3 3.16.0.2 3.16.0 3.16.0.1 3.15.0.9 3.15.0.8 3.0.0.1 3.0.1.1 3.0.2.1 3.1.0.1 3.1.1.1 3.1.2.1 3.10.0.1 3.10.1.1 3.10.2.1 3.11.0.0.1 3.11.0.1.1 3.11.0.2.1 3.11.1.1 3.12.0.0.1 3.12.0.1.1 3.13.0.0.1 3.13.1.0.1 All 219 releases
funnel-builder / admin / class-wffn-admin.php

class-wffn-admin.php in FunnelKit – Funnel Builder for WooCommerce Checkout trunk, at admin/class-wffn-admin.php

3,539 lines 120.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined( 'ABSPATH' ) || exit; // Exit if accessed directly
3
4 /**
5 * Class to initiate admin functionalists
6 * Class WFFN_Admin
7 */
8 if ( ! class_exists( 'WFFN_Admin' ) ) {
9 #[AllowDynamicProperties]
10 class WFFN_Admin {
11
12 private static $ins = null;
13 private $funnel = null;
14 public $fk_memory_limit = 256 * 1024 * 1024;
15 private $step_against_fid = array();
16 private $step_count_against_fid = array();
17
18 /**
19 * @var WFFN_Background_Importer $updater
20 */
21 public $wffn_updater;
22
23 /**
24 * WFFN_Admin constructor.
25 */
26 public function __construct() {
27
28 /** Admin enqueue scripts*/
29 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_assets' ), 99 );
30 if ( $this->is_wffn_flex_page( 'all' ) ) {
31 add_action( 'admin_enqueue_scripts', array( $this, 'js_variables' ), 0 );
32 }
33 if ( $this->is_wffn_flex_page() ) {
34 add_action( 'admin_enqueue_scripts', array( $this, 'maybe_register_breadcrumb_nodes' ), 5 );
35 }
36
37 /** Add update count badge to FunnelKit menu - runs after core menu modifications */
38 add_action( 'admin_head', array( $this, 'add_menu_update_badge' ), 9999 );
39 /**
40 * DB updates and table installation
41 */
42 $get_db_version = get_option( '_wffn_db_version', '0.0.0' );
43
44 if ( version_compare( WFFN_DB_VERSION, $get_db_version, '>' ) ) {
45 add_action( 'admin_init', array( $this, 'check_db_version' ), 990 );
46 }
47 add_action( 'admin_init', array( $this, 'maybe_update_database_update' ), 995 );
48
49 add_action( 'admin_init', array( $this, 'reset_wizard' ) );
50 if ( $this->is_wffn_flex_page() && ( ! isset( $_GET['path'] ) || '/user-setup' !== sanitize_text_field( wp_unslash( $_GET['path'] ) ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
51 add_action( 'admin_init', array( $this, 'maybe_force_redirect_to_wizard' ) );
52 }
53 add_action( 'admin_head', array( $this, 'hide_from_menu' ) );
54
55 add_filter( 'get_pages', array( $this, 'add_landing_in_home_pages' ), 10, 2 );
56 add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ), 1 );
57 global $wffn_notices;
58 if ( is_array( $wffn_notices ) || ! empty( $wffn_notices ) ) {
59 add_action( 'admin_notices', array( $this, 'maybe_show_notices' ) );
60 }
61 if ( $this->is_wffn_flex_page( 'all' ) ) {
62 add_action( 'in_admin_header', array( $this, 'remove_all' ), - 1 );
63 }
64 add_filter( 'plugin_action_links_' . WFFN_PLUGIN_BASENAME, array( $this, 'plugin_actions' ) );
65
66 /** Initiate Background updater if action scheduler is not available for template importing */
67 add_action( 'init', array( $this, 'wffn_maybe_init_background_updater' ), 110 );
68 add_filter(
69 'bwf_general_settings_link',
70 function () {
71 return admin_url( 'admin.php?page=bwf&path=/funnels' );
72 },
73 100000
74 );
75 add_filter( 'woofunnels_show_reset_tracking', '__return_true', 999 );
76 add_action( 'admin_head', array( $this, 'menu_highlight' ), 99999 );
77 add_action( 'pre_get_posts', array( $this, 'load_page_to_home_page' ), 9999 );
78 add_filter( 'bwf_settings_config_general', array( $this, 'settings_config' ) );
79
80 add_filter( 'bwf_experiment_ref_link', array( $this, 'maybe_modify_link' ), 10, 2 );
81
82 add_action( 'before_delete_post', array( $this, 'delete_funnel_step_permanently' ), 10, 2 );
83 add_filter( 'wffn_rest_get_funnel_steps', array( $this, 'maybe_delete_funnel_step' ), 10, 2 );
84 add_action( 'admin_bar_menu', array( $this, 'add_menu_in_admin_bar' ), 99 );
85
86 add_action( 'wffn_rest_plugin_activate_response', array( $this, 'maybe_add_auth_link_stripe' ), 10, 2 );
87 add_action( 'wffn_rest_plugin_activate_response', array( $this, 'maybe_add_auth_link_square' ), 10, 2 );
88
89 add_filter( 'woofunnels_global_settings', array( $this, 'add_global_setting_tabs' ), 5 );
90
91 add_filter( 'woofunnels_global_settings_fields', array( $this, 'add_settings_fields_array' ), 110 );
92
93 add_action( 'wp_ajax_wffn_blocks_incompatible_switch_to_classic', array( $this, 'blocks_incompatible_switch_to_classic_cart_checkout' ) );
94 add_action( 'wp_ajax_wffn_dismiss_notice', array( $this, 'ajax_dismiss_admin_notice' ) );
95 add_filter(
96 'bwf_general_settings_default_config',
97 function ( $config ) {
98 if ( isset( $config['allow_theme_css'] ) ) {
99
100 /**
101 * Allow default theme script if user use any snippet
102 */
103 $allowed_themes = apply_filters( 'wffn_allowed_themes', array( 'flatsome', 'Extra', 'divi', 'Divi', 'astra', 'jupiterx', 'kadence' ) );
104
105 if ( function_exists( 'WFFN_Core' ) && ( ( is_array( $allowed_themes ) && in_array( get_template(), $allowed_themes, true ) ) || WFFN_Core()->page_builders->is_divi_theme_enabled() ) ) {
106 $config['allow_theme_css'] = array(
107 'wfacp_checkout',
108 'wffn_ty',
109 'wffn_landing',
110 'wffn_optin',
111 'wffn_oty',
112 'wfocu_offer',
113 );
114
115 }
116 }
117
118 return $config;
119 },
120 10,
121 2
122 );
123
124 if ( isset( $_GET['wfacp_id'] ) && isset( $_GET['new_ui'] ) && 'wffn' === $_GET['new_ui'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
125 add_action( 'init', array( $this, 'redirect_checkout_edit_link_on_new_ui' ) );
126 }
127 if ( defined( 'FKCART_PLUGIN_FILE' ) ) {
128 add_filter(
129 'fkcart_app_header_menu',
130 function ( $menu ) {
131 if ( isset( $menu['analytics'] ) ) {
132 return $menu;
133 }
134
135 $keys = array_keys( $menu );
136 $values = array_values( $menu );
137
138 $indexToInsert = array_search( 'templates', $keys, true );
139
140 array_splice( $keys, $indexToInsert, 0, 'analytics' );
141 array_splice(
142 $values,
143 $indexToInsert,
144 0,
145 array(
146 'analytics' => array(
147 'name' => 'Analytics',
148 'link' => admin_url( 'admin.php?page=bwf&path=/analytics' ),
149 ),
150 )
151 );
152
153 $resultArray = array_combine( $keys, $values );
154 if ( isset( $menu['settings'] ) ) {
155 unset( $resultArray['settings'] );
156 }
157
158 return $resultArray;
159 }
160 );
161 }
162
163 add_action( 'current_screen', array( $this, 'conditional_includes' ), 1 );
164
165 add_action( 'fk_optimize_conversion_table_analytics', array( $this, 'optimize_conversion_table_analytics' ) );
166 add_action( 'fk_remove_optimize_conversion_table_schedule', array( $this, 'remove_optimize_conversion_table_schedule' ) );
167
168 add_action( 'fk_fb_every_day', array( $this, 'check_memory_limit_errors' ), 9999 );
169 /** Email notification callback */
170 add_action( 'fk_fb_every_day', array( $this, 'maybe_setup_notification_schedule' ) );
171 add_action( 'wffn_performance_notification', array( $this, 'run_notifications' ) );
172 add_action( 'bwfan_run_midnight_cron', array( $this, 'maybe_setup_notification_schedule' ) );
173 if ( isset( $_GET['wffn_email_preview'] ) && current_user_can( 'administrator' ) ) { // @codingStandardsIgnoreLine
174 add_action( 'admin_init', array( $this, 'test_notification_admin' ) );
175 }
176 add_action( 'bwf_global_save_settings_funnelkit_notifications', array( $this, 'save_settings_for_email_notification' ), 10, 1 );
177 add_filter( 'use_block_editor_for_post', array( $this, 'maybe_enable_gutenberg_for_funnel_steps' ), PHP_INT_MAX, 2 );
178
179 /** Update-required surfaces while the installed Pro/Basic is older than WFFN_MIN_PRO_VERSION */
180 if ( is_admin() ) {
181 add_action( 'admin_notices', array( $this, 'maybe_render_sitewide_notice' ) );
182 add_action( 'admin_init', array( $this, 'maybe_register_update_row_message' ) );
183 }
184 add_action( 'admin_bar_menu', array( $this, 'maybe_add_admin_bar_state' ), 100 );
185 add_filter( 'wffn_admin_notifications', array( $this, 'maybe_have_outdated_notif' ) );
186 }
187
188 /**
189 * Enable Gutenberg editor for funnel steps that have Gutenberg design selected
190 *
191 * @param bool $use_block_editor Whether to use block editor
192 * @param WP_Post $post The post object
193 * @return bool
194 */
195 public function maybe_enable_gutenberg_for_funnel_steps( $use_block_editor, $post ) {
196 try {
197 // Define step types and their corresponding meta keys
198 $step_meta_keys = array(
199 'wffn_landing' => '_wflp_selected_design',
200 'wffn_optin' => '_wfop_selected_design',
201 'wffn_oty' => '_wfoty_selected_design',
202 'wfacp_checkout' => '_wfacp_selected_design',
203 'wffn_ty' => '_wftp_selected_design',
204 'wfocu_offer' => '_wfocu_setting',
205 );
206
207 // Check if the post type is one of our funnel steps
208 if ( isset( $step_meta_keys[ $post->post_type ] ) ) {
209 // Get the post meta using the corresponding meta key
210 $selected_design = get_post_meta( $post->ID, $step_meta_keys[ $post->post_type ], true );
211
212 // Handle different meta structures for different step types
213 if ( 'wfocu_offer' === $post->post_type ) {
214 // Upsell offers use template_group property for design type (object structure)
215 if ( ! empty( $selected_design ) && isset( $selected_design->template_group ) && 'gutenberg' === $selected_design->template_group ) {
216 return true; // Enable Gutenberg
217 }
218 } else {
219 // Other step types use selected_type property (array structure)
220 if ( ! empty( $selected_design ) && isset( $selected_design['selected_type'] ) && 'gutenberg' === $selected_design['selected_type'] ) {
221 return true; // Enable Gutenberg
222 }
223 }
224 }
225
226 return $use_block_editor; // Default editor (Classic Editor) for all other cases
227 } catch ( \Throwable $e ) {
228 // Log error and return default behavior
229 BWF_logger::get_instance()->log( 'Error in use_block_editor_for_post filter: ' . $e->getMessage(), 'wffn_admin' );
230 return $use_block_editor;
231 }
232
233 // Ensure we always return a value (fallback)
234 return $use_block_editor;
235 }
236
237 /**
238 * @return WFFN_Admin|null
239 */
240 public static function get_instance() {
241 if ( null === self::$ins ) {
242 self::$ins = new self();
243 }
244
245 return self::$ins;
246 }
247
248 /**
249 * Checks WooCommerce fatal error logs for memory limit issues from the previous day.
250 * If found, updates the `fk_memory_limit` option if the memory size is below the configured limit.
251 * Reads the log file using WP Filesystem and searches for memory-related error patterns.
252 */
253 public function check_memory_limit_errors() {
254 global $wp_filesystem;
255 try {
256 $log_dir = wp_upload_dir()['basedir'] . '/wc-logs/';
257 if ( ! is_dir( $log_dir ) ) {
258 return;
259 }
260
261 $yesterday = gmdate( 'Y-m-d', strtotime( '-1 day' ) );
262
263 $log_files = glob( $log_dir . "fatal-errors-{$yesterday}-*.log" );
264
265 if ( empty( $log_files ) ) {
266 return;
267 }
268
269 $log_file = $log_files[0];
270 $log_contents = '';
271
272 if ( empty( $wp_filesystem ) ) {
273 require_once ABSPATH . '/wp-admin/includes/file.php';
274 WP_Filesystem();
275 }
276
277 if ( $wp_filesystem ) {
278 $log_contents = $wp_filesystem->get_contents( $log_file );
279 }
280 $log_lines = array_reverse( explode( "\n", $log_contents ) );
281 $memory_size = null;
282
283 foreach ( $log_lines as $line ) {
284 if ( preg_match( '/Allowed memory size of (\d+) bytes exhausted/', $line, $matches ) ) {
285 $memory_size = isset( $matches[1] ) ? intval( $matches[1] ) : null;
286 break;
287 } elseif ( preg_match( '/Out of memory \(allocated (\d+)\)/', $line, $matches ) ) {
288 $memory_size = isset( $matches[1] ) ? intval( $matches[1] ) : null;
289 break;
290 }
291 }
292 if ( $memory_size && $memory_size < $this->fk_memory_limit ) {
293 update_option( 'fk_memory_limit', $memory_size, 'no' );
294 }
295 } catch ( Exception |Error $e ) {
296 return;
297 }
298 }
299
300 public function add_global_setting_tabs( $menu ) {
301 $f_tracking = array(
302 'title' => __( 'First Party Tracking', 'funnel-builder' ),
303 'slug' => 'funnelkit_first_party_tracking',
304 'link' => apply_filters( 'bwf_general_settings_link', 'javascript:void(0)' ),
305 'priority' => 6,
306 'pro_tab' => true,
307 );
308
309 array_push( $menu, $f_tracking );
310 array_push(
311 $menu,
312 array(
313 'title' => __( 'Pixel Tracking', 'funnel-builder' ),
314 'slug' => 'funnelkit_pixel_tracking',
315 'link' => apply_filters( 'bwf_general_settings_link', 'javascript:void(0)' ),
316 'priority' => 7,
317 )
318 );
319 array_push(
320 $menu,
321 array(
322 'title' => __( 'Advanced', 'funnel-builder' ),
323 'slug' => 'funnelkit_advanced',
324 'link' => apply_filters( 'bwf_general_settings_link', 'javascript:void(0)' ),
325 'priority' => 70,
326 )
327 );
328 array_push(
329 $menu,
330 array(
331 'title' => __( 'Notifications', 'funnel-builder' ),
332 'slug' => 'funnelkit_notifications',
333 'link' => apply_filters( 'bwf_general_settings_link', 'javascript:void(0)' ),
334 'priority' => 70,
335 )
336 );
337
338 return $menu;
339 }
340
341 public function add_settings_fields_array( $settings ) {
342
343 $temp_settings = $settings['woofunnels_general_settings'];
344 $pixel_tracking = array();
345 $first_party_tracking = array();
346 $funnelkit_advanced = array();
347 $notification_settings = array(
348 'title' => 'Email Performance Summary',
349 'heading' => 'Email Performance Summary',
350 'slug' => 'email_performance_summary',
351 'fields' => array(
352 array(
353 'key' => 'bwf_enable_notification',
354 'label' => __( 'Enable Email Performance Summary', 'funnel-builder' ),
355 'styleClasses' => 'wffn-tools-toggle bwf-tooglecontrol-advance',
356 'type' => 'toggle',
357 ),
358 array(
359 'key' => 'bwf_notification_frequency',
360 'label' => __( 'Frequency', 'funnel-builder' ),
361 'type' => 'checkbox_grid',
362 'class' => '',
363 'placeholder' => '',
364 'required' => false,
365 'options' => array(
366 'weekly' => __( 'Weekly', 'funnel-builder' ),
367 'monthly' => __( 'Monthly', 'funnel-builder' ),
368 ),
369 'hint' => __( 'Emails will be skipped if there are no metrics to show', 'funnel-builder' ),
370 'toggler' => array(
371 'key' => 'bwf_enable_notification',
372 'value' => true,
373 ),
374 ),
375 array(
376 'key' => 'bwf_notification_user_selector',
377 'label' => __( 'Users', 'funnel-builder' ),
378 'type' => 'search',
379 'autocompleter' => 'users',
380 'allowFreeTextSearch' => false,
381 'required' => false,
382 'wrap_before' => '',
383 'toggler' => array(
384 'key' => 'bwf_enable_notification',
385 'value' => true,
386 ),
387 ),
388 array(
389 'key' => 'bwf_external_user',
390 'label' => __( 'Other Recipient', 'funnel-builder' ),
391 'type' => 'addrecipient',
392 'class' => '',
393 'required' => false,
394 'toggler' => array(
395 'key' => 'bwf_enable_notification',
396 'value' => true,
397 ),
398 ),
399
400 array(
401 'key' => 'bwf_notification_time',
402 'type' => 'timeselector',
403 'label' => __( 'Send Time', 'funnel-builder' ),
404 'toggler' => array(
405 'key' => 'bwf_enable_notification',
406 'value' => true,
407 ),
408 ),
409 array(
410 'key' => 'send_test_mail',
411 'type' => 'testmail',
412 'label' => '',
413 'class' => 'bwf-position-test-mail-bottom',
414 'toggler' => array(
415 'key' => 'bwf_enable_notification',
416 'value' => true,
417 ),
418 ),
419 ),
420 );
421
422 $values = array();
423 foreach ( $notification_settings['fields'] as &$field ) {
424
425 $values[ $field['key'] ] = BWF_Admin_General_Settings::get_instance()->get_option( $field['key'] );
426 }
427 $notification_settings['values'] = $values;
428
429 $filter_setting = array_filter(
430 $temp_settings,
431 function ( $v, $k ) use ( &$pixel_tracking, &$first_party_tracking, &$funnelkit_advanced ) {
432 if ( in_array( $k, array( 'general', 'permalinks', 'fk_stripe_gateway', 'funnelkit_google_maps' ), true ) ) {
433 return true;
434 }
435 if ( 'funnelkit_advanced' === $k ) {
436 $funnelkit_advanced[ $k ] = $v;
437 } elseif ( 'utm_parameter' === $k ) {
438 $first_party_tracking[ $k ] = $v;
439 } else {
440 $pixel_tracking[ $k ] = $v;
441 }
442
443 return false;
444 },
445 ARRAY_FILTER_USE_BOTH
446 );
447
448 if ( defined( 'WFFN_PRO_VERSION' ) ) {
449 $settings['funnelkit_first_party_tracking'] = array(
450 array(
451 'tabs' => $first_party_tracking,
452 ),
453 );
454 }
455
456 $settings['funnelkit_pixel_tracking'] = array(
457 array(
458 'tabs' => $pixel_tracking,
459 ),
460 );
461 $settings['funnelkit_notifications'] = array(
462 array(
463 'tabs' => array(
464 'funnelkit_notifications' => $notification_settings,
465 ),
466 ),
467 );
468 $settings['funnelkit_advanced'] = array(
469 array(
470 'tabs' => $funnelkit_advanced,
471 ),
472 );
473
474 $permalinks = $filter_setting['permalinks'];
475
476 /**
477 * place upsell permalink after checkout link
478 */
479 if ( isset( $permalinks['fields'] ) && is_array( $permalinks['fields'] ) ) {
480 $wfocu_key = array_search( 'wfocu_page_base', wp_list_pluck( $permalinks['fields'], 'key' ), true );
481 if ( false !== $wfocu_key ) {
482 $wfacp_key = array_search( 'checkout_page_base', wp_list_pluck( $permalinks['fields'], 'key' ), true );
483 if ( false !== $wfacp_key ) {
484 $wfocu_field = $permalinks['fields'][ $wfocu_key ];
485 unset( $permalinks['fields'][ $wfocu_key ] );
486 array_splice( $permalinks['fields'], $wfacp_key + 1, 0, array( $wfocu_field ) );
487
488 }
489 }
490 }
491
492 $settings['woofunnels_general_settings'] = array(
493 array(
494 'heading' => __( 'License', 'funnel-builder' ),
495 'tabs' => array( 'general' => $temp_settings['general'] ),
496 ),
497 array(
498 'heading' => __( 'Permalinks', 'funnel-builder' ),
499 'tabs' => array( 'permalinks' => $permalinks ),
500 ),
501
502 array(
503 'heading' => __( 'Google Maps', 'funnel-builder' ),
504 'tabs' => array( 'funnelkit_google_maps' => $temp_settings['funnelkit_google_maps'] ),
505 ),
506 );
507
508 return $settings;
509 }
510
511 public function add_automations_menu() {
512
513 if ( class_exists( 'BWFAN_Core' ) ) {
514 return;
515 }
516 $user = WFFN_Role_Capability::get_instance()->user_access( 'menu', 'read' );
517 if ( $user ) {
518 add_submenu_page(
519 'woofunnels',
520 __( 'Automations', 'funnel-builder' ),
521 __( 'Automations', 'funnel-builder' ),
522 $user,
523 'bwf&path=/automations',
524 array(
525 $this,
526 'bwf_funnel_pages',
527 )
528 );
529 }
530 }
531
532 public function add_subscriptions_menu() {
533 $user = WFFN_Role_Capability::get_instance()->user_access( 'menu', 'read' );
534 if ( $user ) {
535 add_submenu_page(
536 'woofunnels',
537 __( 'Subscriptions', 'funnel-builder' ),
538 __( 'Subscriptions', 'funnel-builder' ) . '<span style="padding-left: 2px;color: #f18200; vertical-align: super; font-size: 9px;"> NEW!</span>',
539 $user,
540 'bwf&path=/subscriptions',
541 array(
542 $this,
543 'bwf_funnel_pages',
544 )
545 );
546 }
547 }
548
549 /**
550 * Add update count badge to FunnelKit main menu
551 * Hooks into admin_head at priority 9999 to run AFTER core woofunnels menu modifications
552 * Core's correct_sub_menu_order() runs at admin_head priority 999
553 * This approach avoids modifying core woofunnels files
554 *
555 * @hooked admin_head - 9999
556 * @return void
557 */
558 public function add_menu_update_badge() {
559 global $menu;
560
561 // Get the updates count HTML badge
562 $updates_count_html = $this->get_updates_count_html();
563
564 // If no badge to show, return early
565 if ( empty( $updates_count_html ) ) {
566 return;
567 }
568
569 // Find and update the FunnelKit menu item
570 foreach ( $menu as $key => &$menu_item ) {
571 // Check if this is the FunnelKit menu (woofunnels page)
572 if ( isset( $menu_item[5] ) && $menu_item[5] === 'toplevel_page_woofunnels' ) {
573 // Add the badge to the menu title (index 0)
574 $menu_item[0] = $menu_item[0] . $updates_count_html;
575 break;
576 }
577 }
578 }
579
580 /**
581 * Get the number of updates available for FunnelKit
582 * Returns 1 when BF or promotional campaigns are active
583 *
584 * @return int Number of updates available
585 */
586 public function get_updates_count() {
587 // Check if any promotional campaign is active
588 if ( $this->is_promotional_campaign_active() ) {
589 return 1;
590 }
591
592 // No active campaigns
593 return 0;
594 }
595
596 /**
597 * Check if any promotional campaign is active and NOT dismissed
598 * Follows the complete user flow: Sticky Banner → Transient Cooldown → Admin Notification
599 *
600 * Badge shows if:
601 * - Campaign is active AND
602 * - Transient cooldown is NOT active AND
603 * - Either sticky banner OR admin notification is NOT dismissed
604 *
605 * User Journey:
606 * 1. Sticky shows (not dismissed) → Badge shows
607 * 2. User dismisses sticky → Transient active (5 min) → Badge hidden
608 * 3. Transient expires → Notification shows → Badge shows
609 * 4. User dismisses notification → Badge hidden permanently
610 *
611 * Total 7 campaigns grouped into 3 menu items:
612 * - BF menu: Pre-BF, BF, Small Business Saturday, BF Extended
613 * - CM menu: CM, Extended CM
614 * - Green Monday menu: GM
615 *
616 * @return bool True if campaign active and not fully dismissed, false otherwise
617 */
618 public function is_promotional_campaign_active() {
619 // Only show for non-PRO users
620 if ( defined( 'WFFN_PRO_VERSION' ) ) {
621 return false;
622 }
623
624 $year = gmdate( 'Y' );
625 $current_user_id = get_current_user_id();
626 $notifications = WFFN_Core()->admin_notifications;
627
628 // Check if transient cooldown is active (5-minute period) - if yes, don't show badge
629 $transient = get_transient( 'wffn_sticky_banner_dismissed_' . $current_user_id );
630 if ( false !== $transient ) {
631 return false;
632 }
633
634 // BF group campaigns (4 campaigns)
635 if ( $notifications->show_pre_bfcm_header_notification() ) {
636 $notification_key = 'promo_bf_pre_' . $year;
637 $sticky_key = 'sticky_pre_bfcm_' . $year;
638 // Show badge if sticky is active (not dismissed) OR if notification is active (not dismissed)
639 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
640 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
641 return true;
642 }
643 }
644
645 if ( $notifications->show_bf_header_notification() ) {
646 $notification_key = 'promo_bf_' . $year;
647 $sticky_key = 'sticky_bfcm_' . $year;
648
649 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
650 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
651 return true;
652 }
653 }
654
655 if ( $notifications->show_small_business_saturday_header_notification() ) {
656 $notification_key = 'promo_bf_sbs_' . $year;
657 $sticky_key = 'sticky_sbs_' . $year;
658
659 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
660 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
661 return true;
662 }
663 }
664
665 if ( $notifications->show_bfext_header_notification() ) {
666 $notification_key = 'promo_bf_bfext_' . $year;
667 $sticky_key = 'sticky_bfext_' . $year;
668
669 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
670 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
671 return true;
672 }
673 }
674
675 // CM group campaigns (2 campaigns)
676 if ( $notifications->show_cm_header_notification() ) {
677 $notification_key = 'promo_bf_cm_' . $year;
678 $sticky_key = 'sticky_cm_' . $year;
679
680 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
681 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
682 return true;
683 }
684 }
685
686 if ( $notifications->show_cmext_header_notification() ) {
687 $notification_key = 'promo_bf_cmext_' . $year;
688 $sticky_key = 'sticky_cmext_' . $year;
689
690 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
691 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
692 return true;
693 }
694 }
695
696 // Green Monday campaign (1 campaign)
697 if ( $notifications->show_green_monday_header_notification() ) {
698 $notification_key = 'promo_bf_gm_' . $year;
699 $sticky_key = 'sticky_gm_' . $year;
700
701 if ( ! $notifications->is_user_dismissed( $current_user_id, $sticky_key )
702 || ! $notifications->is_user_dismissed( $current_user_id, $notification_key ) ) {
703 return true;
704 }
705 }
706
707 return false;
708 }
709
710 /**
711 * Return the updates count markup for FunnelKit menus
712 *
713 * @return string Updates count markup, empty string if no updates available
714 */
715 public function get_updates_count_html() {
716 $count = $this->get_updates_count();
717
718 if ( $count <= 0 ) {
719 return '';
720 }
721
722 $count_html = sprintf(
723 ' <span class="update-plugins count-%d"><span class="update-count">%d</span></span>',
724 $count,
725 number_format_i18n( $count )
726 );
727
728 return $count_html;
729 }
730
731 public function register_admin_menu() {
732 $steps = WFFN_Core()->steps->get_supported_steps();
733 if ( count( $steps ) < 1 ) {
734 return;
735 }
736
737 $user = WFFN_Role_Capability::get_instance()->user_access( 'menu', 'read' );
738 if ( $user ) {
739
740 add_submenu_page(
741 'woofunnels',
742 __( 'Dashboard', 'funnel-builder' ),
743 __( 'Dashboard', 'funnel-builder' ),
744 $user,
745 'bwf',
746 array(
747 $this,
748 'bwf_funnel_pages',
749 )
750 );
751
752 add_submenu_page(
753 'woofunnels',
754 __( 'Funnels', 'funnel-builder' ),
755 __( 'Funnels', 'funnel-builder' ),
756 $user,
757 'bwf&path=/funnels',
758 array(
759 $this,
760 'bwf_funnel_pages',
761 )
762 );
763
764 add_submenu_page(
765 'woofunnels',
766 __( 'Templates', 'funnel-builder' ),
767 __( 'Templates', 'funnel-builder' ),
768 $user,
769 'bwf&path=/templates',
770 array(
771 $this,
772 'bwf_funnel_pages',
773 )
774 );
775 add_submenu_page(
776 'woofunnels',
777 __( 'Analytics', 'funnel-builder' ),
778 __( 'Analytics', 'funnel-builder' ),
779 $user,
780 'bwf&path=/analytics',
781 array(
782 $this,
783 'bwf_funnel_pages',
784 )
785 );
786
787 add_submenu_page(
788 'woofunnels',
789 __( 'Store Checkout', 'funnel-builder' ),
790 __( 'Store Checkout', 'funnel-builder' ),
791 $user,
792 'bwf&path=/store-checkout',
793 array(
794 $this,
795 'bwf_funnel_pages',
796 )
797 );
798 }
799 }
800
801 public function is_basic_exists() {
802 return defined( 'WFFN_BASIC_FILE' );
803 }
804
805 public function bwf_funnel_pages() {
806
807 ?>
808 <div id="wffn-contacts" class="wffn-page">
809 </div>
810 <?php
811
812 wp_enqueue_style( 'wffn-flex-admin', $this->get_admin_url() . '/assets/css/admin.css', array(), WFFN_VERSION_DEV );
813 }
814
815 public function admin_enqueue_assets( $hook_suffix ) {
816 wp_enqueue_style( 'bwf-admin-font', $this->get_admin_url() . '/assets/css/bwf-admin-font.css', array(), WFFN_VERSION_DEV );
817
818 if ( strpos( $hook_suffix, 'woofunnels_page' ) > - 1 || strpos( $hook_suffix, 'page_woofunnels' ) > - 1 ) {
819 wp_enqueue_style( 'bwf-admin-header', $this->get_admin_url() . '/assets/css/admin-global-header.css', array(), WFFN_VERSION_DEV );
820 }
821
822 if ( $this->is_wffn_flex_page( 'all' ) ) {
823 if ( WFFN_Role_Capability::get_instance()->user_access( 'funnel', 'write' ) ) {
824 add_filter( 'user_can_richedit', '__return_true', 999 );
825 }
826
827 wp_enqueue_style( 'wffn-flex-admin', $this->get_admin_url() . '/assets/css/admin.css', array(), WFFN_VERSION_DEV );
828
829 if ( WFFN_Core()->admin->is_wffn_flex_page() ) {
830 $this->load_react_app( 'main-20260904101952' ); //phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
831 if ( isset( $_GET['page'] ) && $_GET['page'] === 'bwf' && method_exists( 'BWF_Admin_General_Settings', 'get_localized_bwf_data' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
832 wp_localize_script( 'wffn-contact-admin', 'bwfAdminGen', BWF_Admin_General_Settings::get_instance()->get_localized_bwf_data() );
833
834 }
835
836 add_filter(
837 'wffn_noconflict_scripts',
838 function ( $scripts = array() ) {
839 return array_merge( $scripts, array( 'wffn-contact-admin' ) );
840 }
841 );
842 }
843
844 do_action( 'wffn_admin_assets', $this );
845 }
846 }
847
848 public function get_local_app_path() {
849 return '/admin/app/dist/';
850 }
851
852 public function get_editor_priority_theme_name() {
853 try {
854 $current_theme = strtolower( get_template() );
855
856 // Theme priority mapping
857 $theme_map = array(
858 'flatsome' => 'Flatsome',
859 'avada' => 'Avada',
860 'x' => 'X',
861 );
862
863 // Check for theme priority first
864 if ( isset( $theme_map[ $current_theme ] ) ) {
865 return $theme_map[ $current_theme ];
866 }
867
868 // Plugin priority mapping (page builders)
869 $plugin_map = array(
870 'breakdance/plugin.php' => 'Breakdance',
871 'thrive-visual-editor/thrive-visual-editor.php' => 'Thrive',
872 'bb-plugin/fl-builder.php' => 'Beaver Builder',
873 'js_composer/js_composer.php' => 'Visual Composer',
874 'visualcomposer/visualcomposer.php' => 'Visual Composer',
875 'themify-builder/themify-builder.php' => 'Themify Builder',
876 'brizy/brizy.php' => 'Brizy',
877 'zionbuilder/zionbuilder.php' => 'Zion',
878 'seedprod/seedprod.php' => 'SeedProd',
879 );
880
881 // Check for plugin priority
882 foreach ( $plugin_map as $plugin_basename => $plugin_name ) {
883 if ( wffn_is_plugin_active( $plugin_basename ) ) {
884 return $plugin_name;
885 }
886 }
887
888 return '';
889 } catch ( \Throwable $e ) {
890 // Log error and return empty string
891 BWF_logger::get_instance()->log( 'Error in get_editor_priority_theme_name: ' . $e->getMessage(), 'wffn_admin' );
892 return '';
893 }
894 }
895
896
897
898 public function load_react_app( $app_name = 'main' ) {
899 $app_name = str_replace( '-{{{APP_VERSION}}}', '', $app_name ); //phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
900 $min = 60 * get_option( 'gmt_offset' );
901 $sign = $min < 0 ? '-' : '+';
902 $absmin = abs( $min );
903 $tz = sprintf( '%s%02d:%02d', $sign, $absmin / 60, $absmin % 60 );
904 $contact_page_data = array(
905 'is_wc_active' => false,
906 'is_wc_square_active' => WFFN_Common::is_wc_square_active(),
907 'date_format' => get_option( 'date_format', 'F j, Y' ),
908 'time_format' => get_option( 'time_format', 'g:i a' ),
909 'app_path' => WFFN_Core()->get_plugin_url() . '/admin/app/dist/',
910 'timezone' => $tz,
911 'flag_img' => WFFN_Core()->get_plugin_url() . '/admin/assets/img/phone/flags.png',
912 'updated_pro_version' => defined( 'WFFN_PRO_VERSION' ) && version_compare( WFFN_PRO_VERSION, '3.0.0 beta', '>=' ),
913 'activation_date' => array(
914 'lite' => $this->get_lite_activation_date(),
915 'pro' => $this->get_pro_activation_date(),
916 ),
917 'get_pro_link' => WFFN_Core()->admin->get_pro_link(),
918 'wc_add_product_url' => admin_url( 'post-new.php?post_type=product' ),
919 'admin_url' => admin_url(),
920 'multilingual' => $this->is_language_support_enabled(),
921 'multilingual_name' => WFFN_Plugin_Compatibilities::get_language_compatible_plugin(),
922 'block_editor_priority_theme' => $this->get_editor_priority_theme_name(),
923 );
924 if ( class_exists( 'WooCommerce' ) ) {
925 $currency = get_woocommerce_currency();
926 $contact_page_data['currency'] = array(
927 'code' => $currency,
928 'precision' => wc_get_price_decimals(),
929 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $currency ), ENT_QUOTES | ENT_HTML401 ),
930 'symbolPosition' => get_option( 'woocommerce_currency_pos' ),
931 'decimalSeparator' => wc_get_price_decimal_separator(),
932 'thousandSeparator' => wc_get_price_thousand_separator(),
933 'priceFormat' => html_entity_decode( get_woocommerce_price_format(), ENT_QUOTES | ENT_HTML401 ),
934 );
935 $contact_page_data['is_wc_active'] = true;
936 }
937
938 $frontend_dir = ( 0 === WFFN_REACT_ENVIRONMENT ) ? WFFN_REACT_DEV_URL : WFFN_Core()->get_plugin_url() . $this->get_local_app_path();
939 if ( class_exists( 'WooCommerce' ) ) {
940 wp_dequeue_style( 'woocommerce_admin_styles' );
941 wp_dequeue_style( 'wc-components' );
942 }
943
944 $assets_path = 1 === WFFN_REACT_ENVIRONMENT ? WFFN_PLUGIN_DIR . $this->get_local_app_path() . "$app_name.asset.php" : $frontend_dir . "/$app_name.asset.php";
945 $assets = file_exists( $assets_path ) ? include $assets_path : array(
946 'dependencies' => array(
947 'lodash',
948 'moment',
949 'react',
950 'react-dom',
951 'wp-api-fetch',
952 'wp-components',
953 'wp-compose',
954 'wp-date',
955 'wp-deprecated',
956 'wp-block-editor',
957 'wp-block-library',
958 'wp-dom',
959 'wp-element',
960 'wp-hooks',
961 'wp-html-entities',
962 'wp-i18n',
963 'wp-keycodes',
964 'wp-polyfill',
965 'wp-primitives',
966 'wp-url',
967 'wp-viewport',
968 'wp-color-picker',
969 'wp-i18n',
970 ),
971 'version' => time(),
972 );
973 $deps = ( isset( $assets['dependencies'] ) ? array_merge( $assets['dependencies'], array( 'jquery' ) ) : array( 'jquery' ) );
974 $version = $assets['version'];
975
976 $script_deps = array_filter(
977 $deps,
978 function ( $dep ) {
979 return false === strpos( $dep, 'css' );
980 }
981 );
982 if ( 'settings' === $app_name ) {
983 $script_deps = array_merge( $script_deps, array( 'wp-color-picker' ) );
984 }
985
986 if ( class_exists( 'WFFN_Header' ) ) {
987 $header_ins = new WFFN_Header();
988 $contact_page_data['header_data'] = $header_ins->get_render_data();
989 }
990
991 $contact_page_data['localize_texts'] = apply_filters( 'wffn_localized_text_admin', array() );
992
993 wp_enqueue_style( 'wp-components' );
994 wp_enqueue_style( 'wffn-contact-admin', $frontend_dir . "$app_name.css", array(), $version );
995 wp_register_script( 'wffn-contact-admin', $frontend_dir . "$app_name.js", $script_deps, $version, true );
996 wp_localize_script( 'wffn-contact-admin', 'wffn_contacts_data', $contact_page_data );
997 wp_enqueue_script( 'wffn-contact-admin' );
998 wp_set_script_translations( 'wffn-contact-admin', 'funnel-builder' );
999
1000 wp_enqueue_editor();
1001 wp_tinymce_inline_scripts();
1002 wp_enqueue_media();
1003 }
1004
1005
1006 public function get_admin_url() {
1007 return WFFN_Core()->get_plugin_url() . '/admin';
1008 }
1009
1010 public function get_admin_path() {
1011 return WFFN_PLUGIN_DIR . '/admin';
1012 }
1013
1014 /**
1015 * @param string $page
1016 *
1017 * @return bool
1018 */
1019 public function is_wffn_flex_page( $page = 'bwf' ) {
1020
1021 if ( isset( $_GET['page'] ) && $_GET['page'] === $page ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1022 return true;
1023 }
1024
1025 if ( isset( $_GET['page'] ) && 'bwf' === $_GET['page'] && 'all' === $page ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1026 return true;
1027 }
1028
1029 return false;
1030 }
1031
1032
1033 public function js_variables() {
1034 if ( $this->is_wffn_flex_page( 'all' ) ) {
1035 $steps_data = WFFN_Common::get_steps_data();
1036 $substeps_data = WFFN_Common::get_substeps_data();
1037 $substeps_data['substep'] = true;
1038
1039 $upsell_exist = function_exists( 'WFOCU_Core' );
1040
1041 $data = array(
1042 'steps_data' => $steps_data,
1043 'substeps' => $substeps_data,
1044 'icons' => array(
1045 'error_cross' => '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2" class="wffn_loader wffn_loader_error">
1046 <circle fill="#e6283f" stroke="#e6283f" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1" class="path circle"></circle>
1047 <line fill="none" stroke="#ffffff" stroke-width="8" stroke-linecap="round" stroke-miterlimit="10" x1="34.4" y1="37.9" x2="95.8" y2="92.3" class="path line"></line>
1048 <line fill="none" stroke="#ffffff" stroke-width="8" stroke-linecap="round" stroke-miterlimit="10" x1="95.8" y1="38" x2="34.4" y2="92.2" class="path line"></line>
1049 </svg>',
1050 'success_check' => '<svg class="wffn_loader wffn_loader_ok" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2">
1051 <circle class="path circle" fill="#13c37b" stroke="#13c37b" stroke-width="5" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"></circle>
1052 <polyline class="path check" fill="none" stroke="#ffffff" stroke-width="9" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "></polyline>
1053 </svg>',
1054 'delete_alert' => '<div class="swal2-header wf_funnel-icon-without-swal"><div class="swal2-icon swal2-warning swal2-animate-warning-icon" style="display: flex;"><span class="swal2-icon-text">!</span></div></div>',
1055 ),
1056 );
1057
1058 $data['settings_texts'] = apply_filters( 'wffn_funnel_settings', array() );
1059
1060 $data['i18n'] = array(
1061 'plugin_activate' => __( 'Activating plugin...', 'funnel-builder' ),
1062 'plugin_install' => __( 'Installing plugin...', 'funnel-builder' ),
1063 'preparingsteps' => __( 'Preparing steps...', 'funnel-builder' ),
1064 'redirecting' => __( 'Redirecting...', 'funnel-builder' ),
1065 'importing' => __( 'Importing...', 'funnel-builder' ),
1066 'custom_import' => __( 'Setting up your funnel...', 'funnel-builder' ),
1067 'ribbons' => array(
1068 'lite' => __( 'Lite', 'funnel-builder' ),
1069 'pro' => __( 'PRO', 'funnel-builder' ),
1070 ),
1071 'test' => __( 'Test', 'funnel-builder' ),
1072 );
1073 if ( wffn_is_wc_active() && false === $upsell_exist ) {
1074 $data['wc_upsells'] = array(
1075 'type' => 'wc_upsells',
1076 'group' => WFFN_Steps::STEP_GROUP_WC,
1077 'title' => __( 'One Click Upsells', 'funnel-builder' ),
1078 'desc' => __( 'Deploy post purchase one click upsells to increase average order value', 'funnel-builder' ),
1079 'dashicons' => 'dashicons-tag',
1080 'icon' => 'tags',
1081 'pro' => true,
1082 );
1083 }
1084
1085 $data['welcome_note_dismiss'] = get_user_meta( get_current_user_id(), '_wffn_welcome_note_dismissed', true );//phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.user_meta_get_user_meta
1086 $data['is_bump_dismissed'] = get_user_meta( get_current_user_id(), '_wffn_bump_promotion_hide', true );//phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.user_meta_get_user_meta
1087 $data['is_upsell_dismissed'] = get_user_meta( get_current_user_id(), '_wffn_upsell_promotion_hide', true );//phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.user_meta_get_user_meta
1088
1089 $data['current_logged_user'] = get_current_user_id();
1090 $data['is_rtl'] = is_rtl();
1091
1092 $data['is_ab_experiment'] = class_exists( 'BWFABT_Core' ) ? 1 : 0;
1093 $data['is_ab_experiment_support'] = ( class_exists( 'BWFABT_Core' ) && version_compare( BWFABT_VERSION, '1.3.5', '>' ) ) ? 1 : 0;
1094 $data['wizard_status'] = $this->is_wizard_available();
1095
1096 $data['automation_plugin_status'] = WFFN_Common::get_plugin_status( 'wp-marketing-automations/wp-marketing-automations.php' );
1097 $data['fkcart_img_url'] = esc_url( plugin_dir_url( WFFN_PLUGIN_FILE ) . 'admin/assets/img/fkcart-img.png' );
1098 $data['fkcart_plugin_status'] = WFFN_Common::get_plugin_status( 'cart-for-woocommerce/plugin.php' );
1099 $data['sublium_plugin_status'] = WFFN_Common::get_plugin_status( 'sublium-subscriptions-for-woocommerce/sublium-subscriptions-for-woocommerce.php' );
1100 $data['fkcart_show_analytics'] = ( defined( 'FKCART_DB_VERSION' ) && version_compare( FKCART_DB_VERSION, '1.7.2', '>=' ) ) ? 'yes' : 'no';
1101 $data['ob_arrow_blink_img_url'] = esc_url( plugin_dir_url( WFFN_PLUGIN_FILE ) . 'admin/assets/img/arrow-blink.gif' );
1102 $data['pro_modal_img_path'] = esc_url( plugin_dir_url( WFFN_PLUGIN_FILE ) . 'admin/assets/img/pro_modal/' );
1103 $data['admin_img_path'] = esc_url( plugin_dir_url( WFFN_PLUGIN_FILE ) . 'admin/assets/img/' );
1104 $bwf_notifications = get_user_meta( get_current_user_id(), '_bwf_notifications_close', true ); //phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.user_meta_get_user_meta
1105 $bwf_notifications = is_array( $bwf_notifications ) ? array_values( $bwf_notifications ) : $bwf_notifications;
1106 $data['user_preferences'] = array( 'notices_close' => $bwf_notifications );
1107 $data['user_has_notifications'] = WFFN_Core()->admin_notifications->user_has_notifications( get_current_user_id() );
1108 $data['pro_link'] = $this->get_pro_link();
1109 $data['upgrade_button_text'] = __( 'Upgrade to PRO Now', 'funnel-builder' );
1110 $data['site_options'] = get_option( 'fb_site_options', array() );
1111 $data['nonce_contact_export_download'] = wp_create_nonce( 'bwf_contact_export_download' );
1112 $data['user_display_name'] = get_user_by( 'id', get_current_user_id() )->display_name;
1113 $data['user_email'] = get_user_by( 'id', get_current_user_id() )->user_email;
1114 $data['bfcm_promo_sticky_banner'] = WFFN_Core()->admin_notifications->get_sticky_banner();
1115 $data['bfcm_promo_sticky_secondary_banner'] = WFFN_Core()->admin_notifications->get_sticky_secondary_banner();
1116
1117 $data['pro_version_number'] = defined( 'WFFN_PRO_VERSION' ) ? WFFN_PRO_VERSION : '0.0.0';
1118 $data['cart_analytics_version'] = '3.11.0';
1119 $data['conditional_fields_version'] = '3.15.0';
1120 $data['site_domain'] = $this->get_site_domain_for_admin();
1121
1122 $data['review_count'] = WFFN_REVIEW_RATING_COUNT;
1123 $data['feature_enabled'] = array(
1124 'funnel_categories' => defined( 'WFFN_PRO_VERSION' ) && version_compare( WFFN_PRO_VERSION, '3.11.0', '>=' ),
1125 'conditional_fields' => defined( 'WFFN_PRO_VERSION' ) && version_compare( WFFN_PRO_VERSION, $data['conditional_fields_version'], '>=' ),
1126 );
1127 $data['fkcart_settings'] = $this->get_fkcart_settings();
1128
1129 ?>
1130 <script>window.wffn = <?php echo wp_json_encode( apply_filters( 'wffn_localize_admin', $data ) ); ?>;</script>
1131 <script>
1132 <?php
1133 echo '
1134 (function() {
1135 setTimeout(() => {
1136 const scriptElement = document.getElementById("wffn-contact-admin-js");
1137 if (scriptElement) {
1138
1139 scriptElement.onerror = function() {
1140 if (typeof window.wffn_loaded === "undefined") {
1141 console.warn("Main JS not loaded, retrying with version parameter...");
1142
1143 const newScript = document.createElement("script");
1144 const version = `?ver=${new Date().getTime()}`;
1145 newScript.src = `${scriptElement.src.split("?")[0]}${version}`;
1146 newScript.id = "wffn-contact-admin-js";
1147 newScript.async = true;
1148
1149 document.body.appendChild(newScript);
1150 }
1151 };
1152 }
1153 }, 3000)
1154 })();
1155 '
1156 ?>
1157 </script>
1158 <?php
1159 }
1160 }
1161
1162 /**
1163 * Get the domain that should be localized for the React admin app.
1164 *
1165 * @return string
1166 */
1167 private function get_site_domain_for_admin() {
1168 global $sitepress;
1169
1170 $domain = site_url();
1171
1172 if ( isset( $sitepress ) && ! is_null( $sitepress ) ) {
1173 $default_language = $sitepress->get_default_language();
1174 $domain = $sitepress->convert_url( $sitepress->get_wp_api()->get_home_url(), $default_language );
1175 }
1176
1177 if ( function_exists( 'pll_default_language' ) && function_exists( 'pll_home_url' ) ) {
1178 $default_language = pll_default_language();
1179 $domain = pll_home_url( $default_language );
1180 }
1181
1182 if ( is_multisite() && $this->is_funnel_builder_network_active() ) {
1183 $domain = get_site_url( get_network()->site_id );
1184 }
1185
1186 $domain = str_replace( array( 'https://', 'http://' ), '', $domain );
1187 $domain = trim( $domain, '/' );
1188
1189 return $domain;
1190 }
1191
1192 private function is_funnel_builder_network_active() {
1193 $active_plugins = get_site_option( 'active_sitewide_plugins', array() );
1194 $active_plugins = apply_filters( 'active_plugins', $active_plugins );
1195
1196 return is_array( $active_plugins ) && ( in_array( WFFN_PLUGIN_BASENAME, $active_plugins, true ) || array_key_exists( WFFN_PLUGIN_BASENAME, $active_plugins ) );
1197 }
1198
1199 /**
1200 * Get Cart for WooCommerce plugin settings for admin localization.
1201 * Returns enabled, rewards, and upsells status from the Cart plugin when active.
1202 *
1203 * @return array{enabled: bool, rewards: bool, upsells: bool}
1204 */
1205 public function get_fkcart_settings() {
1206 $default = array(
1207 'enabled' => false,
1208 'rewards' => false,
1209 'upsells' => false,
1210 );
1211
1212 if ( ! class_exists( 'FKCart\Includes\Data' ) ) {
1213 return apply_filters( 'wffn_fkcart_settings', $default );
1214 }
1215
1216 $settings = array(
1217 'enabled' => \FKCart\Includes\Data::is_cart_enabled(),
1218 'rewards' => \FKCart\Includes\Data::is_rewards_enabled(),
1219 'upsells' => \FKCart\Includes\Data::is_upsells_enabled(),
1220 );
1221
1222 return apply_filters( 'wffn_fkcart_settings', $settings );
1223 }
1224
1225 /**
1226 * Get the already setup funnel object
1227 *
1228 * @return WFFN_Funnel
1229 */
1230 public function get_funnel( $funnel_id = 0 ) {
1231 if ( $funnel_id > 0 ) {
1232 if ( $this->funnel instanceof WFFN_Funnel && $funnel_id === $this->funnel->get_id() ) {
1233 return $this->funnel;
1234 }
1235 $this->initiate_funnel( $funnel_id );
1236 }
1237 if ( $this->funnel instanceof WFFN_Funnel ) {
1238 return $this->funnel;
1239 }
1240 $this->funnel = new WFFN_Funnel( $funnel_id );
1241
1242 return $this->funnel;
1243 }
1244
1245 /**
1246 * @param $funnel_id
1247 */
1248 public function initiate_funnel( $funnel_id ) {
1249 if ( ! empty( $funnel_id ) ) {
1250 $this->funnel = new WFFN_Funnel( $funnel_id );
1251
1252 }
1253 }
1254
1255 public static function get_template_filter() {
1256
1257 $options = array(
1258 'all' => __( 'All', 'funnel-builder' ),
1259 'sales' => __( 'Sales', 'funnel-builder' ),
1260 'optin' => __( 'Optin', 'funnel-builder' ),
1261 );
1262
1263 return $options;
1264 }
1265
1266
1267 public function get_license_status() {
1268 $license_key = WFFN_Core()->remote_importer->get_license_key( true );
1269
1270 if ( empty( $license_key ) ) {
1271 return false;
1272 } elseif ( isset( $license_key['manually_deactivated'] ) && 1 === $license_key['manually_deactivated'] ) {
1273 return 'deactivated';
1274 } elseif ( isset( $license_key['expired'] ) && 1 === $license_key['expired'] ) {
1275 return 'expired';
1276 } elseif ( isset( $license_key['activated'] ) && 0 === $license_key['activated'] ) {
1277 return 'not-active';
1278 }
1279
1280 return true;
1281 }
1282
1283 public function is_license_active() {
1284 return true === $this->get_license_status();
1285 }
1286
1287
1288 /**
1289 * @hooked over `admin_enqueue_scripts`
1290 * Check the environment and register appropiate node for the breadcrumb to process
1291 * @since 1.0.0
1292 */
1293 public function maybe_register_breadcrumb_nodes() {
1294 $single_link = '';
1295 $funnel = null;
1296 /**
1297 * IF its experiment builder UI
1298 */
1299 if ( $this->is_wffn_flex_page() ) {
1300
1301 $funnel = $this->get_funnel();
1302
1303 } else {
1304
1305 /**
1306 * its its a page where experiment page is a referrer
1307 */
1308 $get_ref = filter_input( INPUT_GET, 'funnel_id', FILTER_UNSAFE_RAW ); //phpcs:ignore WordPressVIPMinimum.Security.PHPFilterFunctions.RestrictedFilter
1309 $get_ref = apply_filters( 'maybe_setup_funnel_for_breadcrumb', $get_ref );
1310 if ( ! empty( $get_ref ) ) {
1311 $funnel = $this->get_funnel( $get_ref );
1312 if ( absint( $funnel->get_id() ) === WFFN_Common::get_store_checkout_id() ) {
1313 $single_link = WFFN_Common::get_store_checkout_edit_link();
1314 } else {
1315 $single_link = WFFN_Common::get_funnel_edit_link( $funnel->get_id() );
1316 }
1317 }
1318 }
1319
1320 /**
1321 * Register nodes
1322 */
1323 if ( ! empty( $funnel ) && null === filter_input( INPUT_GET, 'bwf_exp_ref', FILTER_UNSAFE_RAW ) ) { //phpcs:ignore WordPressVIPMinimum.Security.PHPFilterFunctions.RestrictedFilter
1324
1325 BWF_Admin_Breadcrumbs::register_node(
1326 array(
1327 'text' => WFFN_Core()->admin->maybe_empty_title( $funnel->get_title() ),
1328 'link' => $single_link,
1329 )
1330 );
1331 BWF_Admin_Breadcrumbs::register_ref( 'funnel_id', $funnel->get_id() );
1332
1333 }
1334 }
1335
1336
1337 public function get_date_format() {
1338 return get_option( 'date_format', '' ) . ' ' . get_option( 'time_format', '' );
1339 }
1340
1341 /**
1342 * @return array
1343 */
1344 public function get_funnels( $args = array() ) {
1345 global $wpdb;
1346 $is_total_query_required = true;
1347 if ( isset( $args['offset'] ) && ! empty( $args['offset'] ) ) {
1348 $is_total_query_required = false;
1349 }
1350 if ( isset( $args['s'] ) ) {
1351 $search_str = wffn_clean( $args['s'] );
1352 } else {
1353 $search_str = isset( $_REQUEST['s'] ) ? wffn_clean( wp_unslash( $_REQUEST['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1354 }
1355 $need_draft_count = $args['need_draft_count'] ?? false;
1356 if ( isset( $args['status'] ) ) {
1357 $status = wffn_clean( $args['status'] );
1358 } else {
1359 $status = isset( $_REQUEST['status'] ) ? wffn_clean( wp_unslash( $_REQUEST['status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1360 }
1361 $args['meta'] = isset( $args['meta'] ) ? $args['meta'] : array();
1362 $limit = isset( $args['limit'] ) ? absint( $args['limit'] ) : $this->posts_per_page();
1363
1364 $sql_query = ' FROM {table_name}';
1365
1366 $args = apply_filters( 'wffn_funnels_args_query', $args );
1367
1368 if ( isset( $args['meta'] ) && is_array( $args['meta'] ) && ! empty( $args['meta'] ) && ! isset( $args['meta']['compare'] ) ) {
1369 $args['meta']['compare'] = '=';
1370 }
1371
1372 /*
1373 * Trying to add join in query base on meta
1374 */
1375 if ( ! empty( $args['meta'] ) ) {
1376 if ( $args['meta']['compare'] === 'NOT_EXISTS' ) {
1377 $sql_query .= ' LEFT JOIN ';
1378 } else {
1379 $sql_query .= ' INNER JOIN ';
1380 }
1381 $sql_query .= '{table_name_meta} ON ( {table_name}.id = {table_name_meta}.bwf_funnel_id ';
1382 if ( $args['meta']['compare'] === 'NOT_EXISTS' ) {
1383 $sql_query .= $wpdb->prepare( 'AND {table_name_meta}.meta_key = %s', $args['meta']['key'] );
1384 }
1385 $sql_query .= ')';
1386
1387 }
1388
1389 /*
1390 * Sort list with meta key.
1391 */
1392 if ( ! empty( $args['order_by_meta'] ) ) {
1393 $sql_query .= $wpdb->prepare(
1394 ' LEFT JOIN {table_name_meta} as order_by_meta
1395 ON ( {table_name}.id = order_by_meta.bwf_funnel_id AND order_by_meta.meta_key = %s )',
1396 $args['order_by_meta']['key']
1397 );
1398 }
1399 if ( ! empty( $args['categories'] ) && is_array( $args['categories'] ) ) {
1400 $sql_query .= ' INNER JOIN {table_name_meta} AS cat_meta ON ( {table_name}.id = cat_meta.bwf_funnel_id AND cat_meta.meta_key = "wffn_funnel_category" )';
1401 }
1402 /*
1403 * where clause start here in query
1404 */
1405 $sql_query .= ' WHERE 1=1';
1406
1407 if ( ! empty( $args['categories'] ) && is_array( $args['categories'] ) ) {
1408 $cat_conditions = array();
1409
1410 foreach ( $args['categories'] as $category ) {
1411 $clean_category = sanitize_title( $category );
1412 $cat_conditions[] = $wpdb->prepare( '(cat_meta.meta_value LIKE %s)', "%$clean_category%" );
1413 }
1414
1415 $sql_query .= ' AND (' . implode( ' OR ', $cat_conditions ) . ')';
1416 }
1417
1418 if ( ! empty( $status ) && 'all' !== $status ) {
1419 $status = ( 'live' === $status ) ? 1 : 0;
1420 $sql_query .= $wpdb->prepare( ' AND `status` = %d', $status );
1421 }
1422
1423 if ( ! empty( $search_str ) ) {
1424 $sql_query .= $wpdb->prepare( ' AND ( `title` LIKE %s OR `desc` LIKE %s )', '%' . $search_str . '%', '%' . $search_str . '%' );
1425 }
1426 if ( ! empty( $args['meta'] ) ) {
1427 if ( $args['meta']['compare'] === 'NOT_EXISTS' ) {
1428
1429 $sql_query .= ' AND ({table_name_meta}.bwf_funnel_id IS NULL) ';
1430 if ( false === $is_total_query_required ) {
1431 $sql_query .= ' GROUP BY {table_name}.id';
1432
1433 }
1434 } else {
1435 $sql_query .= $wpdb->prepare( ' AND ( {table_name_meta}.meta_key = %s AND {table_name_meta}.meta_value = %s )', $args['meta']['key'], $args['meta']['value'] );
1436 }
1437 }
1438
1439 if ( ! empty( $args['order_by_meta'] ) ) {
1440 $sql_query .= ' ORDER BY
1441 CASE WHEN order_by_meta.meta_value IS NULL THEN 1 ELSE 0 END,
1442 order_by_meta.meta_value ' . ( isset( $args['order_by_meta']['order'] ) && 'ASC' === strtoupper( $args['order_by_meta']['order'] ) ? 'ASC' : 'DESC' );
1443 } else {
1444 $sql_query .= ' ORDER BY {table_name}.id DESC';
1445 }
1446
1447 if ( false === $is_total_query_required ) {
1448 $sql_query .= ' LIMIT ' . absint( $args['offset'] ) . ', ' . $limit;
1449 } else {
1450 $found_funnels = WFFN_Core()->get_dB()->get_results( 'SELECT count({table_name}.id) as count ' . $sql_query );
1451 $sql_query .= ' LIMIT ' . 0 . ', ' . $limit;
1452 }
1453 $funnel_ids = WFFN_Core()->get_dB()->get_results( 'SELECT {table_name}.id as funnel_id ' . $sql_query );
1454 $items = array();
1455
1456 if ( isset( $args['search_filter'] ) ) {
1457 foreach ( $funnel_ids as $funnel_id ) {
1458 $funnel = new WFFN_Funnel( $funnel_id['funnel_id'] );
1459 $item = array(
1460 'id' => $funnel->get_id(),
1461 'name' => $funnel->get_title(),
1462 );
1463 $items[] = $item;
1464 }
1465
1466 return $items;
1467
1468 } else {
1469 foreach ( $funnel_ids as $funnel_id ) {
1470 $funnel = new WFFN_Funnel( $funnel_id['funnel_id'] );
1471 $steps = $funnel->get_steps();
1472 $view = ( is_array( $steps ) && count( $steps ) > 0 ) ? get_permalink( $steps[0]['id'] ) : '';
1473 if ( false !== $need_draft_count || isset( $args['need_steps_data'] ) ) {
1474 $this->parse_funnels_step_ids( $funnel );
1475 }
1476 $item = array(
1477 'id' => $funnel->get_id(),
1478 'title' => $funnel->get_title(),
1479 'desc' => $funnel->get_desc(),
1480 'date_added' => $funnel->get_date_added(),
1481 'last_update' => $funnel->get_last_update_date(),
1482 'steps' => ( is_array( $this->step_count_against_fid ) && count( $this->step_count_against_fid ) > 0 ) ? count( array_keys( $this->step_count_against_fid, absint( $funnel->get_id() ), true ) ) : 0,
1483 //phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1484 'view_link' => $view,
1485 'categories' => ( defined( 'WFFN_PRO_VERSION' ) ) ? $funnel->get_category() : array(),
1486 );
1487 if ( isset( $args['need_steps_data'] ) ) {
1488 $item['steps_data'] = $steps;
1489 }
1490 if ( ! isset( $args['context'] ) || 'listing' !== $args['context'] ) {
1491 $item['__funnel'] = $funnel;
1492 }
1493 $items[] = $item;
1494 }
1495 if ( true === $is_total_query_required ) {
1496 $found_posts = array( 'found_posts' => (int) $found_funnels[0]['count'] );
1497
1498 } else {
1499 $found_posts = array();
1500
1501 }
1502 if ( false !== $need_draft_count ) {
1503 $draft_counts = $this->get_draft_steps();
1504 $items = array_map(
1505 function ( $item ) use ( $draft_counts ) {
1506 $item['draft_count'] = $draft_counts[ $item['id'] ] ?? 0;
1507
1508 return $item;
1509 },
1510 $items
1511 );
1512
1513 }
1514 $found_posts['items'] = $items;
1515
1516 return apply_filters( 'wffn_funnels_lists', $found_posts );
1517 }
1518 }
1519
1520 /**
1521 * @param WFFN_Funnel $funnel
1522 *
1523 * @return void
1524 */
1525 private function parse_funnels_step_ids( $funnel ) {
1526 $steps = $funnel->get_steps();
1527 $funnel_id = $funnel->get_id();
1528 foreach ( $steps as $step ) {
1529
1530 $step_id = $step['id'];
1531 $this->step_against_fid[ $step_id ] = $funnel_id;
1532 $this->step_count_against_fid[ absint( $step_id ) ] = absint( $funnel_id );
1533
1534 /**
1535 * Handle case of upsells separately
1536 */
1537 if ( $step['type'] === 'wc_upsells' && WFFN_Core()->steps->get_integration_object( 'wc_upsells' ) instanceof WFFN_Step ) {
1538 $funnel_offers = WFOCU_Core()->funnels->get_funnel_steps( $step['id'] );
1539 if ( ! empty( $funnel_offers ) && count( $funnel_offers ) > 1 ) {
1540 $offer_ids = wp_list_pluck( $funnel_offers, 'id' );
1541
1542 $count = 0;
1543 foreach ( $offer_ids as $offer_id ) {
1544 $this->step_against_fid[ absint( $offer_id ) ] = $funnel_id;
1545 /**
1546 * skip first offer in step listing
1547 */
1548 if ( 0 !== $count ) {
1549 $this->step_count_against_fid[ absint( $offer_id ) ] = absint( $funnel_id );
1550 }
1551 ++$count;
1552 }
1553 }
1554 }
1555 }
1556 }
1557
1558 private function get_draft_steps() {
1559 if ( empty( $this->step_against_fid ) ) {
1560 return array();
1561 }
1562
1563 $step_ids = array_map( 'absint', array_keys( $this->step_against_fid ) );
1564
1565 global $wpdb;
1566 $results = $wpdb->get_results(
1567 $wpdb->prepare(
1568 "SELECT ID FROM {$wpdb->prefix}posts WHERE 1=1 AND post_status != %s and id IN ( " . implode( ', ', array_fill( 0, count( $step_ids ), '%d' ) ) . ' )',
1569 array_merge( array( 'publish' ), $step_ids )
1570 ),
1571 ARRAY_A
1572 );
1573 $draft_count = array();
1574 foreach ( $results as $result ) {
1575 $step_id = $result['ID'];
1576 $funnel_id = $this->step_against_fid[ $step_id ];
1577 if ( ! isset( $draft_count[ $funnel_id ] ) ) {
1578 $draft_count[ $funnel_id ] = 1;
1579 } else {
1580 ++$draft_count[ $funnel_id ];
1581 }
1582 }
1583
1584 return $draft_count;
1585 }
1586
1587 public function posts_per_page() {
1588 return 20;
1589 }
1590
1591
1592 public function hide_from_menu() {
1593 global $submenu;
1594 foreach ( $submenu as $key => $men ) {
1595 if ( 'woofunnels' !== $key ) {
1596 continue;
1597 }
1598 foreach ( $men as $k => $d ) {
1599 if ( 'woofunnels-settings' === $d[2] ) {
1600 unset( $submenu[ $key ][ $k ] );
1601 }
1602 }
1603 }
1604 }
1605
1606
1607 /**
1608 * Adding landing pages in homepage display settings
1609 *
1610 * @param $pages
1611 * @param $args
1612 *
1613 * @return array
1614 */
1615 public function add_landing_in_home_pages( $pages, $args ) {
1616 if ( is_array( $args ) && isset( $args['name'] ) && 'page_on_front' !== $args['name'] && '_customize-dropdown-pages-page_on_front' !== $args['name'] ) {
1617 return $pages;
1618 }
1619
1620 if ( is_array( $args ) && isset( $args['name'] ) && ( 'page_on_front' === $args['name'] || '_customize-dropdown-pages-page_on_front' === $args['name'] ) ) {
1621 $landing_pages = get_posts(
1622 array( //phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_posts_get_posts
1623 'post_type' => WFFN_Core()->landing_pages->get_post_type_slug(),
1624 'numberposts' => 100,
1625 'post_status' => 'publish',
1626 )
1627 );
1628
1629 $optin_pages = get_posts(
1630 array( //phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_posts_get_posts
1631 'post_type' => WFOPP_Core()->optin_pages->get_post_type_slug(),
1632 'numberposts' => 100,
1633 'post_status' => 'publish',
1634 )
1635 );
1636
1637 $pages = array_merge( $pages, $landing_pages, $optin_pages );
1638 }
1639
1640 return $pages;
1641 }
1642
1643
1644 public function admin_footer_text( $footer_text ) {
1645 if ( false === WFFN_Role_Capability::get_instance()->user_access( 'funnel', 'read' ) ) {
1646 return $footer_text;
1647 }
1648
1649 $current_screen = get_current_screen();
1650 $wffn_pages = array( 'woofunnels_page_bwf', 'woofunnels_page_wffn-settings' );
1651
1652 // Check to make sure we're on a WooFunnels admin page.
1653 if ( isset( $current_screen->id ) && apply_filters( 'bwf_funnels_funnels_display_admin_footer_text', in_array( $current_screen->id, $wffn_pages, true ), $current_screen->id ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1654 // Change the footer text.
1655 $footer_text = sprintf( __( 'Over %1$s 5 star reviews show that FunnelKit users trust our top-rated support for their online business. Do you need help? <a href="%2$s" target="_blank"><b>Contact FunnelKit Support</b></a>', 'funnel-builder' ), WFFN_REVIEW_RATING_COUNT . '+', 'https://funnelkit.com/support/?utm_source=WordPress&utm_medium=Support+Footer&utm_campaign=FB+Lite+Plugin' );
1656
1657 }
1658
1659 return $footer_text;
1660 }
1661
1662 public function maybe_show_notices() {
1663
1664 global $wffn_notices;
1665 foreach ( $wffn_notices as $notice ) {
1666 echo wp_kses_post( $notice );
1667 }
1668 }
1669
1670 public function remove_all() {
1671 remove_all_actions( 'admin_notices' );
1672 remove_all_actions( 'all_admin_notices' );
1673 remove_all_actions( 'user_admin_notices' );
1674 remove_all_actions( 'network_admin_notices' );
1675 }
1676
1677 /**
1678 * Hooked over 'plugin_action_links_{PLUGIN_BASENAME}' WordPress hook to add deactivate popup support & add PRO link
1679 *
1680 * @param array $links array of existing links
1681 *
1682 * @return array modified array
1683 */
1684 public function plugin_actions( $links ) {
1685 if ( isset( $links['deactivate'] ) ) {
1686 $links['deactivate'] .= '<i class="woofunnels-slug" data-slug="' . WFFN_PLUGIN_BASENAME . '"></i>';
1687 }
1688 if ( ! defined( 'WFFN_PRO_VERSION' ) ) {
1689 $link = add_query_arg(
1690 array(
1691 'utm_source' => 'WordPress',
1692 'utm_medium' => 'All+Plugins',
1693 'utm_campaign' => 'FB+Lite+Plugin',
1694 'utm_content' => WFFN_VERSION,
1695 ),
1696 $this->get_pro_link()
1697 );
1698 $links = array_merge(
1699 array(
1700 'pro_upgrade' => '<a href="' . $link . '" target="_blank" style="color: #1da867 !important;font-weight:600">' . __( 'Upgrade to Pro', 'funnel-builder' ) . '</a>',
1701 ),
1702 $links
1703 );
1704 }
1705
1706 return $links;
1707 }
1708
1709 /**
1710 * Initiate WFFN_Background_Importer class if ActionScheduler class doesn't exist
1711 *
1712 * @see woofunnels_maybe_update_customer_database()
1713 */
1714 public function wffn_maybe_init_background_updater() {
1715 if ( class_exists( 'WFFN_Background_Importer' ) ) {
1716
1717 $this->wffn_updater = new WFFN_Background_Importer();
1718 }
1719 }
1720
1721 /**
1722 * @hooked over `admin_init`
1723 * This method takes care of template importing
1724 * Checks whether there is a need to import
1725 * Iterates over define callbacks and passes it to background updater class
1726 * Updates templates for all steps of the funnels
1727 */
1728 public function wffn_maybe_run_templates_importer() {
1729 if ( is_null( $this->wffn_updater ) ) {
1730 return;
1731 }
1732 $funnel_id = get_option( '_wffn_scheduled_funnel_id', 0 );
1733
1734 if ( $funnel_id > 0 ) { // WPCS: input var ok, CSRF ok.
1735
1736 $task = 'wffn_maybe_import_funnel_in_background'; // Scanning order table and updating customer tables
1737 $this->wffn_updater->push_to_queue( $task );
1738 BWF_Logger::get_instance()->log( '**************START Importing************', 'wffn_template_import' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1739 $this->wffn_updater->save()->dispatch();
1740 BWF_Logger::get_instance()->log( 'First Dispatch completed', 'wffn_template_import' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1741 }
1742 }
1743
1744 /**
1745 * Delete wffn-wizard and redirect install
1746 */
1747 public function reset_wizard() {
1748 if ( current_user_can( 'manage_options' ) && isset( $_GET['wffn_show_wizard_force'] ) && 'yes' === $_GET['wffn_show_wizard_force'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
1749
1750 delete_option( '_wffn_onboarding_completed' );
1751 delete_user_meta( get_current_user_id(), '_bwf_notifications_close' );
1752 wp_redirect( $this->wizard_url() );
1753 exit;
1754
1755 }
1756 }
1757
1758 /**
1759 * @return array
1760 */
1761 public function get_all_active_page_builders() {
1762 $page_builders = array( 'gutenberg', 'elementor', 'divi', 'oxy', 'bricks' );
1763
1764 return $page_builders;
1765 }
1766
1767 /**
1768 * Detect available page builders and set default settings
1769 *
1770 * This method checks for the following page builders in order of priority:
1771 * - Elementor (elementor/elementor.php)
1772 * - Divi (divi-builder/divi-builder.php or Divi theme)
1773 * - Oxygen (oxygen/functions.php)
1774 * - Bricks (Bricks theme)
1775 *
1776 * Only works if current default builder is 'elementor' or blank.
1777 *
1778 * @return string|null The detected page builder slug or null if none found
1779 */
1780 public function detect_and_set_default_page_builder( $force = false ) {
1781 try {
1782 $general_settings = BWF_Admin_General_Settings::get_instance();
1783 $current_default = $general_settings->get_option( 'default_selected_builder' );
1784
1785 // Only proceed if current default is 'elementor' or blank
1786 if ( ! $force && ! empty( $current_default ) && $current_default !== 'elementor' ) {
1787 return null;
1788 }
1789
1790 $detected_builder = null;
1791 $page_builders = array(
1792 'elementor' => array(
1793 'plugin_file' => 'elementor/elementor.php',
1794 'check_function' => function () {
1795 return class_exists( '\Elementor\Plugin' );
1796 },
1797 ),
1798 'divi5' => array(
1799 'plugin_file' => '',
1800 'check_function' => function () {
1801 return WFFN_Core()->page_builders->is_divi_theme_enabled() && function_exists( 'et_builder_d5_enabled' ) && et_builder_d5_enabled();
1802 },
1803 ),
1804 'divi' => array(
1805 'plugin_file' => 'divi-builder/divi-builder.php',
1806 'check_function' => function () {
1807 return WFFN_Core()->page_builders->is_divi_theme_enabled();
1808 },
1809 ),
1810 'oxy' => array(
1811 'plugin_file' => 'oxygen/functions.php',
1812 'check_function' => function () {
1813 return class_exists( 'OxygenElement' );
1814 },
1815 ),
1816 'bricks' => array(
1817 'plugin_file' => '',
1818 'check_function' => function () {
1819 return WFFN_Core()->page_builders->is_bricks_theme_enabled();
1820 },
1821 ),
1822 );
1823
1824 // Check each page builder
1825 foreach ( $page_builders as $builder_slug => $builder_config ) {
1826 $is_available = false;
1827
1828 // Check if plugin file exists and is active
1829 if ( ! empty( $builder_config['plugin_file'] ) ) {
1830 $plugin_status = WFFN_Common::get_plugin_status( $builder_config['plugin_file'] );
1831 $is_available = ( 'activated' === $plugin_status );
1832 }
1833
1834 // If no plugin file or plugin not active, check using custom function
1835 if ( ! $is_available && isset( $builder_config['check_function'] ) ) {
1836 $is_available = $builder_config['check_function']();
1837 }
1838
1839 if ( $is_available ) {
1840 $detected_builder = $builder_slug;
1841 break;
1842 }
1843 }
1844
1845 // Set default builder if detected, otherwise set to blank
1846 if ( ! empty( $detected_builder ) ) {
1847 // Only update if no default is set or if it's different from detected
1848 if ( empty( $current_default ) || $current_default !== $detected_builder ) {
1849 // Update using the proper settings method
1850 $general_settings->update_global_settings_fields( array( 'default_selected_builder' => $detected_builder ) );
1851
1852 // Log the detection for debugging
1853 BWF_Logger::get_instance()->log(
1854 sprintf( 'Page builder detected and set as default: %s', $detected_builder ),
1855 'wffn_page_builder_detection'
1856 );
1857 }
1858 } else {
1859 // No builder detected, set to blank if not already blank
1860 if ( ! empty( $current_default ) ) {
1861 // Update using the proper settings method
1862 $general_settings->update_global_settings_fields( array( 'default_selected_builder' => '' ) );
1863
1864 // Log the reset for debugging
1865 BWF_Logger::get_instance()->log(
1866 'No page builder detected, default set to blank',
1867 'wffn_page_builder_detection'
1868 );
1869 }
1870 }
1871
1872 return $detected_builder;
1873 } catch ( \Throwable $e ) {
1874 BWF_Logger::get_instance()->log(
1875 sprintf( 'Error detecting page builder: %s', $e->getMessage() ),
1876 'wffn_page_builder_detection'
1877 );
1878 return null;
1879 }
1880 }
1881
1882 /**
1883 * Get the currently detected/default page builder
1884 *
1885 * @return string The page builder slug or empty string if none set
1886 */
1887 public function get_detected_page_builder() {
1888 try {
1889 $general_settings = BWF_Admin_General_Settings::get_instance();
1890 $default_builder = $general_settings->get_option( 'default_selected_builder' );
1891
1892 // If no default is set, try to detect one
1893
1894 $detected_builder = $this->detect_and_set_default_page_builder( true );
1895 $default_builder = ( $detected_builder !== null ) ? $detected_builder : '';
1896
1897 // Ensure we always return a string
1898 return ( is_string( $default_builder ) ) ? $default_builder : '';
1899 } catch ( \Throwable $e ) {
1900 BWF_Logger::get_instance()->log(
1901 sprintf( 'Error getting detected page builder: %s', $e->getMessage() ),
1902 'wffn_page_builder_detection'
1903 );
1904 return '';
1905 }
1906 }
1907
1908
1909
1910 /**
1911 * Keep the menu open when editing the flows.
1912 * Highlights the wanted admin (sub-) menu items for the CPT.
1913 *
1914 * @since 1.0.0
1915 */
1916 public function menu_highlight() {
1917 global $submenu_file;
1918 $get_ref = filter_input( INPUT_GET, 'funnel_id' );
1919 if ( ! empty( $get_ref ) && absint( $get_ref ) === WFFN_Common::get_store_checkout_id() ) {
1920 $submenu_file = 'admin.php?page=bwf&path=/store-checkout'; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1921 } elseif ( $get_ref ) {
1922 $submenu_file = 'admin.php?page=bwf&path=/funnels'; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1923 }
1924 }
1925
1926 /**
1927 * @param $query WP_Query
1928 */
1929 public function load_page_to_home_page( $query ) {
1930 if ( $query->is_main_query() && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
1931
1932 $post_type = $query->get( 'post_type' );
1933
1934 $page_id = $query->get( 'page_id' );
1935
1936 if ( empty( $post_type ) && ! empty( $page_id ) ) {
1937 $t_post = get_post( $page_id );
1938 if ( $t_post instanceof \WP_Post && in_array( $t_post->post_type, array( WFFN_Core()->landing_pages->get_post_type_slug(), WFOPP_Core()->optin_pages->get_post_type_slug() ), true ) ) {
1939 $query->set( 'post_type', get_post_type( $page_id ) );
1940 }
1941 }
1942 }
1943 }
1944
1945 public function check_db_version() {
1946
1947 include_once plugin_dir_path( WFFN_PLUGIN_FILE ) . 'admin/db/class-wffn-db-tables.php';
1948 $tables = WFFN_DB_Tables::get_instance();
1949 $tables->define_tables();
1950 $tables->add_if_needed();
1951 }
1952
1953 /**
1954 * @hooked over `admin_init`
1955 * This method takes care of database updating process.
1956 * Checks whether there is a need to update the database
1957 * Iterates over define callbacks and passes it to background updater class
1958 */
1959 public function maybe_update_database_update() {
1960
1961 try {
1962 $task_list = array(
1963 '3.3.1' => array( 'wffn_handle_store_checkout_config' ),
1964 '3.3.3' => array( 'wffn_alter_conversion_table' ),
1965 '3.3.4' => array( 'wffn_add_utm_columns_in_conversion_table' ),
1966 '3.3.5' => array( 'wffn_update_migrate_data_for_currency_switcher' ),
1967 '3.3.6' => array( 'wffn_alter_conversion_table_add_source' ),
1968 '3.3.7' => array( 'wffn_update_email_default_settings' ),
1969 '3.3.8' => array( 'wffn_set_default_value_in_autoload_option' ),
1970 '3.3.9' => array( 'wffn_cleanup_data_for_conversion' ),
1971 '3.4.0' => array( 'wffn_detect_and_set_default_page_builder' ),
1972 '4.0.0' => array( 'wffn_add_order_number_column_in_conversion_table' ),
1973
1974 );
1975
1976 $task_list_for_new_install = array(
1977 'wffn_update_email_default_settings',
1978 'wffn_set_default_value_in_autoload_option',
1979 'wffn_detect_and_set_default_page_builder',
1980 );
1981 $current_db_version = get_option( '_wffn_db_version', '0.0.0' );
1982
1983 /**
1984 * 1. Fresh customer with no DB data. -
1985 * - no task should run
1986 * - direct update
1987 * 2. Existing customer with db version less than current with task.
1988 * - remaining tasks should run
1989 * - update
1990 * 3. Existing customer with db version less than current but no task.
1991 * - no tasks should run
1992 * - update
1993 * 4. db version is update with current version.
1994 * - return
1995 * 5. db version is more than the current version.
1996 * - return
1997 */
1998
1999 /**
2000 * if the current db version is greater than or equal to the current version then no need to update the database
2001 * case 4 and 5
2002 */
2003 if ( version_compare( $current_db_version, WFFN_DB_VERSION, '>=' ) ) {
2004 return;
2005 }
2006
2007 /**
2008 * if the current db version is 0.0.0
2009 * case 1
2010 */
2011 if ( $current_db_version === '0.0.0' ) {
2012 foreach ( $task_list_for_new_install as $task ) {
2013 call_user_func( $task );
2014 }
2015 update_option( '_wffn_db_version', WFFN_DB_VERSION, true );
2016
2017 return;
2018 }
2019
2020 if ( ! empty( $task_list ) ) {
2021 foreach ( $task_list as $version => $tasks ) {
2022 if ( version_compare( $current_db_version, $version, '<' ) ) {
2023 foreach ( $tasks as $update_callback ) {
2024
2025 call_user_func( $update_callback );
2026 update_option( '_wffn_db_version', $version, true );
2027 $current_db_version = $version;
2028 }
2029 }
2030 }
2031
2032 /**
2033 * If we do not have any task for the specific DB version then directly update option
2034 */
2035 if ( version_compare( $current_db_version, WFFN_DB_VERSION, '<' ) ) {
2036 update_option( '_wffn_db_version', WFFN_DB_VERSION, true );
2037 }
2038
2039 $this->clear_endpoints_from_cache();
2040
2041 }
2042 } catch ( Exception |Error $e ) {
2043 BWF_Logger::get_instance()->log( $e->getMessage(), 'wffn-db-update-exception', 'buildwoofunnels', true );
2044 }
2045 }
2046
2047 public function settings_config( $config ) {
2048 /**
2049 * The free plugin contributes only the upgrade panel. License fields are
2050 * premium-only -- the license classes ship with the premium plugin, which
2051 * registers them on this same filter at a later priority and drops the
2052 * panel below.
2053 */
2054 $fields = array();
2055
2056 $fields[] = array(
2057 'type' => 'label',
2058 'key' => 'label_pro_control',
2059 'label' => __( 'FunnelKit Funnel Builder Pro', 'funnel-builder' ),
2060 'styleClasses' => array( 'wfacp_setting_track_and_events_start', 'bwf_wrap_custom_html_tracking_general' ),
2061 );
2062 array_unshift(
2063 $fields,
2064 array(
2065 'key' => 'pro_control',
2066 'type' => 'upgrade_pro',
2067 'label' => __( '<strong>You are currently using FunnelKit Lite, which does not require a license.</strong><br/> To access more features, consider upgrading to FunnelKit PRO now.', 'funnel-builder' ),
2068 'styleClasses' => array( 'wfacp_checkbox_wrap', 'wfacp_setting_track_and_events_end' ),
2069 'hint' => '',
2070 )
2071 );
2072
2073 return array_merge( $fields, $config );
2074 }
2075
2076
2077 /**
2078 * @param $link
2079 * @param BWFABT_Experiment $experiment
2080 *
2081 * @return string
2082 */
2083 function maybe_modify_link( $link, $experiment ) {
2084
2085 $get_control_id = $experiment->get_control();
2086
2087 $get_funnel_id = get_post_meta( $get_control_id, '_bwf_in_funnel', true );
2088
2089 if ( ! empty( $get_funnel_id ) ) {
2090
2091 return WFFN_Common::get_experiment_edit_link( $get_funnel_id, $get_control_id );
2092 }
2093
2094 return $link;
2095 }
2096
2097 /*
2098 * @param $post_id
2099 * @param $all_meta
2100 *
2101 * Return selected builder based on post meta when import page
2102 * @return string[]
2103 */
2104 public function get_selected_template( $post_id, $all_meta ) {
2105 $meta = '';
2106 if ( ! empty( $all_meta ) ) {
2107 $meta = wp_list_pluck( $all_meta, 'meta_key' );
2108 }
2109
2110 $template = array(
2111 'selected' => 'wp_editor_1',
2112 'selected_type' => 'wp_editor',
2113 'template_active' => 'yes',
2114 );
2115
2116 $selected_template = apply_filters( 'wffn_set_selected_template_on_duplicate', array(), $post_id, $meta );
2117
2118 if ( is_array( $selected_template ) && count( $selected_template ) > 0 ) {
2119 return $selected_template;
2120 }
2121
2122 if ( is_array( $meta ) ) {
2123 if ( in_array( '_elementor_data', $meta, true ) ) {
2124 $template['selected'] = 'elementor_1';
2125 $template['selected_type'] = 'elementor';
2126
2127 return $template;
2128 }
2129 if ( in_array( '_et_builder_version', $meta, true ) ) {
2130 $template['selected'] = 'divi_1';
2131 $template['selected_type'] = 'divi';
2132
2133 return $template;
2134 }
2135 if ( in_array( 'ct_builder_shortcodes', $meta, true ) ) {
2136 $template['selected'] = 'oxy_1';
2137 $template['selected_type'] = 'oxy';
2138
2139 return $template;
2140 }
2141 }
2142
2143 if ( false !== strpos( get_post_field( 'post_content', $post_id ), '<!-- wp:' ) ) {
2144 $template['selected'] = 'gutenberg_1';
2145 $template['selected_type'] = 'gutenberg';
2146
2147 return $template;
2148 }
2149
2150 return $template;
2151 }
2152
2153 public function get_pro_link() {
2154 return esc_url( 'https://funnelkit.com/funnel-builder-lite-upgrade/' );
2155 }
2156
2157 /**
2158 * @param $post_id
2159 * @param $post
2160 *
2161 * hooked over `before_delete_post`
2162 * Checks if funnel step delete, then update associated funnel step meta
2163 *
2164 * @return void
2165 */
2166 public function delete_funnel_step_permanently( $post_id, $post ) {
2167
2168 if ( is_null( $post ) ) {
2169 return;
2170 }
2171
2172 if ( ! in_array(
2173 $post->post_type,
2174 array(
2175 'wfacp_checkout',
2176 'wffn_landing',
2177 'wffn_ty',
2178 'wffn_optin',
2179 'wffn_oty',
2180 ),
2181 true
2182 ) ) {
2183 return;
2184 }
2185
2186 $get_funnel_id = get_post_meta( $post_id, '_bwf_in_funnel', true );
2187
2188 if ( empty( $get_funnel_id ) ) {
2189 return;
2190 }
2191
2192 $funnel = new WFFN_Funnel( $get_funnel_id );
2193
2194 if ( $funnel instanceof WFFN_Funnel ) {
2195 $funnel->delete_step( $get_funnel_id, $post_id );
2196 }
2197 }
2198
2199 /**
2200 * @param $steps
2201 * @param $funnel
2202 *
2203 * Removed step if not exists on funnel steps listing
2204 *
2205 * @return mixed
2206 */
2207 public function maybe_delete_funnel_step( $steps, $funnel ) {
2208
2209 if ( ! $funnel instanceof WFFN_Funnel ) {
2210 return $steps;
2211 }
2212 if ( is_array( $steps ) && count( $steps ) > 0 ) {
2213 foreach ( $steps as $key => &$step ) {
2214
2215 /**
2216 * Skip if store funnel have native checkout
2217 */
2218 if ( absint( $funnel->get_id() ) === WFFN_Common::get_store_checkout_id() && WFFN_Common::store_native_checkout_slug() === $step['type'] ) {
2219 continue;
2220 }
2221
2222 /**
2223 * IF current step post not exist, then remove this step from funnel meta
2224 */
2225 if ( 0 <= $step['id'] && ! get_post( $step['id'] ) instanceof WP_Post ) {
2226 unset( $steps[ $key ] );
2227 $funnel->delete_step( $funnel->get_id(), $step['id'] );
2228 }
2229 }
2230 }
2231
2232 return $steps;
2233 }
2234
2235 /**
2236 * Add a toolbar menu funnelkit and all submenu
2237 *
2238 * @param WP_Admin_Bar $wp_admin_bar
2239 *
2240 * @return void
2241 * @throws Exception
2242 */
2243 public function add_menu_in_admin_bar( WP_Admin_Bar $wp_admin_bar ) {
2244 $user = WFFN_Role_Capability::get_instance()->user_access( 'menu', 'read' );
2245 if ( ! $user ) {
2246 return;
2247 }
2248 global $post;
2249 $wp_admin_bar->add_node(
2250 array(
2251 'id' => 'wffn_funnel',
2252 'title' => 'FunnelKit',
2253 'href' => admin_url( 'admin.php?page=bwf&path=/funnels' ),
2254 )
2255 );
2256 $admin_sub_nodes = array(
2257 array(
2258 'id' => 'fk_funnel',
2259 'parent' => 'wffn_funnel',
2260 'title' => __( 'Funnels', 'funnel-builder' ),
2261 'href' => admin_url( 'admin.php?page=bwf&path=/funnels' ),
2262 ),
2263 array(
2264 'id' => 'fk_funnel_store_checkout',
2265 'parent' => 'wffn_funnel',
2266 'title' => __( 'Store Checkout', 'funnel-builder' ),
2267 'href' => admin_url( 'admin.php?page=bwf&path=/store-checkout' ),
2268 ),
2269 array(
2270 'id' => 'fk_funnel_analytics',
2271 'parent' => 'wffn_funnel',
2272 'title' => __( 'Analytics', 'funnel-builder' ),
2273 'href' => admin_url( 'admin.php?page=bwf&path=/analytics' ),
2274 ),
2275 array(
2276 'id' => 'fk_funnel_templates',
2277 'parent' => 'wffn_funnel',
2278 'title' => __( 'Templates', 'funnel-builder' ),
2279 'href' => admin_url( 'admin.php?page=bwf&path=/templates' ),
2280 ),
2281 array(
2282 'id' => 'fk_funnel_settings',
2283 'parent' => 'wffn_funnel',
2284 'title' => __( 'Settings', 'funnel-builder' ),
2285 'href' => admin_url( 'admin.php?page=bwf&path=/settings' ),
2286 ),
2287
2288 );
2289
2290 $wffn_steps = array(
2291 'wffn_landing' => array(
2292 'slug' => 'funnel-landing',
2293 'title' => __( 'Edit Sales', 'funnel-builder' ),
2294 ),
2295 'wfacp_checkout' => array(
2296 'slug' => 'funnel-checkout',
2297 'title' => __( 'Edit Checkout', 'funnel-builder' ),
2298 ),
2299 'wfocu_offer' => array(
2300 'slug' => 'funnel-offer',
2301 'title' => __( 'Edit Offer', 'funnel-builder' ),
2302 ),
2303 'wffn_ty' => array(
2304 'slug' => 'funnel-thankyou',
2305 'title' => __( 'Edit Thank You', 'funnel-builder' ),
2306 ),
2307 'wffn_optin' => array(
2308 'slug' => 'funnel-optin',
2309 'title' => __( 'Edit Optin', 'funnel-builder' ),
2310 ),
2311 'wffn_oty' => array(
2312 'slug' => 'funnel-optin-confirmation',
2313 'title' => __( 'Edit Optin Confirmation', 'funnel-builder' ),
2314 ),
2315 );
2316 if ( ! is_null( $post ) && ! empty( $post->post_type ) && isset( $wffn_steps[ $post->post_type ] ) ) {
2317 $step = $wffn_steps[ $post->post_type ];
2318
2319 if ( 'wfocu_offer' === $post->post_type ) {
2320 $upsell_id = get_post_meta( $post->ID, '_funnel_id', true );
2321 $funnel_id = get_post_meta( $upsell_id, '_bwf_in_funnel', true );
2322 } else {
2323 $funnel_id = get_post_meta( $post->ID, '_bwf_in_funnel', true );
2324 }
2325 if ( ! empty( $funnel_id ) && abs( $funnel_id ) !== 0 ) {
2326 $is_store_checkout = ( method_exists( 'WFFN_Common', 'get_store_checkout_id' ) && absint( $funnel_id ) === WFFN_Common::get_store_checkout_id() );
2327
2328 if ( $is_store_checkout ) {
2329 $store_checkout_map = array(
2330 'funnel-checkout' => 'checkout',
2331 'funnel-offer' => 'offer',
2332 'funnel-thankyou' => 'thankyou',
2333 );
2334 $step_type = isset( $store_checkout_map[ $step['slug'] ] ) ? $store_checkout_map[ $step['slug'] ] : null;
2335
2336 if ( null !== $step_type ) {
2337 $funnel_link = BWF_Admin_Breadcrumbs::maybe_add_refs(
2338 add_query_arg(
2339 array(
2340 'page' => 'bwf',
2341 'path' => '/store-checkout',
2342 ),
2343 admin_url( 'admin.php' )
2344 )
2345 );
2346 $step_link = BWF_Admin_Breadcrumbs::maybe_add_refs(
2347 add_query_arg(
2348 array(
2349 'page' => 'bwf',
2350 'path' => '/store-checkout/' . $step_type . '/' . $post->ID . '/design',
2351 ),
2352 admin_url( 'admin.php' )
2353 )
2354 );
2355 $funnel_title = __( 'Edit Store Checkout', 'funnel-builder' );
2356 } else {
2357 $is_store_checkout = false;
2358 }
2359 }
2360
2361 if ( ! $is_store_checkout ) {
2362 $funnel_link = BWF_Admin_Breadcrumbs::maybe_add_refs(
2363 add_query_arg(
2364 array(
2365 'page' => 'bwf',
2366 'path' => '/funnels/' . $funnel_id . '/steps',
2367 ),
2368 admin_url( 'admin.php' )
2369 )
2370 );
2371 $step_link = BWF_Admin_Breadcrumbs::maybe_add_refs(
2372 add_query_arg(
2373 array(
2374 'page' => 'bwf',
2375 'path' => '/' . $step['slug'] . '/' . $post->ID . '/design',
2376 'funnel_id' => $funnel_id,
2377 ),
2378 admin_url( 'admin.php' )
2379 )
2380 );
2381 $funnel_title = __( 'Edit Funnel', 'funnel-builder' );
2382 }
2383
2384 /**
2385 * Add submenu for the frontend if its our step
2386 */
2387 $wp_admin_bar->add_menu(
2388 array(
2389 'id' => 'wffn_edit_funnel',
2390 'parent' => 'wffn_funnel',
2391 'title' => $funnel_title,
2392 'href' => $funnel_link,
2393 )
2394 );
2395
2396 $wp_admin_bar->add_menu(
2397 array(
2398 'id' => 'wffn_edit_step',
2399 'parent' => 'wffn_funnel',
2400 'title' => $step['title'],
2401 'href' => $step_link,
2402 )
2403 );
2404 ?>
2405 <style type="text/css">
2406 ul#wp-admin-bar-wffn_funnel-default li#wp-admin-bar-wffn_edit_step {
2407 margin-bottom: 5px;
2408 padding-bottom: 5px;
2409 border-bottom: 1px dashed #65686b;
2410 }
2411 </style>
2412 <?php
2413 }
2414 }
2415
2416 /**
2417 * Add all sub nodes which are static
2418 */
2419 foreach ( $admin_sub_nodes as $node ) {
2420 $wp_admin_bar->add_menu( $node );
2421 }
2422
2423 /**
2424 * Possibly add sub nodes for different pro states
2425 */
2426 if ( ! defined( 'WFFN_PRO_VERSION' ) ) {
2427 $link = add_query_arg(
2428 array(
2429 'utm_source' => 'WordPress',
2430 'utm_medium' => 'Toolbar+Menu',
2431 'utm_campaign' => 'FB+Lite+Plugin',
2432 ),
2433 WFFN_Core()->admin->get_pro_link()
2434 );
2435 $wp_admin_bar->add_menu(
2436 array(
2437 'id' => 'wffn_funnel-additional_lite',
2438 'parent' => 'wffn_funnel',
2439 'title' => __( 'Upgrade to Pro', 'funnel-builder' ),
2440 'href' => $link,
2441 )
2442 );
2443 ?>
2444 <style type="text/css">
2445 ul#wp-admin-bar-wffn_funnel-default li#wp-admin-bar-wffn_funnel-additional_lite a.ab-item {
2446 color: white;
2447 background-color: #1DA867;
2448 }
2449 </style>
2450 <?php
2451 }
2452 }
2453
2454 /**
2455 * @param $title
2456 * set default post title if post tile empty
2457 *
2458 * @return mixed|string|null
2459 */
2460 public function maybe_empty_title( $title ) {
2461 if ( empty( $title ) ) {
2462 return __( '(no title)', 'funnel-builder' );
2463 }
2464
2465 return $title;
2466 }
2467
2468
2469 public function wizard_url() {
2470 return admin_url( 'admin.php?page=bwf&path=/user-setup' );
2471 }
2472
2473 /**
2474 * Filter CB to alter response data to pass connect link to the REST cll
2475 *
2476 * @param array $response
2477 * @param string $basename
2478 *
2479 * @return mixed
2480 */
2481 public function maybe_add_auth_link_stripe( $response, $basename ) {
2482 if ( 'funnelkit-stripe-woo-payment-gateway/funnelkit-stripe-woo-payment-gateway.php' === $basename ) {
2483 $response['next_action'] = 'funnelkit-app/stripe-connect-link';
2484 }
2485
2486 return $response;
2487 }
2488
2489 public function maybe_add_auth_link_square( $response, $basename ) {
2490 if ( 'funnelkit-payment-gateway-square-for-woocommerce/funnelkit-square.php' === $basename ) {
2491 $response['next_action'] = 'funnelkit-app/square-connect-link';
2492 }
2493
2494 return $response;
2495 }
2496
2497
2498 /**
2499 * Expiry date of the premium license, empty when there is none.
2500 *
2501 * Kept as a seam: the premium plugin calls this directly and supplies the
2502 * value on the filter, since the free plugin ships no license data.
2503 *
2504 * @return string
2505 */
2506 public function get_license_expiry() {
2507 return apply_filters( 'wffn_license_expiry', '' );
2508 }
2509 public function get_license_basename_sha1() {
2510 return array(
2511 'checkout' => '742fc61c1b455e2b1efa4154a92da8fb7f9866d3',
2512 'upsell' => 'e837ebc716ca979006da34eecdce9f650ced6bef',
2513 'pro' => 'ffec4bb68f0841db41213ce12305aaef7e0237f3',
2514 'basic' => 'e234ca9ec3e4856bb05ea9f8ec90e7f3831b05c5',
2515
2516 );
2517 }
2518
2519 public function blocks_incompatible_switch_to_classic_cart_checkout( $is_rest = false ) {
2520
2521 if ( ! class_exists( '\Automattic\WooCommerce\Blocks\BlockTypes\ClassicShortcode' ) || // Make sure WC version is at least 8.3. This class is added at version 8.3.
2522 ! current_user_can( 'manage_options' ) || ( empty( $is_rest ) && false === check_ajax_referer( 'wffn_blocks_incompatible_switch_to_classic', 'nonce', false ) ) ) {
2523 return;
2524 }
2525
2526 $wc_cart_page = get_post( wc_get_page_id( 'cart' ) );
2527 $wc_checkout_page = get_post( wc_get_page_id( 'checkout' ) );
2528
2529 if ( has_block( 'woocommerce/checkout', $wc_checkout_page ) ) {
2530 wp_update_post(
2531 array(
2532 'ID' => $wc_checkout_page->ID,
2533 'post_content' => '<!-- wp:woocommerce/classic-shortcode {"shortcode":"checkout"} /-->',
2534 )
2535 );
2536 }
2537
2538 if ( has_block( 'woocommerce/cart', $wc_cart_page ) ) {
2539 wp_update_post(
2540 array(
2541 'ID' => $wc_cart_page->ID,
2542 'post_content' => '<!-- wp:woocommerce/classic-shortcode {"shortcode":"cart"} /-->',
2543 )
2544 );
2545 }
2546
2547 $userdata = get_user_meta( get_current_user_id(), '_bwf_notifications_close', true );
2548 $userdata = empty( $userdata ) && ! is_array( $userdata ) ? array() : $userdata;
2549 $userdata[] = 'wc_block_incompat';
2550 update_user_meta( get_current_user_id(), '_bwf_notifications_close', array_values( array_unique( $userdata ) ) ); //phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.user_meta_update_user_meta
2551
2552 if ( ! empty( $is_rest ) ) {
2553 return rest_ensure_response( array( 'success' => true ) );
2554
2555 }
2556 $redirect = isset( $_REQUEST['redirect'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect'] ) ) : null;
2557 $redirect_url = $redirect && strpos( $redirect, '.php' ) ? admin_url( $redirect ) : null;
2558
2559 wp_safe_redirect( $redirect_url ?? admin_url( 'admin.php?page=bwf&path=/funnels' ) );
2560 exit;
2561 }
2562
2563 /**
2564 * AJAX dismiss admin notice.
2565 *
2566 * @since 1.1
2567 * @since 4.5.1 Add nonce verification when dismissing notices.
2568 * @access public
2569 */
2570 public function ajax_dismiss_admin_notice() {
2571 $notice_key = isset( $_REQUEST['nkey'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nkey'] ) ) : '';
2572
2573 if (
2574 defined( 'DOING_AJAX' ) && DOING_AJAX && current_user_can( 'manage_options' ) && $notice_key && isset( $_REQUEST['nonce'] ) && false !== check_ajax_referer( 'wp_wffn_dismiss_notice', 'nonce', false )
2575
2576 ) {
2577
2578 $userdata = get_user_meta( get_current_user_id(), '_bwf_notifications_close', true );
2579 $userdata = empty( $userdata ) && ! is_array( $userdata ) ? array() : $userdata;
2580 $userdata[] = $notice_key;
2581
2582 update_user_meta( get_current_user_id(), '_bwf_notifications_close', array_values( array_unique( $userdata ) ) ); //phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.user_meta_update_user_meta
2583 // If primary sticky banner is dismissed, set 5-minute transient to hide admin notifications
2584 // Secondary sticky banner does NOT trigger this transient - it will always show until dismissed
2585 if ( strpos( $notice_key, 'sticky_' ) === 0 && strpos( $notice_key, 'secondary_sticky_' ) !== 0 ) {
2586 set_transient( 'wffn_sticky_banner_dismissed_' . get_current_user_id(), $notice_key, 5 * MINUTE_IN_SECONDS );
2587 }
2588 }
2589 $redirect = isset( $_REQUEST['redirect'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect'] ) ) : null;
2590
2591 $redirect_url = $redirect && strpos( $redirect, '.php' ) ? admin_url( $redirect ) : null;
2592
2593 wp_safe_redirect( $redirect_url ?? admin_url( 'admin.php?page=bwf&path=/funnels' ) );
2594 exit;
2595 }
2596
2597 public function get_pro_activation_date() {
2598 if ( ! defined( 'WFFN_PRO_VERSION' ) ) {
2599 return '';
2600 }
2601 $pro_activation_date = get_option( 'fk_fb_active_date', array() );
2602 if ( empty( $pro_activation_date ) || ! isset( $pro_activation_date['pro'] ) ) {
2603 $date = new DateTime( 'now' );
2604 $date->modify( '-10 days' );
2605 $pro_activation_date['pro'] = $date->getTimestamp();
2606 update_option( 'fk_fb_active_date', $pro_activation_date, false );
2607
2608 return $date->format( 'Y-m-d H:i:s' );
2609 }
2610
2611 return gmdate( 'Y-m-d H:i:s', $pro_activation_date['pro'] );
2612 }
2613
2614 public function get_lite_activation_date() {
2615
2616 $pro_activation_date = get_option( 'fk_fb_active_date', array() );
2617 if ( empty( $pro_activation_date ) || ! isset( $pro_activation_date['lite'] ) ) {
2618 $date = new DateTime( 'now' );
2619 $pro_activation_date['lite'] = $date->getTimestamp();
2620 update_option( 'fk_fb_active_date', $pro_activation_date, false );
2621
2622 return $date->format( 'Y-m-d H:i:s' );
2623 }
2624
2625 return gmdate( 'Y-m-d H:i:s', $pro_activation_date['lite'] );
2626 }
2627
2628
2629 /**
2630 * Get license related configuration to spread over all product to handle all features
2631 *
2632 * @param boolean $expiry_only true if only expiry related info need, else all info
2633 *
2634 * @return array a complete array for every product to handle license related info
2635 */
2636 /**
2637 * License state consumed by this plugin, its React app and companion
2638 * plugins (upsells, order bumps, cart).
2639 *
2640 * The free plugin holds no license data of its own; the premium plugin
2641 * supplies the real payload on the wffn_license_config filter. The method
2642 * itself stays because it is a cross-plugin contract that callers probe
2643 * with method_exists().
2644 *
2645 * The default returned here is just a static array kept for backward
2646 * compatibility with the premium plugin; it carries a future expiry date
2647 * so all gated features stay unlocked. It is not real license state.
2648 *
2649 * The default handed to the filter is deliberately fail-open. A premium
2650 * build that predates this filter never answers it, so the default is
2651 * what its front-end gates (WFFN_Step_WC_Upsells / WFFN_Substep_WC_Order_Bump /
2652 * Cart Pro) see. An empty array would resolve to 'pro_without_license' in
2653 * their get_current_app_state() and silently suppress every upsell, order
2654 * bump and cart offer for a genuinely licensed store. So the default
2655 * instead reports an active license (la === true) with a far-future
2656 * expiry, keeping the older premium side working across the version-skew
2657 * window; the moment premium is updated it overrides this default wholesale
2658 * (its filter callback ignores the incoming value). On a free-only install
2659 * nothing consumes this, so the default is inert there.
2660 *
2661 * @param bool $expiry_only Return only expiry information.
2662 * @param bool $get_ad Include activation dates.
2663 *
2664 * @return array
2665 */
2666 public function get_license_config( $expiry_only = false, $get_ad = true ) {
2667 /**
2668 * Static backward-compatibility default for the premium plugin.
2669 *
2670 * This array exists purely so older premium builds (which do not answer
2671 * the wffn_license_config filter) keep working. It is a fixed shape, not
2672 * real license data -- the free plugin has none. The expiry date ('ed')
2673 * is set to a future date on purpose so every gated feature (upsells,
2674 * order bumps, cart) stays unlocked; a past/empty value would read as an
2675 * expired/absent license and suppress those offers.
2676 */
2677 $far_future = gmdate( 'Y-m-d H:i:s', strtotime( '+10 years' ) ); // Future expiry -> keeps all features unlocked for older premium builds.
2678
2679 if ( $expiry_only ) {
2680 $default = array(
2681 'f' => array( 'ed' => $far_future ),
2682 'ck' => array( 'ed' => $far_future ),
2683 'ul' => array( 'ed' => $far_future ),
2684 'gp' => array( 2, 2 ),
2685 );
2686 } else {
2687 $default = array(
2688 'f' => array(
2689 'la' => true,
2690 'ed' => $far_future,
2691 'e' => true,
2692 ),
2693 'ck' => array(
2694 'la' => true,
2695 'ed' => $far_future,
2696 'e' => true,
2697 ),
2698 'ul' => array(
2699 'la' => true,
2700 'ed' => $far_future,
2701 'e' => true,
2702 ),
2703 'gp' => array( 2, 2 ),
2704 );
2705 }
2706
2707 return apply_filters( 'wffn_license_config', $default, $expiry_only, $get_ad );
2708 }
2709
2710 /**
2711 * redirect checkout edit link on react screen when click on edit link from wc order screen
2712 *
2713 * @return void
2714 */
2715 public function redirect_checkout_edit_link_on_new_ui() {
2716 $checkout_id = filter_input( INPUT_GET, 'wfacp_id', FILTER_VALIDATE_INT );
2717 if ( empty( $checkout_id ) ) {
2718 return;
2719 }
2720 $funnel_id = get_post_meta( $checkout_id, '_bwf_in_funnel', true );
2721 if ( ! empty( $funnel_id ) && abs( $funnel_id ) > 0 ) {
2722 // @codingStandardsIgnoreStart
2723 $edit_link = add_query_arg( [
2724 'page' => 'bwf',
2725 'path' => "/funnel-checkout/" . $checkout_id . "/design"
2726 ], admin_url( 'admin.php' ) );
2727 // @codingStandardsIgnoreEnd
2728 wp_redirect( $edit_link );
2729 exit;
2730 }
2731 }
2732
2733 public function maybe_force_redirect_to_wizard() {
2734
2735 if ( false === $this->is_wizard_available( true ) ) {
2736 return;
2737 }
2738
2739 wp_redirect( WFFN_Core()->admin->wizard_url() );
2740 exit;
2741 }
2742
2743
2744 /**
2745 * Helper method to detect if the wizard is available to show
2746 * Check all prior conditions before showing force wizard or notice
2747 *
2748 * @param bool $force check if it's a check for force wizard open
2749 *
2750 * @return bool true if all checks are passed, false otherwise
2751 */
2752 public function is_wizard_available( $force = false ) {
2753 try {
2754 $first_version = get_option( 'wffn_first_v', '0.0.0' );
2755 if ( $force !== false && false === version_compare( $first_version, WFFN_VERSION, '=' ) ) {
2756
2757 /**
2758 * bail out if old users versions
2759 */
2760 return false;
2761 }
2762
2763 if ( WFFN_Core()->admin_notifications->is_user_dismissed( get_current_user_id(), 'onboarding_wizard' ) ) {
2764 /**
2765 * This flag tells us that the wizard notice has already been dismissed
2766 */
2767 return false;
2768 }
2769
2770 $status = get_option( '_wffn_onboarding_completed', false );
2771 if ( false !== $status ) {
2772 /**
2773 * bail out if wizard started/skipped/completed
2774 */
2775 return false;
2776 }
2777
2778 if ( $force !== false && WFFN_Core()->admin_notifications->is_user_dismissed( get_current_user_id(), 'wizard_open' ) ) {
2779 /**
2780 * This flag tells us that the wizard first step is already opened
2781 * We are using this to make sure we never redirect multiple times.
2782 */
2783 return false;
2784 }
2785
2786 if ( false !== get_option( '_bwfan_onboarding_completed', false ) && 'activated' === WFFN_Common::get_plugin_status( 'funnelkit-stripe-woo-payment-gateway/funnelkit-stripe-woo-payment-gateway.php' ) ) {
2787 /**
2788 * This checks if user has completed wizard from automations plugin & we have stripe plugin activated
2789 */
2790 return false;
2791 }
2792
2793 return true;
2794 } catch ( Exception |Error $e ) {
2795 return false;
2796 }
2797 }
2798
2799
2800 /**
2801 * Check if update is available for the plugin lite Or PRO versions
2802 *
2803 * @return false|mixed version number if update available, false if not available
2804 */
2805 public function is_update_available( $only_pro = false ) {
2806
2807 $plugins = get_site_transient( 'update_plugins' );
2808
2809 if ( $only_pro ) {
2810 if ( defined( 'WFFN_PRO_PLUGIN_BASENAME' ) && isset( $plugins->response ) && is_array( $plugins->response ) && isset( $plugins->response[ WFFN_PRO_PLUGIN_BASENAME ] ) ) {
2811 return $this->compare_version( WFFN_PRO_VERSION, $plugins->response[ WFFN_PRO_PLUGIN_BASENAME ]->new_version );
2812 } else {
2813 return false;
2814 }
2815 } elseif ( isset( $plugins->response ) && is_array( $plugins->response ) && isset( $plugins->response[ WFFN_PLUGIN_BASENAME ] ) ) {
2816
2817 return $this->compare_version( WFFN_VERSION, $plugins->response[ WFFN_PLUGIN_BASENAME ]->new_version );
2818 } elseif ( defined( 'WFFN_PRO_PLUGIN_BASENAME' ) && isset( $plugins->response ) && is_array( $plugins->response ) && isset( $plugins->response[ WFFN_PRO_PLUGIN_BASENAME ] ) ) {
2819 return $this->compare_version( WFFN_PRO_VERSION, $plugins->response[ WFFN_PRO_PLUGIN_BASENAME ]->new_version );
2820 }
2821
2822 return false;
2823 }
2824
2825 /**
2826 * Compare version
2827 *
2828 * @param $old_version
2829 * @param $new_version
2830 *
2831 * @return false|mixed
2832 */
2833 public function compare_version( $old_version, $new_version ) {
2834
2835 if ( version_compare( $new_version, $old_version, '>' ) ) {
2836 return $new_version;
2837 } else {
2838 return false;
2839 }
2840 }
2841
2842
2843
2844
2845 /**
2846 * Clear wffn endpoints form cache plugins
2847 *
2848 * @return void
2849 */
2850 public function clear_endpoints_from_cache() {
2851 /** Cache handling */
2852 if ( class_exists( 'BWF_JSON_Cache' ) && method_exists( 'BWF_JSON_Cache', 'run_json_endpoints_cache_handling' ) ) {
2853 BWF_JSON_Cache::run_json_endpoints_cache_handling();
2854 }
2855 }
2856
2857 public function conditional_includes() {
2858
2859 $screen = get_current_screen();
2860
2861 if ( ! $screen ) {
2862 return;
2863 }
2864
2865 switch ( $screen->id ) {
2866 case 'dashboard':
2867 case 'dashboard-network':
2868 include_once __DIR__ . '/class-wffn-admin-dashboard-widget.php';
2869
2870 break;
2871
2872 case 'product':
2873 case 'edit-product_cat':
2874 require_once __DIR__ . '/class-fk-checkout-redirect-admin.php';
2875 FK_Checkout_Redirect_Admin::get_instance();
2876
2877 // Load product tab addon only on product edit screen
2878 include_once __DIR__ . '/includes/class-wffn-product-tab-addon.php';
2879 WFFN_Product_Tab_Addon::get_instance();
2880
2881 break;
2882
2883 }
2884 }
2885
2886 /**
2887 * Function run on schedule for clean up data
2888 * duplicate entry and remove subscription form conversion
2889 *
2890 * @return void
2891 */
2892 public function optimize_conversion_table_analytics() {
2893
2894 global $wpdb;
2895 try {
2896 if ( ! class_exists( 'BWF_WC_Compatibility' ) ) {
2897 return;
2898 }
2899 $conv_table = $wpdb->prefix . 'bwf_conversion_tracking';
2900 $order_table = $wpdb->prefix . 'wc_orders';
2901 $aero_table = $wpdb->prefix . 'wfacp_stats';
2902 $limit = 100;
2903 $is_clear_schedule = true;
2904 WFFN_Common::$start_time = time();
2905
2906 while ( true ) {
2907 // Stop if time or memory exceeded
2908 if ( ( WFFN_Common::time_exceeded() || WFFN_Common::memory_exceeded() ) ) {
2909 $is_clear_schedule = false;
2910 break;
2911 }
2912
2913 // Query Setup
2914 if ( BWF_WC_Compatibility::is_hpos_enabled() ) {
2915 $order_meta_table = $wpdb->prefix . 'wc_orders_meta';
2916 $conv_query = "SELECT conv.source as ID FROM {$conv_table} AS conv
2917 INNER JOIN {$order_meta_table} AS om ON (conv.source = om.order_id AND om.meta_key = '_subscription_renewal')
2918 WHERE conv.type = %s
2919 ORDER BY conv.timestamp DESC LIMIT 0, %d";//phpcs:ignore
2920
2921 } else {
2922 $conv_query = "SELECT conv.source as ID FROM {$conv_table} AS conv
2923 INNER JOIN {$wpdb->postmeta} AS pm ON (conv.source = pm.post_id AND pm.meta_key = '_subscription_renewal')
2924 WHERE conv.type = %s
2925 ORDER BY conv.timestamp DESC LIMIT 0, %d";
2926 }
2927
2928 $get_ids = $wpdb->get_col( $wpdb->prepare( $conv_query, 2, $limit ) );//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2929
2930 // Stop loop if no more data
2931 if ( empty( $get_ids ) || ! is_array( $get_ids ) ) {
2932 /*
2933 * Attempt duplicate query
2934 */
2935 $duplicate_row = $wpdb->get_col( $wpdb->prepare( "SELECT source FROM {$conv_table} WHERE type = 2 AND source != 0 GROUP BY source HAVING COUNT(*) > %d ", 1 ) );//phpcs:ignore
2936
2937 if ( ! empty( $duplicate_row ) && is_array( $duplicate_row ) ) {
2938 $is_clear_schedule = false;
2939 $delete_query = "DELETE FROM {$conv_table} WHERE id IN ( SELECT id FROM (
2940 SELECT id FROM {$conv_table} WHERE source IN (
2941 SELECT source FROM {$conv_table} WHERE type = 2 AND source != 0
2942 GROUP BY source HAVING COUNT(*) > 1 ) AND id NOT IN (
2943 SELECT MAX(id) FROM {$conv_table} GROUP BY source ) LIMIT 100 ) AS subquery )";
2944 $wpdb->query( $delete_query ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2945 WFFN_Core()->logger->log( 'Clear duplicate rows.', 'wffn_ay', true );
2946 continue;
2947
2948 } else {
2949 WFFN_Core()->logger->log( 'No more data to clear from conversion tracking.', 'wffn_ay', true );
2950 break;
2951 }
2952 }
2953
2954 $is_clear_schedule = false;
2955 $delete_query = "DELETE FROM {$conv_table} WHERE type = %d AND source IN (" . implode( ',', array_map( 'intval', $get_ids ) ) . ')';
2956 $wpdb->query( $wpdb->prepare( $delete_query, 2 ) );//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2957
2958 if ( ! empty( $wpdb->last_error ) ) {
2959 $is_clear_schedule = true;
2960 WFFN_Core()->logger->log( 'Conversion tracking cleanup last error: ' . $wpdb->last_error, 'wffn_ay', true );
2961 }
2962
2963 $update_query = "UPDATE {$conv_table} AS conv LEFT JOIN {$aero_table} AS stats ON conv.source = stats.order_id SET conv.checkout_total = stats.total_revenue
2964 WHERE 1=1 AND conv.funnel_id != 0
2965 AND conv.checkout_total = 0
2966 AND stats.total_revenue != 0
2967 AND conv.type = %d
2968 AND conv.timestamp > '2025-01-01 00:00:00'";
2969
2970 $wpdb->query( $wpdb->prepare( $update_query, 2 ) );//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2971
2972 if ( ! empty( $wpdb->last_error ) ) {
2973 $is_clear_schedule = true;
2974 WFFN_Core()->logger->log( 'Conversion tracking update last error: ' . $wpdb->last_error, 'wffn_ay', true );
2975 }
2976 }
2977
2978 if ( ( WFFN_Common::time_exceeded() || WFFN_Common::memory_exceeded() ) ) {
2979 return;
2980 }
2981
2982 /**
2983 * Remove all subscription order from aero
2984 */
2985 $aero_table = $wpdb->prefix . 'wfacp_stats';
2986
2987 if ( BWF_WC_Compatibility::is_hpos_enabled() ) {
2988 $aero_query = "SELECT ch.order_id as ID FROM {$aero_table} AS ch
2989 INNER JOIN {$order_table} AS ot ON (ch.order_id = ot.id )
2990 WHERE ot.type = %s
2991 ORDER BY ch.date DESC LIMIT 0, %d";
2992
2993 } else {
2994 $aero_query = "SELECT ch.order_id as ID FROM {$aero_table} AS ch
2995 INNER JOIN {$wpdb->posts} AS p ON (ch.order_id = p.ID)
2996 WHERE p.post_type = %s
2997 ORDER BY ch.date DESC LIMIT 0, %d";
2998 }
2999
3000 $get_aero_ids = $wpdb->get_col( $wpdb->prepare( $aero_query, 'shop_subscription', $limit ) );//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3001 if ( is_array( $get_aero_ids ) && count( $get_aero_ids ) > 0 ) {
3002 $is_clear_schedule = false;
3003 $wpdb->query( "DELETE FROM {$aero_table} WHERE order_id IN (" . implode( ',', $get_aero_ids ) . " )" );//phpcs:ignore
3004 if ( ! empty( $wpdb->last_error ) ) {
3005 $is_clear_schedule = true;
3006 WFFN_Core()->logger->log( 'Conversion aero tracking cleanup last error: ' . $wpdb->last_error, 'wffn_ay', true );
3007 }
3008 }
3009
3010 /**
3011 * Remove duplicate entry
3012 */
3013 $duplicate_query = "SELECT order_id as ID FROM {$aero_table}
3014 WHERE order_id IN ( SELECT order_id FROM {$aero_table}
3015 GROUP BY order_id
3016 HAVING COUNT(order_id) > %d
3017 )
3018 ORDER BY order_id, ID LIMIT 0, %d";
3019
3020 $duplicate_aero_ids = $wpdb->get_col( $wpdb->prepare( $duplicate_query, 1, $limit ) );//phpcs:ignore
3021
3022 if ( is_array( $duplicate_aero_ids ) && count( $duplicate_aero_ids ) > 0 ) {
3023 $is_clear_schedule = false;
3024 $delete_query = "DELETE FROM {$aero_table} WHERE ID IN (
3025 SELECT ID FROM (
3026 SELECT ID
3027 FROM {$aero_table} t1
3028 WHERE ID NOT IN (
3029 SELECT MAX(ID) FROM {$aero_table} t2 GROUP BY order_id
3030 )
3031 AND order_id IN (
3032 SELECT order_id FROM {$aero_table} GROUP BY order_id HAVING COUNT(order_id) > %d
3033 )
3034 ORDER BY ID DESC
3035 LIMIT %d
3036 ) AS subquery
3037 )";
3038 $wpdb->query( $wpdb->prepare( $delete_query, 1, $limit ) );//phpcs:ignore
3039 if ( ! empty( $wpdb->last_error ) ) {
3040 $is_clear_schedule = true;
3041 WFFN_Core()->logger->log( 'Conversion duplicate tracking cleanup last error: ' . $wpdb->last_error, 'wffn_ay', true );
3042 }
3043 }
3044
3045 /**
3046 * Remove all subscription order from bump
3047 */
3048 if ( class_exists( 'WFOB_Core' ) ) {
3049 $bump_table = $wpdb->prefix . 'wfob_stats';
3050 if ( BWF_WC_Compatibility::is_hpos_enabled() ) {
3051 $bump_query = "SELECT ch.oid as ID FROM {$bump_table} AS ch
3052 INNER JOIN {$order_table} AS ot ON (ch.oid = ot.id )
3053 WHERE ot.type = %s
3054 ORDER BY ch.date DESC LIMIT 0, %d";
3055
3056 } else {
3057 $bump_query = "SELECT ch.oid as ID FROM {$bump_table} AS ch
3058 INNER JOIN {$wpdb->posts} AS p ON (ch.oid = p.ID)
3059 WHERE p.post_type = %s
3060 ORDER BY ch.date DESC LIMIT 0, %d";
3061 }
3062
3063 $get_bump_ids = $wpdb->get_col( $wpdb->prepare( $bump_query, 'shop_subscription', $limit ) );//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3064 if ( is_array( $get_bump_ids ) && count( $get_bump_ids ) > 0 ) {
3065 $is_clear_schedule = false;
3066 $wpdb->query( "DELETE FROM {$bump_table} WHERE oid IN (" . implode( ',', $get_bump_ids ) . " )" );//phpcs:ignore
3067 if ( ! empty( $wpdb->last_error ) ) {
3068 $is_clear_schedule = true;
3069 WFFN_Core()->logger->log( 'Conversion bump tracking cleanup last error: ' . $wpdb->last_error, 'wffn_ay', true );
3070 }
3071 }
3072 }
3073
3074 // If no remaining rows, schedule a single action to remove the recurring action
3075 if ( $is_clear_schedule ) {
3076 wp_schedule_single_event( time(), 'fk_remove_optimize_conversion_table_schedule' );
3077 }
3078 } catch ( Exception |Error $e ) {
3079 // Schedule a single action to delete the recurring action
3080 wp_schedule_single_event( time(), 'fk_remove_optimize_conversion_table_schedule' );
3081 WFFN_Core()->logger->log( 'Exception occurred in : ' . __FUNCTION__ . $e->getMessage(), 'wffn_ay', true );
3082
3083 }
3084 }
3085
3086 public function remove_optimize_conversion_table_schedule() {
3087 // Clear the scheduled action
3088 wp_clear_scheduled_hook( 'fk_optimize_conversion_table_analytics' );
3089 WFFN_Core()->logger->log( 'Recurring action "fk_optimize_conversion_table_analytics" cleared : ' . __FUNCTION__, 'wffn_ay', true );
3090 }
3091
3092 /**
3093 * Runs the email notifications.
3094 *
3095 * @return void
3096 */
3097 public function run_notifications() {
3098 WFFN_Email_Notification::run_notifications();
3099 }
3100
3101 /**
3102 * Runs the email notifications.
3103 *
3104 * @return void
3105 */
3106 public function maybe_setup_notification_schedule() {
3107 WFFN_Email_Notification::maybe_setup_notification_schedule();
3108 }
3109
3110 /**
3111 * Tests the email notification in the admin area.
3112 *
3113 * @return void
3114 */
3115 public function test_notification_admin() {
3116
3117 WFFN_Email_Notification::test_notification_admin();
3118 }
3119
3120 /**
3121 * save settings for email notification
3122 **/
3123 public function save_settings_for_email_notification( $settings ) {
3124 WFFN_Email_Notification::save_settings( $settings );
3125 }
3126
3127
3128 /**
3129 * Check if language support is enabled
3130 *
3131 * @return bool
3132 */
3133 public function is_language_support_enabled() {
3134 return '' !== WFFN_Plugin_Compatibilities::get_language_compatible_plugin();
3135 }
3136
3137 /**
3138 * Whether the installed Funnel Builder Pro is older than the build this
3139 * release pairs with (WFFN_MIN_PRO_VERSION).
3140 *
3141 * @return bool
3142 */
3143 public function is_outdated() {
3144 if ( ! defined( 'WFFN_PRO_VERSION' ) || ! defined( 'WFFN_MIN_PRO_VERSION' ) ) {
3145 return false;
3146 }
3147
3148 /** Source checkouts carry an unstamped build placeholder, not a comparable version. */
3149 if ( false !== strpos( WFFN_PRO_VERSION, '{{{' ) ) {
3150 return false;
3151 }
3152
3153 return version_compare( WFFN_PRO_VERSION, WFFN_MIN_PRO_VERSION, '<' );
3154 }
3155
3156 /**
3157 * Name of the installed premium companion plugin the pairing applies to.
3158 *
3159 * @return string
3160 */
3161 public function get_companion_name() {
3162 return defined( 'WFFN_BASIC_FILE' )
3163 ? __( 'FunnelKit Funnel Builder Basic', 'funnel-builder' )
3164 : __( 'FunnelKit Funnel Builder Pro', 'funnel-builder' );
3165 }
3166
3167 /**
3168 * Companion plugin name without the brand prefix, for tight spaces.
3169 *
3170 * @return string
3171 */
3172 public function get_companion_short_name() {
3173 return defined( 'WFFN_BASIC_FILE' )
3174 ? __( 'Funnel Builder Basic', 'funnel-builder' )
3175 : __( 'Funnel Builder Pro', 'funnel-builder' );
3176 }
3177
3178 /**
3179 * Adds a pairing-requirement row under the free Funnel Builder plugin on
3180 * the plugins page. A separate row on its own hook — the core update rows
3181 * (both plugins') stay untouched. Registered on admin_init so it also
3182 * covers the AJAX search rerender, where load-plugins.php never fires.
3183 *
3184 * @return void
3185 */
3186 public function maybe_register_update_row_message() {
3187 if ( ! $this->is_outdated() || ! defined( 'WFFN_PLUGIN_BASENAME' ) ) {
3188 return;
3189 }
3190
3191 add_action( 'in_plugin_update_message-' . WFFN_PLUGIN_BASENAME, array( $this, 'render_in_core_update_row' ) );
3192 add_action( 'after_plugin_row_' . WFFN_PLUGIN_BASENAME, array( $this, 'render_update_row' ), 11, 2 );
3193 }
3194
3195 /**
3196 * Appended inside the core "There is a new version…" row when the free
3197 * plugin itself has an update on offer.
3198 *
3199 * @return void
3200 */
3201 public function render_in_core_update_row() {
3202 $this->render_pairing_warning_block( true );
3203 }
3204
3205 /**
3206 * The warning block shared by both plugins-page placements.
3207 *
3208 * @param bool $with_separator Whether a core message precedes the block.
3209 *
3210 * @return void
3211 */
3212 private function render_pairing_warning_block( $with_separator ) {
3213 $update_link = '';
3214 if ( defined( 'WFFN_PRO_FILE' ) && current_user_can( 'update_plugins' ) ) {
3215 $pro_file = plugin_basename( WFFN_PRO_FILE );
3216 $updates = get_site_transient( 'update_plugins' );
3217 if ( ! empty( $updates->response[ $pro_file ]->package ) ) {
3218 $update_link = sprintf(
3219 ' <a href="%1$s" aria-label="%2$s">%3$s</a>',
3220 esc_url( wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $pro_file, 'upgrade-plugin_' . $pro_file ) ),
3221 esc_attr( sprintf( /* translators: %s: premium plugin name */ __( 'Update %s now', 'funnel-builder' ), $this->get_companion_name() ) ),
3222 esc_html__( 'Update now', 'funnel-builder' )
3223 );
3224 }
3225 }
3226
3227 if ( $with_separator ) {
3228 ?>
3229 <hr class="wffn-pairing-warning__separator" />
3230 <?php
3231 }
3232 ?>
3233 <div class="wffn-pairing-warning">
3234 <div class="wffn-pairing-warning__icon">
3235 <span class="dashicons dashicons-info" aria-hidden="true"></span>
3236 </div>
3237 <div>
3238 <div class="wffn-pairing-warning__title">
3239 <?php
3240 printf( /* translators: %s: premium plugin name */
3241 esc_html__( 'Update Required: %s', 'funnel-builder' ),
3242 esc_html( $this->get_companion_name() )
3243 );
3244 ?>
3245 </div>
3246 <div class="wffn-pairing-warning__message">
3247 <?php
3248 printf( /* translators: 1: premium plugin name, 2: required version, 3: installed version */
3249 esc_html__( 'This version of Funnel Builder pairs with %1$s %2$s or newer, but %3$s is installed.', 'funnel-builder' ),
3250 esc_html( $this->get_companion_name() ),
3251 esc_html( WFFN_MIN_PRO_VERSION ),
3252 esc_html( WFFN_PRO_VERSION )
3253 );
3254 echo wp_kses_post( $update_link );
3255 ?>
3256 </div>
3257 </div>
3258 </div>
3259 <style>
3260 /* Inline so the block also survives the AJAX search rerender of the list table. */
3261 .wffn-pairing-warning {
3262 display: flex;
3263 max-width: 1000px;
3264 margin-block-end: 5px;
3265 }
3266
3267 .wffn-pairing-warning__separator {
3268 margin: 15px -12px;
3269 border: 1px solid #ffb900;
3270 border-bottom: none;
3271 }
3272
3273 .wffn-pairing-warning__icon {
3274 margin-inline-end: 9px;
3275 margin-inline-start: 2px;
3276 color: #f56e28;
3277 }
3278
3279 .wffn-pairing-warning__icon .dashicons {
3280 font-size: 17px;
3281 width: 17px;
3282 height: 17px;
3283 vertical-align: text-top;
3284 }
3285
3286 .wffn-pairing-warning__title {
3287 font-weight: 600;
3288 margin-block-end: 10px;
3289 }
3290
3291 .wffn-pairing-warning ~ p {
3292 display: none;
3293 }
3294 </style>
3295 <?php
3296 }
3297
3298 /**
3299 * Same markup core uses for its update row, so styling and the shiny
3300 * (AJAX) update flow keep working.
3301 *
3302 * @param string $file Plugin basename.
3303 * @param array $plugin_data Plugin headers.
3304 *
3305 * @return void
3306 */
3307 public function render_update_row( $file, $plugin_data ) {
3308 /** The core update row is present instead; the block renders inside it. */
3309 $updates = get_site_transient( 'update_plugins' );
3310 if ( isset( $updates->response[ $file ] ) ) {
3311 return;
3312 }
3313
3314 $wp_list_table = _get_list_table( 'WP_Plugins_List_Table', array( 'screen' => get_current_screen() ) );
3315 $active_class = is_plugin_active( $file ) ? ' active' : '';
3316 ?>
3317 <tr class="plugin-update-tr wffn-pro-pairing-tr<?php echo esc_attr( $active_class ); ?>" id="wffn-pro-pairing-update" data-plugin="<?php echo esc_attr( $file ); ?>">
3318 <td colspan="<?php echo esc_attr( $wp_list_table->get_column_count() ); ?>" class="plugin-update colspanchange">
3319 <div class="update-message notice inline notice-warning notice-alt">
3320 <?php $this->render_pairing_warning_block( false ); ?>
3321 </div>
3322 </td>
3323 </tr>
3324 <?php
3325 }
3326
3327 /**
3328 * Site-wide admin notice while Pro is outdated. Skipped on the FunnelKit
3329 * app screens, where the full-page prompt already covers it.
3330 *
3331 * @return void
3332 */
3333 public function maybe_render_sitewide_notice() {
3334 if ( ! $this->is_outdated() ) {
3335 return;
3336 }
3337
3338 if ( class_exists( 'WFFN_Role_Capability' ) && ! WFFN_Role_Capability::get_instance()->user_access( 'menu', 'read' ) ) {
3339 return;
3340 }
3341
3342 if ( $this->is_wffn_flex_page( 'all' ) ) {
3343 return;
3344 }
3345 ?>
3346 <div class="notice notice-warning">
3347 <p>
3348 <strong>
3349 <?php
3350 printf( /* translators: %s: premium plugin name */
3351 esc_html__( 'Update %s', 'funnel-builder' ),
3352 esc_html( $this->get_companion_name() )
3353 );
3354 ?>
3355 </strong>
3356 </p>
3357 <p>
3358 <?php
3359 printf( /* translators: 1: installed Funnel Builder version, 2: premium plugin name without brand prefix, 3: required version */
3360 esc_html__( 'Funnel Builder %1$s pairs with %2$s version %3$s or higher.', 'funnel-builder' ),
3361 esc_html( WFFN_VERSION ),
3362 esc_html( $this->get_companion_short_name() ),
3363 esc_html( WFFN_MIN_PRO_VERSION )
3364 );
3365 echo ' ';
3366 esc_html_e( 'Some Pro features may not work correctly until Pro is updated. Updating takes about a minute.', 'funnel-builder' );
3367 ?>
3368 </p>
3369 <p>
3370 <a class="button button-primary" href="<?php echo esc_url( self_admin_url( 'plugins.php?s=FunnelKit+Funnel+Builder' ) ); ?>"><?php esc_html_e( 'Update Now', 'funnel-builder' ); ?></a>
3371 </p>
3372 </div>
3373 <?php
3374 }
3375
3376 /**
3377 * Marks the FunnelKit toolbar menu while Pro is outdated: a badge on the
3378 * top-level node and an update link as the only dropdown entry.
3379 *
3380 * @param WP_Admin_Bar $wp_admin_bar
3381 *
3382 * @return void
3383 */
3384 public function maybe_add_admin_bar_state( WP_Admin_Bar $wp_admin_bar ) {
3385 if ( ! $this->is_outdated() ) {
3386 return;
3387 }
3388
3389 $node = $wp_admin_bar->get_node( 'wffn_funnel' );
3390 if ( empty( $node ) ) {
3391 return;
3392 }
3393
3394 $wp_admin_bar->add_node(
3395 array(
3396 'id' => 'wffn_funnel',
3397 'title' => $node->title . '<span class="wffn-ab-update-badge" aria-label="' . esc_attr__( 'Update Required', 'funnel-builder' ) . '"><span class="wffn-ab-update-badge__dot"></span></span>',
3398 'href' => self_admin_url( 'plugins.php?s=FunnelKit+Funnel+Builder' ),
3399 )
3400 );
3401
3402 /** The admin app is replaced by the update prompt, so its routes lead nowhere. */
3403 $dead_parents = array( 'wffn_funnel' );
3404 do {
3405 $removed = false;
3406 foreach ( (array) $wp_admin_bar->get_nodes() as $sub_node ) {
3407 if ( 'wffn_funnel' !== $sub_node->id && in_array( $sub_node->parent, $dead_parents, true ) ) {
3408 $wp_admin_bar->remove_node( $sub_node->id );
3409 $dead_parents[] = $sub_node->id;
3410 $removed = true;
3411 }
3412 }
3413 } while ( $removed );
3414
3415 $wp_admin_bar->add_menu(
3416 array(
3417 'id' => 'wffn_funnel_update_required',
3418 'parent' => 'wffn_funnel',
3419 'title' => '<span class="wffn-ab-update-title">' . sprintf( /* translators: %s: premium plugin name without brand prefix */ esc_html__( 'Update %s', 'funnel-builder' ), esc_html( $this->get_companion_short_name() ) ) . '</span><span class="wffn-ab-update-sub">' . sprintf( /* translators: %s: required version ("+" stands for "or higher") */ esc_html__( 'Requires %s+', 'funnel-builder' ), esc_html( WFFN_MIN_PRO_VERSION ) ) . '</span>',
3420 'href' => self_admin_url( 'plugins.php?s=FunnelKit+Funnel+Builder' ),
3421 )
3422 );
3423 ?>
3424 <style type="text/css">
3425 #wp-admin-bar-wffn_funnel .wffn-ab-update-badge {
3426 display: inline-flex;
3427 align-items: center;
3428 justify-content: center;
3429 width: 16px;
3430 height: 16px;
3431 margin-left: 6px;
3432 border-radius: 50%;
3433 background: #f2c14b2e;
3434 vertical-align: middle;
3435 }
3436
3437 #wp-admin-bar-wffn_funnel .wffn-ab-update-badge__dot {
3438 width: 8px;
3439 height: 8px;
3440 border-radius: 50%;
3441 background: #f2c14b;
3442 }
3443
3444 #wpadminbar ul#wp-admin-bar-wffn_funnel-default li#wp-admin-bar-wffn_funnel_update_required a.ab-item {
3445 height: auto;
3446 padding-top: 4px;
3447 padding-bottom: 6px;
3448 line-height: 18px;
3449 }
3450
3451 #wpadminbar ul#wp-admin-bar-wffn_funnel-default .wffn-ab-update-title {
3452 display: block;
3453 font-weight: 600;
3454 line-height: 18px;
3455 color: #e2b644;
3456 }
3457
3458 #wpadminbar ul#wp-admin-bar-wffn_funnel-default .wffn-ab-update-sub {
3459 display: block;
3460 margin-top: 2px;
3461 line-height: 18px;
3462 color: #a7aaad;
3463 }
3464 </style>
3465 <?php
3466 }
3467
3468 /**
3469 * Adds the update-required entry to the in-app notifications feed while
3470 * Pro is outdated.
3471 *
3472 * @param array $notifs
3473 *
3474 * @return array
3475 */
3476 public function maybe_have_outdated_notif( $notifs ) {
3477 if ( $this->is_outdated() ) {
3478 if ( false !== $this->is_update_available( true ) ) {
3479 $notifs[] = array(
3480 'key' => 'fb_required_update_3_16',
3481 'content' => '<div class="bwf-notifications-message current">
3482 <h3 class="bwf-notifications-title">' . sprintf( __( 'Alert! Your %s version is not in sync with the current lite version.', 'funnel-builder' ), $this->get_companion_name() ) . '</h3>
3483 <p class="bwf-notifications-content">' . __( 'Due to some technical changes in how admin application is loaded, the free version has fallen out of sync with the premium version. Consider updating the premium version.', 'funnel-builder' ) . '</p>
3484 </div>',
3485 'not_dismissible' => true,
3486 'customButtons' => array(
3487 array(
3488 'label' => __( 'Update Now', 'funnel-builder' ),
3489 'href' => admin_url( 'plugins.php?s=FunnelKit+Funnel+Builder' ),
3490 'className' => 'is-primary',
3491 'target' => '__blank',
3492 ),
3493
3494 ),
3495 'index' => 9,
3496
3497 );
3498 } else {
3499 $url = 'https://myaccount.funnelkit.com/';
3500 $deactivate_url = '';
3501 if ( defined( 'WFFN_PRO_FILE' ) ) {
3502 $pro_basename = plugin_basename( WFFN_PRO_FILE );
3503 $deactivate_url = wp_nonce_url( self_admin_url( 'plugins.php?action=deactivate&plugin=' . rawurlencode( $pro_basename ) ), 'deactivate-plugin_' . $pro_basename );
3504 }
3505 $notifs[] = array(
3506 'key' => 'fb_required_update_3_16',
3507 'content' => '<div class="bwf-notifications-message current">
3508 <h3 class="bwf-notifications-title">' . sprintf( __( 'Alert! Your %s version is not in sync with the current lite version.', 'funnel-builder' ), $this->get_companion_name() ) . '</h3>
3509 <p class="bwf-notifications-content">' . __( 'Due to some technical changes in how admin application is loaded, the free version has fallen out of sync with the premium version. We recommend logging into the Funnelkit accounts and downloading the latest version.', 'funnel-builder' ) . '</p>
3510 </div>',
3511 'not_dismissible' => true,
3512 'customButtons' => array(
3513 array(
3514 'label' => __( 'Get the latest version', 'funnel-builder' ),
3515 'href' => $url,
3516 'className' => 'is-primary',
3517 'target' => '__blank',
3518 ),
3519 array(
3520 'label' => __( 'Deactivate Funnel Builder Pro', 'funnel-builder' ),
3521 'href' => $deactivate_url,
3522 'target' => '__blank',
3523 ),
3524 ),
3525 'index' => 9,
3526
3527 );
3528 }
3529 }
3530
3531 return $notifs;
3532 }
3533 }
3534
3535 if ( class_exists( 'WFFN_Core' ) ) {
3536 WFFN_Core::register( 'admin', 'WFFN_Admin' );
3537 }
3538 }
3539