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 +751 -173 2.8.16 → 3.13.3 View file →
@@ -1,8 +1,8 @@
1 1 <?php
2 2 /**
3 3 * Settings module
4 - *
4 + *
5 5 * @package
6 6 */
7 7
8 8 namespace WPFunnels\Modules\Admin\Settings;
@@ -19,94 +19,44 @@
19 19 protected $validations;
20 20
21 21 protected $prefix = '_wpfunnels_';
22 22
23 - protected $general_settings;
24 -
25 - protected $permalink_settings;
26 -
27 - protected $offer_settings;
28 -
29 - protected $user_roles;
30 -
31 - protected $gtm_events;
32 -
33 - protected $gtm_settings;
34 -
35 - protected $facebook_pixel_events;
36 -
37 - protected $facebook_pixel_settings;
38 -
39 - protected $recaptcha_settings;
40 -
41 - protected $utm_params;
42 -
43 - protected $utm_settings;
44 -
45 - protected $is_allow_sales;
46 -
47 - protected $settings_meta_keys = [
48 - '_wpfunnels_funnel_type' => 'sales',
49 - '_wpfunnels_builder' => 'elementor',
50 - '_wpfunnels_paypal_reference' => '',
51 - '_wpfunnels_order_bump' => '',
52 - '_wpfunnels_ab_testing' => '',
53 - '_wpfunnels_allow_funnels' => [
54 - 'administrator' => true,
55 - ],
56 - '_wpfunnels_permalink_settings' => '',
57 - '_wpfunnels_permalink_step_base' => 'wpfunnels',
58 - '_wpfunnels_permalink_flow_base' => 'step',
59 - '_wpfunnels_set_permalink' => 'step',
60 - ];
61 -
62 23 public function __construct()
63 24 {
64 - add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
65 25 add_action('admin_init', [$this, 'after_permalink_settings_saved']);
66 26 add_action( 'admin_post_wpfunnels_rollback', [ $this, 'post_wpfunnels_rollback' ] );
27 + add_action( 'wpfunnels_send_revenue_report_email', [ $this, 'send_revenue_report_email' ] );
67 28 $this->init_ajax();
68 29 }
69 30
70 - 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()
71 44 {
72 - $path = 'woocommerce/woocommerce.php';
73 - $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 + );
74 55
75 - return isset($plugins[ $path ]);
56 + require WPFNL_ADMIN_DIR . 'views/react-admin.php';
76 57 }
77 58
78 - public function is_ff_installed()
79 - {
80 - $path = 'fluentform/fluentform.php';
81 - $plugins = get_plugins();
82 -
83 - return isset($plugins[ $path ]);
84 - }
85 -
86 - public function is_elementor_installed()
87 - {
88 - $path = 'elementor/elementor.php';
89 - $plugins = get_plugins();
90 -
91 - return isset($plugins[ $path ]);
92 - }
93 -
94 -
95 - public function enqueue_scripts()
96 - {
97 - wp_enqueue_script('settings', plugin_dir_url(__FILE__) . 'js/settings.js', ['jquery'], WPFNL_VERSION, true);
98 - }
99 -
100 -
101 - public function get_view()
102 - {
103 - $this->init_settings();
104 - $is_pro_activated = Wpfnl_functions::is_wpfnl_pro_activated();
105 - $global_funnel_type = Wpfnl_functions::get_global_funnel_type();
106 - require_once WPFNL_DIR . '/admin/modules/settings/views/view.php';
107 - }
108 -
109 59 /**
110 60 * Init ajax hooks for
111 61 * saving metas
112 62 *
@@ -113,11 +63,20 @@
113 63 * @since 1.0.0
114 64 */
115 65 public function init_ajax()
116 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 + */
117 76 $this->validations = [
118 77 'logged_in' => true,
119 - 'user_can' => 'manage_options',
78 + 'user_can' => 'wpf_manage_funnels',
120 79 ];
121 80 wp_ajax_helper()->handle('update-general-settings')
122 81 ->with_callback([ $this, 'update_general_settings' ])
123 82 ->with_validation($this->validations);
@@ -125,19 +84,8 @@
125 84 wp_ajax_helper()->handle('clear-templates')
126 85 ->with_callback([ $this, 'clear_templates_data' ])
127 86 ->with_validation($this->validations);
128 87
129 - wp_ajax_helper()->handle('clear-transient')
130 - ->with_callback([ $this, 'clear_transient_cache_data' ])
131 - ->with_validation($this->validations);
132 -
133 - wp_ajax_helper()->handle('wpfnl-show-log')
134 - ->with_callback([ $this, 'wpfnl_show_log' ])
135 - ->with_validation($this->validations);
136 -
137 - wp_ajax_helper()->handle('wpfnl-delete-log')
138 - ->with_callback([ $this, 'wpfnl_delete_log' ])
139 - ->with_validation($this->validations);
140 88 }
141 89
142 90
143 91 /**
@@ -144,9 +92,9 @@
144 92 * Update handler for settings
145 93 * page
146 94 *
147 95 * @param $payload
148 - *
96 + *
149 97 * @return array
150 98 * @since 1.0.0
151 99 */
152 100 public function update_general_settings($payload)
@@ -152,29 +100,55 @@
152 100 public function update_general_settings($payload)
153 101 {
154 102 do_action('wpfunnels/before_settings_saved', $payload);
155 103
104 + $this->save_ai_settings($payload);
105 + $existing_settings = Wpfnl_functions::get_general_settings();
156 106 $general_settings = [
157 - 'funnel_type' => $payload['funnel_type'],
158 - 'builder' => $payload['builder'],
107 + 'builder' => sanitize_text_field($payload['builder']),
108 + 'uninstall_cleanup' => isset($payload['uninstall_cleanup']) ? sanitize_text_field($payload['uninstall_cleanup']) : 'off',
159 109 'disable_analytics' => isset($payload['analytics_roles']) ? $payload['analytics_roles'] : '',
160 110 'allow_funnels' => isset($payload['permission_role']) ? $payload['permission_role'] : [],
161 - 'paypal_reference' => $payload['paypal_reference'],
162 - 'order_bump' => $payload['order_bump'],
163 - 'ab_testing' => $payload['ab_testing'],
164 - 'disable_theme_style' => $payload['disable_theme_style'],
165 - 'enable_log_status' => $payload['enable_log_status'],
111 + 'paypal_reference' => isset($payload['paypal_reference']) ? sanitize_text_field($payload['paypal_reference']) : 'off',
112 + 'order_bump' => isset($payload['order_bump']) ? sanitize_text_field($payload['order_bump']) : 'off',
113 + 'ab_testing' => isset($payload['ab_testing']) ? sanitize_text_field($payload['ab_testing']) : 'off',
114 + 'disable_theme_style' => isset($payload['disable_theme_style']) ? sanitize_text_field($payload['disable_theme_style']) : 'off',
115 + 'enable_log_status' => isset($payload['enable_log_status']) ? sanitize_text_field($payload['enable_log_status']) : 'off',
116 + 'enable_skip_cart' => isset($payload['enable_skip_cart']) ? $payload['enable_skip_cart'] : 'no',
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(),
166 119 ];
167 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 +
168 132 $permalink_settings = [
169 - 'structure' => $payload['permalink_settings'],
170 - 'step_base' => $payload['permalink_step_base'],
171 - 'funnel_base' => $payload['permalink_funnel_base'],
133 + 'structure' => isset($payload['permalink_settings']) ? sanitize_text_field($payload['permalink_settings']) : 'default',
134 + 'step_base' => isset($payload['permalink_step_base']) ? sanitize_text_field($payload['permalink_step_base']) : WPFNL_STEPS_POST_TYPE,
135 + 'funnel_base' => isset($payload['permalink_funnel_base']) ? sanitize_text_field($payload['permalink_funnel_base']) : WPFNL_FUNNELS_POST_TYPE,
172 136 ];
137 +
138 + if( isset($payload['sender_name']) && isset($payload['sender_email']) ){
139 + $optin_settings = [
140 + 'sender_name' => sanitize_text_field($payload['sender_name']),
141 + 'sender_email' => sanitize_text_field($payload['sender_email']),
142 + 'email_subject' => isset($payload['email_subject']) ? sanitize_text_field($payload['email_subject']) : '',
143 + ];
144 + }else{
145 + $optin_settings = Wpfnl_functions::get_optin_settings();
146 + }
147 +
148 +
173 149 foreach ($payload as $key => $value) {
174 -
175 150 switch ($key) {
176 - case 'funnel_type':
177 151 case 'builder':
178 152 $cache_key = 'wpfunnels_remote_template_data_' . WPFNL_VERSION;
179 153 delete_transient($cache_key);
180 154 delete_option(WPFNL_TEMPLATES_OPTION_KEY);
@@ -188,12 +162,18 @@
188 162 default:
189 163 break;
190 164 }
191 165 }
192 -
166 +
193 167 Wpfnl_functions::update_admin_settings($this->prefix.'general_settings', $general_settings);
194 168 Wpfnl_functions::update_admin_settings($this->prefix.'permalink_settings', $permalink_settings);
169 + Wpfnl_functions::update_admin_settings($this->prefix.'optin_settings', $optin_settings);
195 170 $this->save_recaptcha_settings($payload);
171 + $this->save_tracking_consent($payload);
172 + $this->save_user_role_management_data($payload);
173 + $this->save_google_map_key($payload);
174 + $this->save_lms_settings($payload);
175 + $this->save_notification_settings($payload);
196 176
197 177 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_74');
198 178 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_73');
199 179 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_52');
@@ -210,8 +190,54 @@
210 190 ];
211 191 }
212 192
213 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 + /**
214 240 * Save Facebook pixel settings
215 241 *
216 242 * @param $settings
217 243 *
@@ -229,42 +255,206 @@
229 255 Wpfnl_functions::update_admin_settings('_wpfunnels_recaptcha_setting', $recaptcha_settings );
230 256 }
231 257
232 258
233 - /**
234 - * Initialize all the settings value
235 - *
236 - * @since 1.0.0
237 - */
238 - public function init_settings()
239 - {
240 -
241 - $this->general_settings = Wpfnl_functions::get_general_settings();
242 -
243 - $this->is_allow_sales = Wpfnl_functions::maybe_allow_sales_funnel();
244 - $this->permalink_settings = Wpfnl_functions::get_permalink_settings();
245 - $this->offer_settings = Wpfnl_functions::get_offer_settings();
246 - $this->user_roles = Wpfnl_functions::get_user_roles();
247 - $this->gtm_events = Wpfnl_functions::get_gtm_events();
248 - $this->gtm_settings = Wpfnl_functions::get_gtm_settings();
249 - $this->facebook_pixel_events = Wpfnl_functions::get_facebook_events();
250 - $this->facebook_pixel_settings = Wpfnl_functions::get_facebook_pixel_settings();
251 - $this->utm_params = Wpfnl_functions::get_utm_params();
252 - $this->utm_settings = Wpfnl_functions::get_utm_settings();
253 - $this->utm_settings = Wpfnl_functions::get_utm_settings();
254 - $this->recaptcha_settings = Wpfnl_functions::get_recaptcha_settings();
259 + /**
260 + * Save user role management data
261 + *
262 + * @param $payload
263 + *
264 + * @since 3.1.4
265 + */
266 + public function save_user_role_management_data($payload) {
267 + if ( !isset($payload['user_role_management']) ) {
268 + return;
269 + }
255 270
256 -
257 - }
271 + $roles = wp_unslash( $payload['user_role_management']['roles'] );
272 + $sanitized_settings = array();
273 + foreach ( $roles as $key => $value ) {
274 + $sanitized_settings[ $key ] = ( isset( $roles[ $key ] ) ) ? sanitize_text_field( $value ) : '';
275 + }
276 + Wpfnl_functions::update_admin_settings('_wpfunnels_user_roles', $sanitized_settings );
258 277
278 + $this->update_user_role_management( $sanitized_settings );
279 + }
259 280
260 281
282 + /**
283 + * Update user role management data
284 + *
285 + * @param $sanitized_settings
286 + *
287 + * @since 3.1.4
288 + */
289 + private function update_user_role_management( $sanitized_settings ) {
261 290
291 + foreach ( $sanitized_settings as $user_role => $value ) {
292 + $user_role_obj = get_role( $user_role );
293 + if ( $user_role_obj ) {
294 + if ( 'yes' === $value ) {
295 + $user_role_obj->add_cap('wpf_manage_funnels' );
296 + } elseif ( 'no' === $value ) {
297 + $user_role_obj->remove_cap('wpf_manage_funnels' );
298 + }
299 + }
300 + }
301 + }
302 +
303 +
304 +
305 + /**
306 + * Save google map api key settings key
307 + *
308 + * @param $payload
309 + *
310 + * @since 3.1.3
311 + */
312 + public function save_google_map_key( $payload ) {
313 + if ( isset($payload['google_map_api_key']) ) {
314 + Wpfnl_functions::update_admin_settings('_wpfunnels_google_map_api_key', $payload['google_map_api_key'] );
315 + }
316 + }
317 +
318 +
319 + /**
320 + * Save LMS provider settings
321 + *
322 + * @param $payload
323 + *
324 + * @since 2.0.0
325 + */
326 + public function save_lms_settings( $payload ) {
327 + if ( isset($payload['lms_provider']) ) {
328 + $lms_settings = array(
329 + 'lms_provider' => sanitize_text_field( $payload['lms_provider'] )
330 + );
331 + update_option( '_wpfunnels_lms_settings', $lms_settings );
332 + }
333 + }
334 +
335 + /**
336 + * Save notification settings
337 + *
338 + * @param $payload
339 + *
340 + * @since 3.2.0
341 + */
342 + public function save_notification_settings( $payload ) {
343 + $old_settings = Wpfnl_functions::get_notification_settings();
344 + $old_enabled = isset($old_settings['enable_revenue_report']) ? $old_settings['enable_revenue_report'] : 'no';
345 +
346 + $notification_settings = array(
347 + 'enable_revenue_report' => isset($payload['enable_revenue_report']) ? sanitize_text_field($payload['enable_revenue_report']) : 'no',
348 + 'revenue_report_frequency' => isset($payload['revenue_report_frequency']) ? sanitize_text_field($payload['revenue_report_frequency']) : 'weekly',
349 + 'revenue_report_recipient' => isset($payload['revenue_report_recipient']) ? sanitize_text_field($payload['revenue_report_recipient']) : get_option('admin_email'),
350 + 'revenue_report_subject' => isset($payload['revenue_report_subject']) ? sanitize_text_field($payload['revenue_report_subject']) : 'Store Revenue Report - {period}',
351 + 'send_time' => isset($payload['send_time']) ? sanitize_text_field($payload['send_time']) : '10:00',
352 + );
353 +
354 + Wpfnl_functions::update_admin_settings('_wpfunnels_notification_settings', $notification_settings );
355 +
356 + $new_enabled = $notification_settings['enable_revenue_report'];
357 +
358 + // Handle action scheduler based on enable/disable
359 + if ( $new_enabled === 'yes' && $old_enabled !== 'yes' ) {
360 + // Enabled: Schedule the action
361 + $this->schedule_revenue_report_email( $notification_settings );
362 + } elseif ( $new_enabled !== 'yes' && $old_enabled === 'yes' ) {
363 + // Disabled: Remove the scheduled action
364 + $this->unschedule_revenue_report_email();
365 + } elseif ( $new_enabled === 'yes' ) {
366 + // Already enabled but settings changed: Reschedule
367 + $this->unschedule_revenue_report_email();
368 + $this->schedule_revenue_report_email( $notification_settings );
369 + }
370 + }
371 +
372 +
373 + /**
374 + * Schedule revenue report email using Action Scheduler
375 + *
376 + * @param array $settings
377 + *
378 + * @since 3.2.0
379 + */
380 + private function schedule_revenue_report_email( $settings ) {
381 + Wpfnl_functions::schedule_revenue_report_email( $settings );
382 + }
383 +
384 +
385 + /**
386 + * Unschedule revenue report email
387 + *
388 + * @since 3.2.0
389 + */
390 + private function unschedule_revenue_report_email() {
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' ) ) {
409 + return;
410 + }
411 +
412 + $settings = '\WPFunnels\AI\Settings\AISettings';
413 +
414 + foreach ( array( 'anthropic', 'openai', 'gemini' ) as $provider ) {
415 + $field = 'ai_' . $provider . '_key';
416 + $key = isset( $payload[ $field ] ) ? trim( sanitize_text_field( $payload[ $field ] ) ) : '';
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 + }
423 +
424 + call_user_func( array( $settings, 'connect' ), $provider, $key );
425 + }
426 +
427 + if ( isset( $payload['ai_provider'] ) && '' !== $payload['ai_provider'] ) {
428 + $provider = sanitize_text_field( $payload['ai_provider'] );
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 +
262 452 /**
263 453 * Clear saved templates data
264 454 *
265 455 * @param $payload
266 - *
456 + *
267 457 * @return array
268 458 * @since 1.0.0
269 459 */
270 460 public function clear_templates_data($payload) {
@@ -271,12 +461,20 @@
271 461
272 462 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_wc');
273 463 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_lms');
274 464
275 -
465 + $builder = Wpfnl_functions::get_builder_type();
466 +
276 467 delete_transient('wpfunnels_remote_template_data_wc_' . WPFNL_VERSION);
277 468 delete_transient('wpfunnels_remote_template_data_lms_' . WPFNL_VERSION);
278 469 delete_transient('wpfunnels_remote_template_data_lead_' . WPFNL_VERSION);
470 + delete_transient('wpfunnels_remote_template_data_store_checkout_' . WPFNL_VERSION);
471 +
472 + delete_transient('wpfunnels_remote_template_data_wc_'. $builder . '_' . WPFNL_VERSION);
473 + delete_transient('wpfunnels_remote_template_data_lms_'. $builder . '_' . WPFNL_VERSION);
474 + delete_transient('wpfunnels_remote_template_data_lead_'. $builder . '_' . WPFNL_VERSION);
475 + delete_transient('wpfunnels_remote_template_data_store_checkout_'. $builder . '_' . WPFNL_VERSION);
476 +
279 477 return [
280 478 'success' => true
281 479 ];
282 480 }
@@ -282,12 +480,12 @@
282 480 }
283 481
284 482
285 483 /**
286 - * Clear transient
287 - *
484 + * Clear transient
485 + *
288 486 * @param $payload
289 - *
487 + *
290 488 * @return array
291 489 */
292 490 public function clear_transient_cache_data($payload) {
293 491
@@ -294,8 +492,9 @@
294 492 delete_transient('wpfunnels_remote_template_data_' . WPFNL_VERSION);
295 493 delete_transient('wpfunnels_remote_template_data_wc_' . WPFNL_VERSION);
296 494 delete_transient('wpfunnels_remote_template_data_lms_' . WPFNL_VERSION);
297 495 delete_transient('wpfunnels_remote_template_data_lead_' . WPFNL_VERSION);
496 + delete_transient('wpfunnels_remote_template_data_store_checkout_' . WPFNL_VERSION);
298 497 delete_transient('wpfunnels_rollback_versions_' . WPFNL_VERSION);
299 498 do_action('wpfunnels/after_clear_transient');
300 499 return array(
301 500 'success' => true
@@ -309,29 +508,19 @@
309 508 * @since 1.0.0
310 509 */
311 510 public function after_permalink_settings_saved()
312 511 {
313 - $is_permalink_saved = get_option('_wpfunnels_permalink_saved');
314 - if ($is_permalink_saved) {
315 - flush_rewrite_rules();
316 - delete_option('_wpfunnels_permalink_saved');
512 +
513 + if( Wpfnl_functions::maybe_funnel_page() ){
514 + $is_permalink_saved = get_option('_wpfunnels_permalink_saved');
515 + if ($is_permalink_saved) {
516 + flush_rewrite_rules();
517 + delete_option('_wpfunnels_permalink_saved');
518 + }
317 519 }
318 520 }
319 521
320 522
321 - /**
322 - * Get settings by meta key
323 - *
324 - * @param $key
325 - *
326 - * @return mixed|string
327 - * @since 1.0.0
328 - */
329 - public function get_settings_by_key($key)
330 - {
331 - return isset($this->settings_meta_keys[$key]) ? $this->settings_meta_keys[$key]: '';
332 - }
333 -
334 523 public function get_name()
335 524 {
336 525 return __('settings','wpfnl');
337 526 }
@@ -429,32 +618,53 @@
429 618 ]
430 619 );
431 620 }
432 621
433 -
622 +
434 623 /**
435 624 * WPFunnels log
436 625 *
437 626 * @param $payload
438 - *
627 + *
439 628 * @return array
440 629 */
441 630 public static function wpfnl_show_log( $payload ) {
442 631 if( isset($payload['logKey']) ){
443 - $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 +
444 636 $upload_dir = wp_upload_dir( null, false );
445 - $log_url = $upload_dir['basedir'].'/wpfunnels-logs/';
446 - $file_url = $log_url . $key;
447 -
448 - ob_start();
449 - include_once $file_url;
450 - $out = ob_get_clean();
451 - ob_end_clean();
637 + $log_url = trailingslashit( $upload_dir['basedir'] ) . 'wpfunnels/wpfunnels-logs/';
638 + $file_url = $log_url . $key;
452 639
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;
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 +
453 663 return array(
454 - 'success' => true,
455 - 'content' => $out,
456 - 'file_url' => $log_url. $key
664 + 'success' => false,
665 + 'content' => '',
666 + 'file_url' => '',
457 667 );
458 668 }
459 669
460 670 return array(
@@ -461,33 +671,403 @@
461 671 'success' => false,
462 672 'content' => '',
463 673 'file_url' => ''
464 674 );
465 -
466 675
676 +
467 677 }
468 678
469 679
680 + /**
681 + * Send test notification email
682 + *
683 + * @param $payload
684 + *
685 + * @return array
686 + * @since 3.2.0
687 + */
688 + public function send_test_notification( $payload ) {
689 + if ( ! isset( $payload['email'] ) || ! is_email( $payload['email'] ) ) {
690 + return array(
691 + 'success' => false,
692 + 'message' => __( 'Please provide a valid email address.', 'wpfnl' )
693 + );
694 + }
695 +
696 + $settings = Wpfnl_functions::get_notification_settings();
697 + $subject_template = isset( $settings['revenue_report_subject'] ) ? $settings['revenue_report_subject'] : 'Store Revenue Report - {period}';
698 +
699 + // Calculate date range for test email (last week Monday to Sunday)
700 + $start_date = date( 'Y-m-d', strtotime( 'last monday -1 week' ) );
701 + $end_date = date( 'Y-m-d', strtotime( 'last sunday' ) );
702 + $date_range = date( 'F j, Y', strtotime( $start_date ) ) . ' - ' . date( 'F j, Y', strtotime( $end_date ) );
703 +
704 + $subject = '[Test] ' . str_replace( '{period}', $date_range, $subject_template );
705 +
706 + $to = sanitize_email( $payload['email'] );
707 + $message = $this->get_revenue_report_email_content( 'weekly' );
708 + $headers = array( 'Content-Type: text/html; charset=UTF-8' );
709 +
710 + $sent = wp_mail( $to, $subject, $message, $headers );
711 +
712 + if ( $sent ) {
713 + return array(
714 + 'success' => true,
715 + 'message' => __( 'Test email sent successfully! Please check your inbox.', 'wpfnl' )
716 + );
717 + } else {
718 + return array(
719 + 'success' => false,
720 + 'message' => __( 'Failed to send test email. Please check your email configuration.', 'wpfnl' )
721 + );
722 + }
723 + }
724 +
725 +
726 + /**
727 + * Send revenue report email (triggered by Action Scheduler)
728 + *
729 + * @since 3.2.0
730 + */
731 + public function send_revenue_report_email() {
732 + $settings = Wpfnl_functions::get_notification_settings();
733 +
734 + // Check if still enabled
735 + if ( ! isset( $settings['enable_revenue_report'] ) || $settings['enable_revenue_report'] !== 'yes' ) {
736 + return;
737 + }
738 +
739 + $recipients = isset( $settings['revenue_report_recipient'] ) ? $settings['revenue_report_recipient'] : get_option('admin_email');
740 + $subject = isset( $settings['revenue_report_subject'] ) ? $settings['revenue_report_subject'] : 'Store Revenue Report - {period}';
741 + $frequency = isset( $settings['revenue_report_frequency'] ) ? $settings['revenue_report_frequency'] : 'weekly';
742 +
743 + // Calculate date range based on frequency
744 + if ( $frequency === 'monthly' ) {
745 + $start_date = date( 'Y-m-d', strtotime( 'first day of last month' ) );
746 + $end_date = date( 'Y-m-d', strtotime( 'last day of last month' ) );
747 + } else {
748 + // Weekly: Last week Monday to Sunday
749 + $start_date = date( 'Y-m-d', strtotime( 'last monday -1 week' ) );
750 + $end_date = date( 'Y-m-d', strtotime( 'last sunday' ) );
751 + }
752 +
753 + $date_range = date( 'F j, Y', strtotime( $start_date ) ) . ' - ' . date( 'F j, Y', strtotime( $end_date ) );
754 +
755 + // Replace {period} placeholder with date range
756 + $subject = str_replace( '{period}', $date_range, $subject );
757 +
758 + // Generate report content
759 + $message = $this->get_revenue_report_email_content( $frequency );
760 +
761 + // Prepare headers
762 + $headers = array( 'Content-Type: text/html; charset=UTF-8' );
763 +
764 + // Send email to recipients (can be multiple, comma-separated)
765 + $recipient_array = array_map( 'trim', explode( ',', $recipients ) );
766 +
767 + foreach ( $recipient_array as $recipient ) {
768 + if ( is_email( $recipient ) ) {
769 + wp_mail( sanitize_email( $recipient ), $subject, $message, $headers );
770 + }
771 + }
772 + }
773 +
774 +
775 + /**
776 + * Get revenue report email content with actual data
777 + *
778 + * @param string $frequency
779 + * @return string
780 + * @since 3.2.0
781 + */
782 + private function get_revenue_report_email_content( $frequency = 'weekly' ) {
783 + $site_name = get_bloginfo( 'name' );
784 + $site_url = home_url();
785 +
786 + // Calculate date range
787 + if ( $frequency === 'monthly' ) {
788 + $start_date = date( 'Y-m-d', strtotime( 'first day of last month' ) );
789 + $end_date = date( 'Y-m-d', strtotime( 'last day of last month' ) );
790 + $period_text = __( 'Monthly', 'wpfnl' );
791 + } else {
792 + // Weekly: Last week Monday to Sunday
793 + $start_date = date( 'Y-m-d', strtotime( 'last monday -1 week' ) );
794 + $end_date = date( 'Y-m-d', strtotime( 'last sunday' ) );
795 + $period_text = __( 'Weekly', 'wpfnl' );
796 + }
797 +
798 + $date_range = date( 'F j, Y', strtotime( $start_date ) ) . ' - ' . date( 'F j, Y', strtotime( $end_date ) );
799 +
800 + // Calculate previous period for comparison
801 + if ( $frequency === 'monthly' ) {
802 + $prev_start_date = date( 'Y-m-d', strtotime( 'first day of -2 months' ) );
803 + $prev_end_date = date( 'Y-m-d', strtotime( 'last day of -2 months' ) );
804 + } else {
805 + $prev_start_date = date( 'Y-m-d', strtotime( 'last monday -2 weeks' ) );
806 + $prev_end_date = date( 'Y-m-d', strtotime( 'last sunday -1 week' ) );
807 + }
808 +
809 + // Get revenue data for current and previous periods
810 + $revenue_data = $this->get_revenue_data( $start_date, $end_date );
811 + $previous_data = $this->get_revenue_data( $prev_start_date, $prev_end_date );
812 +
813 + // Calculate percentage changes
814 + $revenue_data['total_orders_change'] = $this->calculate_percentage_change( $previous_data['total_orders'], $revenue_data['total_orders'] );
815 + $revenue_data['total_customers_change'] = $this->calculate_percentage_change( $previous_data['total_customers'], $revenue_data['total_customers'] );
816 + $revenue_data['total_revenue_change'] = $this->calculate_percentage_change( $previous_data['total_revenue_raw'], $revenue_data['total_revenue_raw'] );
817 + $revenue_data['average_order_value_change'] = $this->calculate_percentage_change( $previous_data['average_order_value_raw'], $revenue_data['average_order_value_raw'] );
818 +
819 + // Build email with FunnelKit-style design
820 + $message = '<!DOCTYPE html>';
821 + $message .= '<html lang="en">';
822 + $message .= '<head>';
823 + $message .= '<meta charset="UTF-8">';
824 + $message .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
825 + $message .= '<title>Performance Report</title>';
826 + $message .= '</head>';
827 + $message .= '<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', Arial, sans-serif; background-color: #f5f5f5;">';
828 +
829 + // Main container
830 + $message .= '<table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5; padding: 40px 20px;">';
831 + $message .= '<tr><td align="center">';
832 + $message .= '<table width="600" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">';
833 +
834 + // Top orange banner with text
835 + $message .= '<tr><td style="background-color: #6e42d3; padding: 12px 40px; text-align: center;">';
836 + $message .= '<div style="font-size: 16px; font-weight: 600; color: #ffffff;">Your ' . esc_html( strtolower( $frequency ) ) . ' summary from WPFunnels.</div>';
837 + $message .= '</td></tr>';
838 +
839 + // WPFunnels logo section
840 + $message .= '<tr><td style="background-color: #ffffff; padding: 20px 40px 20px; text-align: center;">';
841 + $message .= '<svg width="38" height="28" viewBox="0 0 38 28" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; margin-right: 10px; vertical-align: middle;">';
842 + $message .= '<path d="M7.01532 18H31.9847L34 11H5L7.01532 18Z" fill="#EE8134"/>';
843 + $message .= '<path d="M11.9621 27.2975C12.0923 27.7154 12.4792 28 12.9169 28H26.0831C26.5208 28 26.9077 27.7154 27.0379 27.2975L29 21H10L11.9621 27.2975Z" fill="#6E42D3"/>';
844 + $message .= '<path d="M37.8161 0.65986C37.61 0.247888 37.2609 0 36.8867 0H1.11326C0.739128 0 0.390003 0.247888 0.183972 0.65986C-0.0220592 1.07193 -0.0573873 1.59277 0.0898627 2.04655L1.69781 7H36.3022L37.9102 2.04655C38.0574 1.59287 38.022 1.07193 37.8161 0.65986Z" fill="#6E42D3"/>';
845 + $message .= '</svg>';
846 + $message .= '<span style="font-size: 32px; font-weight: bold; color: #231748; vertical-align: middle;">WPFunnels</span>';
847 + $message .= '</td></tr>';
848 +
849 + // Date range, Performance Report title, and View Button - all in same background
850 + $message .= '<tr><td style="padding: 20px 40px 30px; text-align: center; background-color: #f8f9fa;">';
851 + // Date range
852 + $message .= '<div style="font-size: 14px; color: #6c757d; font-weight: 500; margin-bottom: 20px;">' . esc_html( $date_range ) . '</div>';
853 + // Performance Report title
854 + $message .= '<h1 style="margin: 0 0 10px; font-size: 32px; font-weight: bold; color: #212529;">Store Revenue Summary</h1>';
855 + $message .= '<p style="margin: 0 0 20px; font-size: 14px; color: #6c757d;">Track your store performance and revenue metrics for this period</p>';
856 + // View Dashboard Button
857 + $message .= '<a href="' . esc_url( admin_url( 'admin.php?page=wpfunnels' ) ) . '" style="display: inline-block; background-color: #6e42d3; color: #ffffff; text-decoration: none; padding: 12px 30px; border-radius: 5px; font-size: 14px; font-weight: 600;">View Dashboard</a>';
858 + $message .= '</td></tr>';
859 +
860 + // Key Performance Metrics section
861 + $message .= '<tr><td style="padding: 20px 40px;">';
862 + $message .= '<h2 style="margin: 0 0 10px; font-size: 24px; font-weight: bold; color: #212529; text-align: center;">Revenue Overview</h2>';
863 + $message .= '<p style="margin: 0 0 30px; font-size: 14px; color: #6c757d; text-align: center;">Your ' . esc_html( strtolower( $frequency ) ) . ' performance snapshot</p>';
864 +
865 + // Metrics grid
866 + $message .= '<table width="100%" border="0" cellspacing="0" cellpadding="0">';
867 +
868 + // Row 1: Total orders and Total contacts
869 + $message .= '<tr>';
870 + $message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
871 + $message .= '<div style="text-align: left;">';
872 + $message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Total orders</div>';
873 + $message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . esc_html( $revenue_data['total_orders'] ) . '</div>';
874 + $message .= $revenue_data['total_orders_change'];
875 + $message .= '</div>';
876 + $message .= '</td>';
877 + $message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
878 + $message .= '<div style="text-align: left;">';
879 + $message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Total customers</div>';
880 + $message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . esc_html( $revenue_data['total_customers'] ) . '</div>';
881 + $message .= $revenue_data['total_customers_change'];
882 + $message .= '</div>';
883 + $message .= '</td>';
884 + $message .= '</tr>';
885 +
886 + // Row 2: Revenue and Bump revenue
887 + $message .= '<tr>';
888 + $message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
889 + $message .= '<div style="text-align: left;">';
890 + $message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Revenue</div>';
891 + $message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . wp_strip_all_tags( $revenue_data['total_revenue'] ) . '</div>';
892 + $message .= $revenue_data['total_revenue_change'];
893 + $message .= '</div>';
894 + $message .= '</td>';
895 + $message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
896 + $message .= '<div style="text-align: left;">';
897 + $message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Bump revenue</div>';
898 + if ( Wpfnl_functions::is_wpfnl_pro_activated() ) {
899 + $message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">0 <span style="font-size: 18px; font-weight: normal; color: #6c757d;">USD</span></div>';
900 + $message .= '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
901 + } else {
902 + $message .= '<div style="font-size: 36px; margin-bottom: 5px;"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17 11V7C17 4.79086 15.2091 3 13 3H11C8.79086 3 7 4.79086 7 7V11M12 14V16M8 11H16C17.1046 11 18 11.8954 18 13V19C18 20.1046 17.1046 21 16 21H8C6.89543 21 6 20.1046 6 19V13C6 11.8954 6.89543 11 8 11Z" stroke="#6c757d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
903 + $message .= '<div style="font-size: 12px; color: #6c757d;">WPFunnels Pro</div>';
904 + }
905 + $message .= '</div>';
906 + $message .= '</td>';
907 + $message .= '</tr>';
908 +
909 + // Row 3: Upsell revenue and Average order value
910 + $message .= '<tr>';
911 + $message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
912 + $message .= '<div style="text-align: left;">';
913 + $message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Upsell revenue</div>';
914 + if ( Wpfnl_functions::is_wpfnl_pro_activated() ) {
915 + $message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">0 <span style="font-size: 18px; font-weight: normal; color: #6c757d;">USD</span></div>';
916 + $message .= '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
917 + } else {
918 + $message .= '<div style="font-size: 36px; margin-bottom: 5px;"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17 11V7C17 4.79086 15.2091 3 13 3H11C8.79086 3 7 4.79086 7 7V11M12 14V16M8 11H16C17.1046 11 18 11.8954 18 13V19C18 20.1046 17.1046 21 16 21H8C6.89543 21 6 20.1046 6 19V13C6 11.8954 6.89543 11 8 11Z" stroke="#6c757d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
919 + $message .= '<div style="font-size: 12px; color: #6c757d;">WPFunnels Pro</div>';
920 + }
921 + $message .= '</div>';
922 + $message .= '</td>';
923 + $message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
924 + $message .= '<div style="text-align: left;">';
925 + $message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Average order value</div>';
926 + $message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . wp_strip_all_tags( $revenue_data['average_order_value'] ) . '</div>';
927 + $message .= $revenue_data['average_order_value_change'];
928 + $message .= '</div>';
929 + $message .= '</td>';
930 + $message .= '</tr>';
931 +
932 + $message .= '</table>';
933 + $message .= '</td></tr>';
934 +
935 + // Additional revenue info
936 + if ( isset( $revenue_data['total_revenue_raw'] ) && $revenue_data['total_revenue_raw'] > 0 ) {
937 + $message .= '<tr><td style="padding: 20px 40px;">';
938 + $message .= '<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 4px;">';
939 + $message .= '<p style="margin: 0; font-size: 14px; color: #856404;">Since installing WPFunnels you have captured additional revenue of <strong>' . wp_kses_post( wc_price( $revenue_data['total_revenue_raw'] ) ) . '</strong></p>';
940 + $message .= '</div>';
941 + $message .= '</td></tr>';
942 + }
943 +
944 + // Footer
945 + $message .= '<tr><td style="padding: 30px 40px; text-align: center; background-color: #ffffff; border-top: 1px solid #e9ecef;">';
946 + $message .= '<p style="margin: 0 0 10px; font-size: 12px; color: #6c757d;">This performance report email was sent from the <a href="' . esc_url( $site_url ) . '" style="color: #007bff; text-decoration: none;">' . esc_html( $site_name ) . '</a> by admin ' . esc_html( date( 'F j, Y' ) ) . '</p>';
947 + $message .= '<p style="margin: 0; font-size: 12px; color: #6c757d;"><a href="' . esc_url( admin_url( 'admin.php?page=wpfnl_settings#notifications-settings' ) ) . '" style="color: #007bff; text-decoration: none;">Click here</a> to change the frequency and recipients.</p>';
948 + $message .= '</td></tr>';
949 +
950 + $message .= '</table>';
951 + $message .= '</td></tr>';
952 + $message .= '</table>';
953 +
954 + $message .= '</body>';
955 + $message .= '</html>';
956 +
957 + return $message;
958 + }
959 +
960 +
961 + /**
962 + * Get revenue data from WPFunnels stats
963 + *
964 + * @param string $start_date
965 + * @param string $end_date
966 + * @return array
967 + * @since 3.2.0
968 + */
969 + private function get_revenue_data( $start_date, $end_date ) {
970 + global $wpdb;
971 + $table = $wpdb->prefix . 'wpfnl_stats';
972 +
973 + // Get total orders count
974 + $total_orders = $wpdb->get_var( $wpdb->prepare(
975 + "SELECT COUNT(id) FROM $table WHERE date_created >= %s AND date_created <= %s AND status = 'completed'",
976 + $start_date,
977 + $end_date . ' 23:59:59'
978 + ) );
979 +
980 + // Get total revenue (total_sales includes everything: checkout + order bump + upsell + downsell)
981 + $total_revenue_raw = $wpdb->get_var( $wpdb->prepare(
982 + "SELECT SUM(total_sales) FROM $table WHERE date_created >= %s AND date_created <= %s AND status = 'completed'",
983 + $start_date,
984 + $end_date . ' 23:59:59'
985 + ) );
986 +
987 + // Get total customers (unique customer_id)
988 + $total_customers = $wpdb->get_var( $wpdb->prepare(
989 + "SELECT COUNT(DISTINCT customer_id) FROM $table WHERE date_created >= %s AND date_created <= %s AND status = 'completed'",
990 + $start_date,
991 + $end_date . ' 23:59:59'
992 + ) );
993 +
994 + // Calculate average order value
995 + $total_orders_int = intval( $total_orders );
996 + $total_revenue_float = floatval( $total_revenue_raw );
997 + $average = $total_orders_int > 0 ? $total_revenue_float / $total_orders_int : 0;
998 +
999 + // Format data
1000 + $data = array(
1001 + 'total_revenue' => function_exists( 'wc_price' ) ? wc_price( $total_revenue_float ) : '$' . number_format( $total_revenue_float, 2 ),
1002 + 'total_revenue_raw' => $total_revenue_float,
1003 + 'total_orders' => $total_orders_int,
1004 + 'total_customers' => intval( $total_customers ),
1005 + 'average_order_value' => function_exists( 'wc_price' ) ? wc_price( $average ) : '$' . number_format( $average, 2 ),
1006 + 'average_order_value_raw' => $average,
1007 + );
1008 +
1009 + return $data;
1010 + }
1011 +
1012 +
1013 + /**
1014 + * Calculate percentage change between two values
1015 + *
1016 + * @param float $old_value
1017 + * @param float $new_value
1018 + * @return string HTML formatted percentage change
1019 + * @since 3.2.0
1020 + */
1021 + private function calculate_percentage_change( $old_value, $new_value ) {
1022 + $old_value = floatval( $old_value );
1023 + $new_value = floatval( $new_value );
1024 +
1025 + if ( $old_value == 0 ) {
1026 + if ( $new_value > 0 ) {
1027 + return '<div style="font-size: 12px; color: #28a745;">+100% - Previous period</div>';
1028 + } else {
1029 + return '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
1030 + }
1031 + }
1032 +
1033 + $change = ( ( $new_value - $old_value ) / $old_value ) * 100;
1034 + $change_formatted = number_format( abs( $change ), 1 );
1035 +
1036 + if ( $change > 0 ) {
1037 + return '<div style="font-size: 12px; color: #28a745;">+' . $change_formatted . '% - Previous period</div>';
1038 + } elseif ( $change < 0 ) {
1039 + return '<div style="font-size: 12px; color: #dc3545;">-' . $change_formatted . '% - Previous period</div>';
1040 + } else {
1041 + return '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
1042 + }
1043 + }
1044 +
1045 +
470 1046 /**
471 1047 * WPFunnels log
472 1048 *
473 1049 * @param $payload
474 - *
1050 + *
475 1051 * @return array
476 1052 */
477 1053 public static function wpfnl_delete_log( $payload ) {
478 1054 if( isset($payload['logKey']) ){
479 - $key = $payload['logKey'];
1055 + $key = sanitize_file_name( basename( $payload['logKey'] ) );
480 1056 $upload_dir = wp_upload_dir( null, false );
481 - $file_name = $upload_dir['basedir'].'/wpfunnels-logs/'.$key;
1057 + $log_dir = $upload_dir['basedir'].'/wpfunnels/wpfunnels-logs/';
1058 + $file_name = $log_dir . $key;
1059 +
1060 + // Verify the resolved path is within the log directory
1061 + $real_file = realpath($file_name);
1062 + $real_log_dir = realpath($log_dir);
482 1063
483 - $response = \Wpfnl_Logger::delete_log_file( $file_name );
484 - if( $response ){
485 - return array(
486 - 'success' => true,
487 - );
1064 + if ( $real_file && $real_log_dir && strpos( $real_file, $real_log_dir ) === 0 ) {
1065 + $response = \Wpfnl_Logger::delete_log_file( $file_name );
1066 + if( $response ){
1067 + return array('success' => true);
1068 + }
488 1069 }
489 -
490 1070 }
491 1071
492 1072 return array(
493 1073 'success' => false,
@@ -493,8 +1073,6 @@
493 1073 'success' => false,
494 1074 'content' => '',
495 1075 'file_url' => ''
496 1076 );
497 -
498 -
499 1077 }
500 1078 }