PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
← All changes | admin/modules/settings/class-wpfnl-settings.php +177 -227 3.12.1 → 3.13.3 View file →
@@ -9,9 +9,8 @@
9 9
10 10 use WPFunnels\Admin\Module\Wpfnl_Admin_Module;
11 11 use WPFunnels\Rollback;
12 12 use WPFunnels\Traits\SingletonTrait;
13 -use WPFunnels\Wpfnl;
14 13 use WPFunnels\Wpfnl_functions;
15 14
16 15 class Module extends Wpfnl_Admin_Module
17 16 {
@@ -20,93 +19,10 @@
20 19 protected $validations;
21 20
22 21 protected $prefix = '_wpfunnels_';
23 22
24 - protected $general_settings;
25 -
26 - protected $permalink_settings;
27 -
28 - protected $optin_settings;
29 -
30 - protected $offer_settings;
31 -
32 - protected $user_roles;
33 -
34 - protected $gtm_events;
35 -
36 - protected $gtm_settings;
37 -
38 - protected $facebook_pixel_events;
39 -
40 - protected $facebook_pixel_settings;
41 -
42 - protected $recaptcha_settings;
43 -
44 - protected $utm_params;
45 -
46 - protected $utm_settings;
47 -
48 - protected $is_allow_sales;
49 -
50 - /**
51 - * User Roles Settings.
52 - *
53 - * @var array $user_roles_settings An associative array containing user roles settings.
54 - */
55 - protected $user_roles_settings;
56 -
57 - /**
58 - * Notification Settings.
59 - *
60 - * @var array $notification_settings An associative array containing notification settings.
61 - */
62 - protected $notification_settings;
63 -
64 -
65 - /**
66 - * Google Maps API Key.
67 - *
68 - * This property stores the API key used for accessing Google Maps services.
69 - *
70 - * @var string $google_map_api_key The Google Maps API key.
71 - */
72 - protected $google_map_api_key;
73 -
74 - /**
75 - * Page hooks
76 - *
77 - * @var array
78 - */
79 - private $page_hooks = [
80 - 'toplevel_page_wp_funnels',
81 - 'wp-funnels_page_wp_funnel_settings',
82 - 'wpfunnels_page_wpfnl_settings',
83 - 'wp-funnels_page_edit_funnel',
84 - 'wp-funnels_page_create_funnel',
85 - 'wp-funnels_page_wpfnl_settings',
86 - 'wp-funnels_page_wpf-license',
87 - 'wpfunnels_page_email-builder',
88 - ];
89 -
90 - protected $settings_meta_keys = [
91 - '_wpfunnels_funnel_type' => 'sales',
92 - '_wpfunnels_builder' => 'elementor',
93 - '_wpfunnels_paypal_reference' => '',
94 - '_wpfunnels_order_bump' => '',
95 - '_wpfunnels_ab_testing' => '',
96 - '_wpfunnels_allow_funnels' => [
97 - 'administrator' => true,
98 - ],
99 - '_wpfunnels_permalink_settings' => '',
100 - '_wpfunnels_optin_settings' => '',
101 - '_wpfunnels_permalink_step_base' => 'wpfunnels',
102 - '_wpfunnels_permalink_flow_base' => 'step',
103 - '_wpfunnels_set_permalink' => 'step',
104 - ];
105 -
106 23 public function __construct()
107 24 {
108 - add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
109 25 add_action('admin_init', [$this, 'after_permalink_settings_saved']);
110 26 add_action( 'admin_post_wpfunnels_rollback', [ $this, 'post_wpfunnels_rollback' ] );
111 27 add_action( 'wpfunnels_send_revenue_report_email', [ $this, 'send_revenue_report_email' ] );
112 28 $this->init_ajax();
@@ -111,50 +27,36 @@
111 27 add_action( 'wpfunnels_send_revenue_report_email', [ $this, 'send_revenue_report_email' ] );
112 28 $this->init_ajax();
113 29 }
114 30
115 - public function is_wc_installed()
31 + /**
32 + * Render the settings screen.
33 + *
34 + * The screen is a route of the React admin now — `?page=wpfunnels#/settings`
35 + * — which admin_init sends this page to (see
36 + * Wpfnl_Menus::maybe_redirect_to_react_admin). Printing the app's root here
37 + * keeps the page working for anyone who filters that redirect off; there is
38 + * no server-rendered settings markup any more.
39 + *
40 + * @return void
41 + * @since 1.0.0
42 + */
43 + public function get_view()
116 44 {
117 - $path = 'woocommerce/woocommerce.php';
118 - $plugins = get_plugins();
45 + /*
46 + * The app reads its route from the fragment, which never reaches the
47 + * server — so landing here with no fragment would open the dashboard.
48 + * The bundle is a footer script, so this still prints before it runs.
49 + */
50 + wp_add_inline_script(
51 + 'wpfnl-react-admin',
52 + "if ( ! window.location.hash ) { window.location.hash = '#/settings'; }",
53 + 'before'
54 + );
119 55
120 - return isset($plugins[ $path ]);
56 + require WPFNL_ADMIN_DIR . 'views/react-admin.php';
121 57 }
122 58
123 - public function is_ff_installed()
124 - {
125 - $path = 'fluentform/fluentform.php';
126 - $plugins = get_plugins();
127 -
128 - return isset($plugins[ $path ]);
129 - }
130 -
131 - public function is_elementor_installed()
132 - {
133 - $path = 'elementor/elementor.php';
134 - $plugins = get_plugins();
135 -
136 - return isset($plugins[ $path ]);
137 - }
138 -
139 -
140 - public function enqueue_scripts( $hook )
141 - {
142 - if ( ! in_array( $hook, $this->page_hooks, true ) ) {
143 - return;
144 - }
145 - wp_enqueue_script('settings', plugin_dir_url(__FILE__) . 'js/settings.js', ['jquery'], WPFNL_VERSION, true);
146 - }
147 -
148 -
149 - public function get_view()
150 - {
151 - $this->init_settings();
152 - $is_pro_activated = Wpfnl_functions::is_wpfnl_pro_activated();
153 - $global_funnel_type = Wpfnl_functions::get_global_funnel_type();
154 - require_once WPFNL_DIR . '/admin/modules/settings/views/view.php';
155 - }
156 -
157 59 /**
158 60 * Init ajax hooks for
159 61 * saving metas
160 62 *
@@ -161,8 +63,17 @@
161 63 * @since 1.0.0
162 64 */
163 65 public function init_ajax()
164 66 {
67 + /*
68 + * Only the two actions that are still posted to from outside this module
69 + * are registered: the Vue template library's sync button
70 + * (`clear-templates`) and the setup wizard (`update-general-settings`).
71 + * Everything the settings screen itself used to post goes through
72 + * wpfunnels/v1/settings/* now — see
73 + * includes/core/rest-api/Controllers/class-settings-page-controller.php,
74 + * which calls the very same handlers below.
75 + */
165 76 $this->validations = [
166 77 'logged_in' => true,
167 78 'user_can' => 'wpf_manage_funnels',
168 79 ];
@@ -173,23 +84,8 @@
173 84 wp_ajax_helper()->handle('clear-templates')
174 85 ->with_callback([ $this, 'clear_templates_data' ])
175 86 ->with_validation($this->validations);
176 87
177 - wp_ajax_helper()->handle('clear-transient')
178 - ->with_callback([ $this, 'clear_transient_cache_data' ])
179 - ->with_validation($this->validations);
180 -
181 - wp_ajax_helper()->handle('wpfnl-show-log')
182 - ->with_callback([ $this, 'wpfnl_show_log' ])
183 - ->with_validation($this->validations);
184 -
185 - wp_ajax_helper()->handle('wpfnl-delete-log')
186 - ->with_callback([ $this, 'wpfnl_delete_log' ])
187 - ->with_validation($this->validations);
188 -
189 - wp_ajax_helper()->handle('wpfnl-send-test-notification')
190 - ->with_callback([ $this, 'send_test_notification' ])
191 - ->with_validation($this->validations);
192 88 }
193 89
194 90
195 91 /**
@@ -203,11 +99,12 @@
203 99 */
204 100 public function update_general_settings($payload)
205 101 {
206 102 do_action('wpfunnels/before_settings_saved', $payload);
103 +
104 + $this->save_ai_settings($payload);
207 105 $existing_settings = Wpfnl_functions::get_general_settings();
208 106 $general_settings = [
209 - 'funnel_type' => sanitize_text_field($payload['funnel_type']),
210 107 'builder' => sanitize_text_field($payload['builder']),
211 108 'uninstall_cleanup' => isset($payload['uninstall_cleanup']) ? sanitize_text_field($payload['uninstall_cleanup']) : 'off',
212 109 'disable_analytics' => isset($payload['analytics_roles']) ? $payload['analytics_roles'] : '',
213 110 'allow_funnels' => isset($payload['permission_role']) ? $payload['permission_role'] : [],
@@ -217,13 +114,20 @@
217 114 'disable_theme_style' => isset($payload['disable_theme_style']) ? sanitize_text_field($payload['disable_theme_style']) : 'off',
218 115 'enable_log_status' => isset($payload['enable_log_status']) ? sanitize_text_field($payload['enable_log_status']) : 'off',
219 116 'enable_skip_cart' => isset($payload['enable_skip_cart']) ? $payload['enable_skip_cart'] : 'no',
220 117 'skip_cart_for' => isset($payload['skip_cart_for']) ? $payload['skip_cart_for'] : 'whole',
221 - 'funnel_builder_mode' => isset($payload['funnel_builder_mode']) ? sanitize_text_field($payload['funnel_builder_mode']) : $existing_settings['funnel_builder_mode'],
118 + 'funnel_builder_mode' => isset($payload['funnel_builder_mode']) ? sanitize_text_field($payload['funnel_builder_mode']) : Wpfnl_functions::get_default_funnel_builder_mode(),
222 119 ];
223 120
224 - if ( isset( $payload['funnel_builder_mode'] ) && 'vertical' === $payload['funnel_builder_mode'] ) {
225 - do_action( 'wpfunnels_vertical_mode_used' );
121 + if ( isset( $payload['funnel_builder_mode'] ) ) {
122 + // Choosing the site default here is an explicit statement of intent, so
123 + // drop this user's own canvas override — otherwise a view they toggled
124 + // earlier would keep winning over the mode they just picked.
125 + delete_user_meta( get_current_user_id(), Wpfnl_functions::CANVAS_VIEW_MODE_META_KEY );
126 +
127 + if ( 'vertical' === $payload['funnel_builder_mode'] ) {
128 + do_action( 'wpfunnels_vertical_mode_used' );
129 + }
226 130 }
227 131
228 132 $permalink_settings = [
229 133 'structure' => isset($payload['permalink_settings']) ? sanitize_text_field($payload['permalink_settings']) : 'default',
@@ -243,9 +147,8 @@
243 147
244 148
245 149 foreach ($payload as $key => $value) {
246 150 switch ($key) {
247 - case 'funnel_type':
248 151 case 'builder':
249 152 $cache_key = 'wpfunnels_remote_template_data_' . WPFNL_VERSION;
250 153 delete_transient($cache_key);
251 154 delete_option(WPFNL_TEMPLATES_OPTION_KEY);
@@ -264,8 +167,9 @@
264 167 Wpfnl_functions::update_admin_settings($this->prefix.'general_settings', $general_settings);
265 168 Wpfnl_functions::update_admin_settings($this->prefix.'permalink_settings', $permalink_settings);
266 169 Wpfnl_functions::update_admin_settings($this->prefix.'optin_settings', $optin_settings);
267 170 $this->save_recaptcha_settings($payload);
171 + $this->save_tracking_consent($payload);
268 172 $this->save_user_role_management_data($payload);
269 173 $this->save_google_map_key($payload);
270 174 $this->save_lms_settings($payload);
271 175 $this->save_notification_settings($payload);
@@ -286,8 +190,54 @@
286 190 ];
287 191 }
288 192
289 193 /**
194 + * Persist the usage-tracking opt-in from the Advanced panel.
195 + *
196 + * The toggle only sends a value when the user actually changed it (see
197 + * getTrackingConsentChange() in js/settings.js), so an unrelated save on a
198 + * site that has never answered the setup-wizard question leaves that
199 + * "undecided" state alone instead of writing an explicit "no". Consent
200 + * itself lives in the Linno telemetry option, not in general settings, and
201 + * the SDK migrates the legacy `wpfunnels_allow_tracking` key on read — so
202 + * existing users keep whatever they agreed to during onboarding.
203 + *
204 + * @param array $payload Settings payload from the save request.
205 + *
206 + * @since 3.13.0
207 + */
208 + private function save_tracking_consent( $payload ) {
209 + if ( ! isset( $payload['allow_tracking'] ) ) {
210 + return;
211 + }
212 +
213 + $submitted = sanitize_text_field( $payload['allow_tracking'] );
214 +
215 + if ( ! in_array( $submitted, [ 'yes', 'no' ], true ) ) {
216 + return;
217 + }
218 +
219 + $consent = 'yes' === $submitted;
220 +
221 + $telemetry = \WPFunnels\Tracking\Telemetry::instance();
222 +
223 + if ( ! $telemetry ) {
224 + return;
225 + }
226 +
227 + // Nothing to do when the toggle already matches what is stored — this
228 + // also stops a redundant save from turning "never decided" into "no".
229 + if ( $telemetry->get_consent_state() === ( $consent ? 'yes' : 'no' ) ) {
230 + return;
231 + }
232 +
233 + $telemetry->save_consent( $consent );
234 +
235 + do_action( 'wpfunnels_tracking_consent_updated', $consent );
236 + }
237 +
238 +
239 + /**
290 240 * Save Facebook pixel settings
291 241 *
292 242 * @param $settings
293 243 *
@@ -427,42 +377,9 @@
427 377 *
428 378 * @since 3.2.0
429 379 */
430 380 private function schedule_revenue_report_email( $settings ) {
431 - if ( ! function_exists( 'as_schedule_recurring_action' ) ) {
432 - return;
433 - }
434 -
435 - // Calculate next run time based on send_time
436 - $send_time = isset( $settings['send_time'] ) ? $settings['send_time'] : '10:00';
437 - $frequency = isset( $settings['revenue_report_frequency'] ) ? $settings['revenue_report_frequency'] : 'weekly';
438 -
439 - // Parse time (format: HH:MM)
440 - list( $hour, $minute ) = explode( ':', $send_time );
441 -
442 - // Get current time
443 - $now = current_time( 'timestamp' );
444 - $today = strtotime( date( 'Y-m-d', $now ) );
445 -
446 - // Calculate first run time (today at specified time)
447 - $first_run = $today + ( $hour * HOUR_IN_SECONDS ) + ( $minute * MINUTE_IN_SECONDS );
448 -
449 - // If the time has already passed today, schedule for tomorrow
450 - if ( $first_run <= $now ) {
451 - $first_run += DAY_IN_SECONDS;
452 - }
453 -
454 - // Calculate interval based on frequency
455 - $interval = ( $frequency === 'monthly' ) ? MONTH_IN_SECONDS : WEEK_IN_SECONDS;
456 -
457 - // Schedule recurring action
458 - as_schedule_recurring_action(
459 - $first_run,
460 - $interval,
461 - 'wpfunnels_send_revenue_report_email',
462 - array(),
463 - 'wpfunnels'
464 - );
381 + Wpfnl_functions::schedule_revenue_report_email( $settings );
465 382 }
466 383
467 384
468 385 /**
@@ -470,44 +387,69 @@
470 387 *
471 388 * @since 3.2.0
472 389 */
473 390 private function unschedule_revenue_report_email() {
474 - if ( ! function_exists( 'as_unschedule_all_actions' ) ) {
391 + Wpfnl_functions::unschedule_revenue_report_email();
392 + }
393 +
394 +
395 + /**
396 + * Persist the AI Assistant fields that ride along with the settings save.
397 + *
398 + * Keys are stored through AISettings::connect(), which encrypts them. A
399 + * blank field means "keep the stored key", so an unrelated settings save
400 + * never wipes a connection.
401 + *
402 + * @param array $payload Settings payload.
403 + *
404 + * @since 3.13.0
405 + * @return void
406 + */
407 + private function save_ai_settings( $payload ) {
408 + if ( ! class_exists( '\WPFunnels\AI\Settings\AISettings' ) ) {
475 409 return;
476 410 }
477 -
478 - as_unschedule_all_actions( 'wpfunnels_send_revenue_report_email', array(), 'wpfunnels' );
479 - }
480 411
412 + $settings = '\WPFunnels\AI\Settings\AISettings';
481 413
482 - /**
483 - * Initialize all the settings value
484 - *
485 - * @since 1.0.0
486 - */
487 - public function init_settings()
488 - {
489 - $this->general_settings = Wpfnl_functions::get_general_settings();
490 - $this->is_allow_sales = Wpfnl_functions::maybe_allow_sales_funnel();
491 - $this->permalink_settings = Wpfnl_functions::get_permalink_settings();
492 - $this->optin_settings = Wpfnl_functions::get_optin_settings();
493 - $this->offer_settings = Wpfnl_functions::get_offer_settings();
494 - $this->user_roles = Wpfnl_functions::get_user_roles();
495 - $this->gtm_events = Wpfnl_functions::get_gtm_events();
496 - $this->gtm_settings = Wpfnl_functions::get_gtm_settings();
497 - $this->facebook_pixel_events = Wpfnl_functions::get_facebook_events();
498 - $this->facebook_pixel_settings = Wpfnl_functions::get_facebook_pixel_settings();
499 - $this->utm_params = Wpfnl_functions::get_utm_params();
500 - $this->utm_settings = Wpfnl_functions::get_utm_settings();
501 - $this->recaptcha_settings = Wpfnl_functions::get_recaptcha_settings();
502 - $this->user_roles_settings = Wpfnl_functions::get_user_role_settings();
503 - $this->google_map_api_key = Wpfnl_functions::get_google_map_api_key();
504 - $this->notification_settings = Wpfnl_functions::get_notification_settings();
505 - }
414 + foreach ( array( 'anthropic', 'openai', 'gemini' ) as $provider ) {
415 + $field = 'ai_' . $provider . '_key';
416 + $key = isset( $payload[ $field ] ) ? trim( sanitize_text_field( $payload[ $field ] ) ) : '';
506 417
418 + // An untouched field arrives blank, and the placeholder mask must
419 + // never be stored as a credential.
420 + if ( '' === $key || false !== strpos( $key, '•' ) ) {
421 + continue;
422 + }
507 423
424 + call_user_func( array( $settings, 'connect' ), $provider, $key );
425 + }
508 426
427 + if ( isset( $payload['ai_provider'] ) && '' !== $payload['ai_provider'] ) {
428 + $provider = sanitize_text_field( $payload['ai_provider'] );
509 429
430 + // wordpress_ai needs no key; activating it is the whole connection.
431 + if ( 'wordpress_ai' === $provider && ! call_user_func( array( $settings, 'isConnected' ), $provider ) ) {
432 + call_user_func( array( $settings, 'connect' ), $provider, '' );
433 + }
434 +
435 + call_user_func( array( $settings, 'setActiveProvider' ), $provider );
436 + }
437 +
438 + if ( isset( $payload['ai_custom_instructions'] ) ) {
439 + call_user_func( array( $settings, 'saveCustomInstructions' ), sanitize_textarea_field( $payload['ai_custom_instructions'] ) );
440 + }
441 +
442 + if ( isset( $payload['ai_enabled'] ) ) {
443 + call_user_func( array( $settings, 'setEnabled' ), 'yes' === $payload['ai_enabled'] );
444 + }
445 +
446 + if ( isset( $payload['mcp_enabled'] ) ) {
447 + update_option( '_wpfnl_mcp_enabled', 'yes' === $payload['mcp_enabled'] ? 'yes' : 'no' );
448 + }
449 + }
450 +
451 +
510 452 /**
511 453 * Clear saved templates data
512 454 *
513 455 * @param $payload
@@ -577,21 +519,8 @@
577 519 }
578 520 }
579 521
580 522
581 - /**
582 - * Get settings by meta key
583 - *
584 - * @param $key
585 - *
586 - * @return mixed|string
587 - * @since 1.0.0
588 - */
589 - public function get_settings_by_key($key)
590 - {
591 - return isset($this->settings_meta_keys[$key]) ? $this->settings_meta_keys[$key]: '';
592 - }
593 -
594 523 public function get_name()
595 524 {
596 525 return __('settings','wpfnl');
597 526 }
@@ -699,22 +628,43 @@
699 628 * @return array
700 629 */
701 630 public static function wpfnl_show_log( $payload ) {
702 631 if( isset($payload['logKey']) ){
703 - $key = $payload['logKey'];
632 + // Reduce to a bare filename — strips any directory traversal (../),
633 + // separators, and stream-wrapper prefixes (phar://, php://, etc.).
634 + $key = basename( wp_unslash( $payload['logKey'] ) );
635 +
704 636 $upload_dir = wp_upload_dir( null, false );
705 - $log_url = $upload_dir['basedir'].'/wpfunnels/wpfunnels-logs/';
706 - $file_url = $log_url . $key;
637 + $log_url = trailingslashit( $upload_dir['basedir'] ) . 'wpfunnels/wpfunnels-logs/';
638 + $file_url = $log_url . $key;
707 639
708 - ob_start();
709 - include_once $file_url;
710 - $out = ob_get_clean();
711 - ob_end_clean();
640 + // Whitelist: key must be a .log filename that actually resolves inside
641 + // the logs directory. realpath() comparison defeats any residual traversal.
642 + $real_dir = realpath( $log_url );
643 + $real_file = file_exists( $file_url ) ? realpath( $file_url ) : false;
712 644
645 + $is_valid = $key
646 + && '.log' === strtolower( substr( $key, -4 ) )
647 + && $real_dir
648 + && $real_file
649 + && 0 === strpos( $real_file, trailingslashit( $real_dir ) );
650 +
651 + if ( $is_valid ) {
652 + // Never include/execute the log file — read it as data and escape it.
653 + $out = file_get_contents( $file_url );
654 + $out = '<pre>' . esc_html( $out ) . '</pre>';
655 +
656 + return array(
657 + 'success' => true,
658 + 'content' => $out,
659 + 'file_url' => $file_url,
660 + );
661 + }
662 +
713 663 return array(
714 - 'success' => true,
715 - 'content' => $out,
716 - 'file_url' => $log_url. $key
664 + 'success' => false,
665 + 'content' => '',
666 + 'file_url' => '',
717 667 );
718 668 }
719 669
720 670 return array(