← All changes
|
admin/modules/settings/class-wpfnl-settings.php
+177
-227
3.12.5
→
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( |