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 +180 -224 3.11.0 → 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,10 +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);
105 + $existing_settings = Wpfnl_functions::get_general_settings();
207 106 $general_settings = [
208 - 'funnel_type' => sanitize_text_field($payload['funnel_type']),
209 107 'builder' => sanitize_text_field($payload['builder']),
210 108 'uninstall_cleanup' => isset($payload['uninstall_cleanup']) ? sanitize_text_field($payload['uninstall_cleanup']) : 'off',
211 109 'disable_analytics' => isset($payload['analytics_roles']) ? $payload['analytics_roles'] : '',
212 110 'allow_funnels' => isset($payload['permission_role']) ? $payload['permission_role'] : [],
@@ -216,10 +114,22 @@
216 114 'disable_theme_style' => isset($payload['disable_theme_style']) ? sanitize_text_field($payload['disable_theme_style']) : 'off',
217 115 'enable_log_status' => isset($payload['enable_log_status']) ? sanitize_text_field($payload['enable_log_status']) : 'off',
218 116 'enable_skip_cart' => isset($payload['enable_skip_cart']) ? $payload['enable_skip_cart'] : 'no',
219 117 'skip_cart_for' => isset($payload['skip_cart_for']) ? $payload['skip_cart_for'] : 'whole',
118 + 'funnel_builder_mode' => isset($payload['funnel_builder_mode']) ? sanitize_text_field($payload['funnel_builder_mode']) : Wpfnl_functions::get_default_funnel_builder_mode(),
220 119 ];
221 120
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 + }
130 + }
131 +
222 132 $permalink_settings = [
223 133 'structure' => isset($payload['permalink_settings']) ? sanitize_text_field($payload['permalink_settings']) : 'default',
224 134 'step_base' => isset($payload['permalink_step_base']) ? sanitize_text_field($payload['permalink_step_base']) : WPFNL_STEPS_POST_TYPE,
225 135 'funnel_base' => isset($payload['permalink_funnel_base']) ? sanitize_text_field($payload['permalink_funnel_base']) : WPFNL_FUNNELS_POST_TYPE,
@@ -237,9 +147,8 @@
237 147
238 148
239 149 foreach ($payload as $key => $value) {
240 150 switch ($key) {
241 - case 'funnel_type':
242 151 case 'builder':
243 152 $cache_key = 'wpfunnels_remote_template_data_' . WPFNL_VERSION;
244 153 delete_transient($cache_key);
245 154 delete_option(WPFNL_TEMPLATES_OPTION_KEY);
@@ -258,8 +167,9 @@
258 167 Wpfnl_functions::update_admin_settings($this->prefix.'general_settings', $general_settings);
259 168 Wpfnl_functions::update_admin_settings($this->prefix.'permalink_settings', $permalink_settings);
260 169 Wpfnl_functions::update_admin_settings($this->prefix.'optin_settings', $optin_settings);
261 170 $this->save_recaptcha_settings($payload);
171 + $this->save_tracking_consent($payload);
262 172 $this->save_user_role_management_data($payload);
263 173 $this->save_google_map_key($payload);
264 174 $this->save_lms_settings($payload);
265 175 $this->save_notification_settings($payload);
@@ -280,8 +190,54 @@
280 190 ];
281 191 }
282 192
283 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 + /**
284 240 * Save Facebook pixel settings
285 241 *
286 242 * @param $settings
287 243 *
@@ -421,42 +377,9 @@
421 377 *
422 378 * @since 3.2.0
423 379 */
424 380 private function schedule_revenue_report_email( $settings ) {
425 - if ( ! function_exists( 'as_schedule_recurring_action' ) ) {
426 - return;
427 - }
428 -
429 - // Calculate next run time based on send_time
430 - $send_time = isset( $settings['send_time'] ) ? $settings['send_time'] : '10:00';
431 - $frequency = isset( $settings['revenue_report_frequency'] ) ? $settings['revenue_report_frequency'] : 'weekly';
432 -
433 - // Parse time (format: HH:MM)
434 - list( $hour, $minute ) = explode( ':', $send_time );
435 -
436 - // Get current time
437 - $now = current_time( 'timestamp' );
438 - $today = strtotime( date( 'Y-m-d', $now ) );
439 -
440 - // Calculate first run time (today at specified time)
441 - $first_run = $today + ( $hour * HOUR_IN_SECONDS ) + ( $minute * MINUTE_IN_SECONDS );
442 -
443 - // If the time has already passed today, schedule for tomorrow
444 - if ( $first_run <= $now ) {
445 - $first_run += DAY_IN_SECONDS;
446 - }
447 -
448 - // Calculate interval based on frequency
449 - $interval = ( $frequency === 'monthly' ) ? MONTH_IN_SECONDS : WEEK_IN_SECONDS;
450 -
451 - // Schedule recurring action
452 - as_schedule_recurring_action(
453 - $first_run,
454 - $interval,
455 - 'wpfunnels_send_revenue_report_email',
456 - array(),
457 - 'wpfunnels'
458 - );
381 + Wpfnl_functions::schedule_revenue_report_email( $settings );
459 382 }
460 383
461 384
462 385 /**
@@ -464,44 +387,69 @@
464 387 *
465 388 * @since 3.2.0
466 389 */
467 390 private function unschedule_revenue_report_email() {
468 - 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' ) ) {
469 409 return;
470 410 }
471 -
472 - as_unschedule_all_actions( 'wpfunnels_send_revenue_report_email', array(), 'wpfunnels' );
473 - }
474 411
412 + $settings = '\WPFunnels\AI\Settings\AISettings';
475 413
476 - /**
477 - * Initialize all the settings value
478 - *
479 - * @since 1.0.0
480 - */
481 - public function init_settings()
482 - {
483 - $this->general_settings = Wpfnl_functions::get_general_settings();
484 - $this->is_allow_sales = Wpfnl_functions::maybe_allow_sales_funnel();
485 - $this->permalink_settings = Wpfnl_functions::get_permalink_settings();
486 - $this->optin_settings = Wpfnl_functions::get_optin_settings();
487 - $this->offer_settings = Wpfnl_functions::get_offer_settings();
488 - $this->user_roles = Wpfnl_functions::get_user_roles();
489 - $this->gtm_events = Wpfnl_functions::get_gtm_events();
490 - $this->gtm_settings = Wpfnl_functions::get_gtm_settings();
491 - $this->facebook_pixel_events = Wpfnl_functions::get_facebook_events();
492 - $this->facebook_pixel_settings = Wpfnl_functions::get_facebook_pixel_settings();
493 - $this->utm_params = Wpfnl_functions::get_utm_params();
494 - $this->utm_settings = Wpfnl_functions::get_utm_settings();
495 - $this->recaptcha_settings = Wpfnl_functions::get_recaptcha_settings();
496 - $this->user_roles_settings = Wpfnl_functions::get_user_role_settings();
497 - $this->google_map_api_key = Wpfnl_functions::get_google_map_api_key();
498 - $this->notification_settings = Wpfnl_functions::get_notification_settings();
499 - }
414 + foreach ( array( 'anthropic', 'openai', 'gemini' ) as $provider ) {
415 + $field = 'ai_' . $provider . '_key';
416 + $key = isset( $payload[ $field ] ) ? trim( sanitize_text_field( $payload[ $field ] ) ) : '';
500 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 + }
501 423
424 + call_user_func( array( $settings, 'connect' ), $provider, $key );
425 + }
502 426
427 + if ( isset( $payload['ai_provider'] ) && '' !== $payload['ai_provider'] ) {
428 + $provider = sanitize_text_field( $payload['ai_provider'] );
503 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 +
504 452 /**
505 453 * Clear saved templates data
506 454 *
507 455 * @param $payload
@@ -571,21 +519,8 @@
571 519 }
572 520 }
573 521
574 522
575 - /**
576 - * Get settings by meta key
577 - *
578 - * @param $key
579 - *
580 - * @return mixed|string
581 - * @since 1.0.0
582 - */
583 - public function get_settings_by_key($key)
584 - {
585 - return isset($this->settings_meta_keys[$key]) ? $this->settings_meta_keys[$key]: '';
586 - }
587 -
588 523 public function get_name()
589 524 {
590 525 return __('settings','wpfnl');
591 526 }
@@ -693,22 +628,43 @@
693 628 * @return array
694 629 */
695 630 public static function wpfnl_show_log( $payload ) {
696 631 if( isset($payload['logKey']) ){
697 - $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 +
698 636 $upload_dir = wp_upload_dir( null, false );
699 - $log_url = $upload_dir['basedir'].'/wpfunnels/wpfunnels-logs/';
700 - $file_url = $log_url . $key;
637 + $log_url = trailingslashit( $upload_dir['basedir'] ) . 'wpfunnels/wpfunnels-logs/';
638 + $file_url = $log_url . $key;
701 639
702 - ob_start();
703 - include_once $file_url;
704 - $out = ob_get_clean();
705 - 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;
706 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 +
707 663 return array(
708 - 'success' => true,
709 - 'content' => $out,
710 - 'file_url' => $log_url. $key
664 + 'success' => false,
665 + 'content' => '',
666 + 'file_url' => '',
711 667 );
712 668 }
713 669
714 670 return array(