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
wpfunnels / admin / modules / settings / class-wpfnl-settings.php

class-wpfnl-settings.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.3, at admin/modules/settings/class-wpfnl-settings.php

1,079 lines 42.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings module
4 *
5 * @package
6 */
7
8 namespace WPFunnels\Modules\Admin\Settings;
9
10 use WPFunnels\Admin\Module\Wpfnl_Admin_Module;
11 use WPFunnels\Rollback;
12 use WPFunnels\Traits\SingletonTrait;
13 use WPFunnels\Wpfnl_functions;
14
15 class Module extends Wpfnl_Admin_Module
16 {
17 use SingletonTrait;
18
19 protected $validations;
20
21 protected $prefix = '_wpfunnels_';
22
23 public function __construct()
24 {
25 add_action('admin_init', [$this, 'after_permalink_settings_saved']);
26 add_action( 'admin_post_wpfunnels_rollback', [ $this, 'post_wpfunnels_rollback' ] );
27 add_action( 'wpfunnels_send_revenue_report_email', [ $this, 'send_revenue_report_email' ] );
28 $this->init_ajax();
29 }
30
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()
44 {
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 );
55
56 require WPFNL_ADMIN_DIR . 'views/react-admin.php';
57 }
58
59 /**
60 * Init ajax hooks for
61 * saving metas
62 *
63 * @since 1.0.0
64 */
65 public function init_ajax()
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 */
76 $this->validations = [
77 'logged_in' => true,
78 'user_can' => 'wpf_manage_funnels',
79 ];
80 wp_ajax_helper()->handle('update-general-settings')
81 ->with_callback([ $this, 'update_general_settings' ])
82 ->with_validation($this->validations);
83
84 wp_ajax_helper()->handle('clear-templates')
85 ->with_callback([ $this, 'clear_templates_data' ])
86 ->with_validation($this->validations);
87
88 }
89
90
91 /**
92 * Update handler for settings
93 * page
94 *
95 * @param $payload
96 *
97 * @return array
98 * @since 1.0.0
99 */
100 public function update_general_settings($payload)
101 {
102 do_action('wpfunnels/before_settings_saved', $payload);
103
104 $this->save_ai_settings($payload);
105 $existing_settings = Wpfnl_functions::get_general_settings();
106 $general_settings = [
107 'builder' => sanitize_text_field($payload['builder']),
108 'uninstall_cleanup' => isset($payload['uninstall_cleanup']) ? sanitize_text_field($payload['uninstall_cleanup']) : 'off',
109 'disable_analytics' => isset($payload['analytics_roles']) ? $payload['analytics_roles'] : '',
110 'allow_funnels' => isset($payload['permission_role']) ? $payload['permission_role'] : [],
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(),
119 ];
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
132 $permalink_settings = [
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,
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
149 foreach ($payload as $key => $value) {
150 switch ($key) {
151 case 'builder':
152 $cache_key = 'wpfunnels_remote_template_data_' . WPFNL_VERSION;
153 delete_transient($cache_key);
154 delete_option(WPFNL_TEMPLATES_OPTION_KEY);
155 break;
156 case 'permalink_settings':
157 Wpfnl_functions::update_admin_settings($this->prefix.'permalink_saved', true);
158 break;
159 case 'advanced_settings':
160 Wpfnl_functions::update_admin_settings($this->prefix.'advanced_settings', $value);
161 break;
162 default:
163 break;
164 }
165 }
166
167 Wpfnl_functions::update_admin_settings($this->prefix.'general_settings', $general_settings);
168 Wpfnl_functions::update_admin_settings($this->prefix.'permalink_settings', $permalink_settings);
169 Wpfnl_functions::update_admin_settings($this->prefix.'optin_settings', $optin_settings);
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);
176
177 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_74');
178 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_73');
179 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_52');
180 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_53');
181
182 delete_transient('wpfunnels_remote_template_data_74_' . WPFNL_VERSION);
183 delete_transient('wpfunnels_remote_template_data_73_' . WPFNL_VERSION);
184 delete_transient('wpfunnels_remote_template_data_52_' . WPFNL_VERSION);
185 delete_transient('wpfunnels_remote_template_data_53_' . WPFNL_VERSION);
186
187 do_action('wpfunnels/after_settings_saved', $payload );
188 return [
189 'success' => true
190 ];
191 }
192
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 /**
240 * Save Facebook pixel settings
241 *
242 * @param $settings
243 *
244 * @since 1.0.0
245 */
246 public function save_recaptcha_settings( $settings ) {
247 $enable_recapcha = isset($settings['enable_recaptcha']) ? $settings['enable_recaptcha'] : 'no';
248 $recapcha_site_key = isset($settings['recaptcha_site_key']) ? $settings['recaptcha_site_key'] : '';
249 $recapcha_site_secret = isset($settings['recaptcha_site_secret']) ? $settings['recaptcha_site_secret'] : '';
250 $recaptcha_settings = array(
251 'enable_recaptcha' => $enable_recapcha,
252 'recaptcha_site_key' => $recapcha_site_key,
253 'recaptcha_site_secret' => $recapcha_site_secret
254 );
255 Wpfnl_functions::update_admin_settings('_wpfunnels_recaptcha_setting', $recaptcha_settings );
256 }
257
258
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 }
270
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 );
277
278 $this->update_user_role_management( $sanitized_settings );
279 }
280
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 ) {
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
452 /**
453 * Clear saved templates data
454 *
455 * @param $payload
456 *
457 * @return array
458 * @since 1.0.0
459 */
460 public function clear_templates_data($payload) {
461
462 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_wc');
463 delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_lms');
464
465 $builder = Wpfnl_functions::get_builder_type();
466
467 delete_transient('wpfunnels_remote_template_data_wc_' . WPFNL_VERSION);
468 delete_transient('wpfunnels_remote_template_data_lms_' . WPFNL_VERSION);
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
477 return [
478 'success' => true
479 ];
480 }
481
482
483 /**
484 * Clear transient
485 *
486 * @param $payload
487 *
488 * @return array
489 */
490 public function clear_transient_cache_data($payload) {
491
492 delete_transient('wpfunnels_remote_template_data_' . WPFNL_VERSION);
493 delete_transient('wpfunnels_remote_template_data_wc_' . WPFNL_VERSION);
494 delete_transient('wpfunnels_remote_template_data_lms_' . WPFNL_VERSION);
495 delete_transient('wpfunnels_remote_template_data_lead_' . WPFNL_VERSION);
496 delete_transient('wpfunnels_remote_template_data_store_checkout_' . WPFNL_VERSION);
497 delete_transient('wpfunnels_rollback_versions_' . WPFNL_VERSION);
498 do_action('wpfunnels/after_clear_transient');
499 return array(
500 'success' => true
501 );
502 }
503
504
505 /**
506 * After settings saved hooks
507 *
508 * @since 1.0.0
509 */
510 public function after_permalink_settings_saved()
511 {
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 }
519 }
520 }
521
522
523 public function get_name()
524 {
525 return __('settings','wpfnl');
526 }
527
528
529 /**
530 * Get rollback version of WPF
531 *
532 * @return array|mixed
533 * @since 2.3.0
534 */
535 public function get_roll_back_versions() {
536 $rollback_versions = get_transient( 'wpfunnels_rollback_versions_' . WPFNL_VERSION );
537 if ( false === $rollback_versions ) {
538 $max_versions = 10;
539 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
540 $plugin_information = plugins_api(
541 'plugin_information', [
542 'slug' => 'wpfunnels',
543 ]
544 );
545 if ( empty( $plugin_information->versions ) || ! is_array( $plugin_information->versions ) ) {
546 return [];
547 }
548
549 natsort( $plugin_information->versions );
550 $plugin_information->versions = array_reverse($plugin_information->versions);
551 $rollback_versions = [];
552
553 $current_index = 0;
554 foreach ( $plugin_information->versions as $version => $download_link ) {
555 if ( $max_versions <= $current_index ) {
556 break;
557 }
558
559 $lowercase_version = strtolower( $version );
560 $is_valid_rollback_version = ! preg_match( '/(trunk|beta|rc|dev)/i', $lowercase_version );
561
562 /**
563 * Is rollback version is valid.
564 *
565 * Filters the check whether the rollback version is valid.
566 *
567 * @param bool $is_valid_rollback_version Whether the rollback version is valid.
568 */
569 $is_valid_rollback_version = apply_filters(
570 'wpfunnels/is_valid_rollback_version',
571 $is_valid_rollback_version,
572 $lowercase_version
573 );
574
575 if ( ! $is_valid_rollback_version ) {
576 continue;
577 }
578
579 if ( version_compare( $version, WPFNL_VERSION, '>=' ) ) {
580 continue;
581 }
582
583 $current_index++;
584 $rollback_versions[] = $version;
585 }
586
587 set_transient( 'wpfunnels_rollback_versions_' . WPFNL_VERSION, $rollback_versions, DAY_IN_SECONDS );
588 }
589
590 return $rollback_versions;
591 }
592
593
594 public function post_wpfunnels_rollback() {
595 check_admin_referer( 'wpfunnels_rollback' );
596
597 $rollback_versions = $this->get_roll_back_versions();
598 if ( empty( $_GET['version'] ) || ! in_array( $_GET['version'], $rollback_versions ) ) {
599 wp_die( esc_html__( 'Error occurred, The version selected is invalid. Try selecting different version.', 'wpfnl' ) );
600 }
601
602 $plugin_slug = basename( 'wpfunnels', '.php' );
603
604 $rollback = new Rollback(
605 [
606 'version' => $_GET['version'],
607 'plugin_name' => WPFNL_BASE,
608 'plugin_slug' => $plugin_slug,
609 'package_url' => sprintf( 'https://downloads.wordpress.org/plugin/%1s.%2s.zip', $plugin_slug, $_GET['version'] ),
610 ]
611 );
612
613 $rollback->run();
614
615 wp_die(
616 '', esc_html__( 'Rollback to Previous Version', 'wpfnl' ), [
617 'response' => 200,
618 ]
619 );
620 }
621
622
623 /**
624 * WPFunnels log
625 *
626 * @param $payload
627 *
628 * @return array
629 */
630 public static function wpfnl_show_log( $payload ) {
631 if( isset($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
636 $upload_dir = wp_upload_dir( null, false );
637 $log_url = trailingslashit( $upload_dir['basedir'] ) . 'wpfunnels/wpfunnels-logs/';
638 $file_url = $log_url . $key;
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
663 return array(
664 'success' => false,
665 'content' => '',
666 'file_url' => '',
667 );
668 }
669
670 return array(
671 'success' => false,
672 'content' => '',
673 'file_url' => ''
674 );
675
676
677 }
678
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
1046 /**
1047 * WPFunnels log
1048 *
1049 * @param $payload
1050 *
1051 * @return array
1052 */
1053 public static function wpfnl_delete_log( $payload ) {
1054 if( isset($payload['logKey']) ){
1055 $key = sanitize_file_name( basename( $payload['logKey'] ) );
1056 $upload_dir = wp_upload_dir( null, false );
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);
1063
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 }
1069 }
1070 }
1071
1072 return array(
1073 'success' => false,
1074 'content' => '',
1075 'file_url' => ''
1076 );
1077 }
1078 }
1079