PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
← All changes | includes/extensions/Fomo_Notifications/Fomo_Notifications.php +551 -94 51.1.44 → 51.1.86 View file →
@@ -182,32 +182,79 @@
182 182 * @return void
183 183 */
184 184 public function register_post_meta(): void
185 185 {
186 - $meta_fields = [
186 + // Simple string fields
187 + $string_fields = [
187 188 '_kng_fomo_status',
188 189 '_kng_fomo_type',
189 190 '_kng_fomo_source',
191 + ];
192 +
193 + foreach ($string_fields as $meta_key) {
194 + register_post_meta(self::POST_TYPE, $meta_key, [
195 + 'type' => 'string',
196 + 'single' => true,
197 + 'show_in_rest' => false,
198 + 'sanitize_callback' => 'sanitize_text_field',
199 + ]);
200 + }
201 +
202 + // JSON fields — sanitize_text_field would break JSON, so use a custom callback
203 + $json_fields = [
190 204 '_kng_fomo_source_config',
191 205 '_kng_fomo_design',
192 206 '_kng_fomo_content',
193 207 '_kng_fomo_display',
194 208 '_kng_fomo_customize',
209 + ];
210 +
211 + foreach ($json_fields as $meta_key) {
212 + register_post_meta(self::POST_TYPE, $meta_key, [
213 + 'type' => 'string',
214 + 'single' => true,
215 + 'show_in_rest' => false,
216 + 'sanitize_callback' => [$this, 'sanitize_json_meta'],
217 + ]);
218 + }
219 +
220 + // Numeric fields
221 + $numeric_fields = [
195 222 '_kng_fomo_views',
196 223 '_kng_fomo_clicks',
197 224 ];
198 225
199 - foreach ($meta_fields as $meta_key) {
226 + foreach ($numeric_fields as $meta_key) {
200 227 register_post_meta(self::POST_TYPE, $meta_key, [
201 228 'type' => 'string',
202 229 'single' => true,
203 230 'show_in_rest' => false,
204 - 'sanitize_callback' => 'sanitize_text_field',
231 + 'sanitize_callback' => 'absint',
205 232 ]);
206 233 }
207 234 }
208 235
209 236 /**
237 + * Sanitize JSON meta value
238 + *
239 + * @param mixed $value The value to sanitize
240 + * @return string
241 + */
242 + public function sanitize_json_meta($value): string
243 + {
244 + if (is_string($value)) {
245 + $decoded = json_decode($value, true);
246 + if (json_last_error() === JSON_ERROR_NONE) {
247 + return wp_json_encode($decoded);
248 + }
249 + }
250 + if (is_array($value)) {
251 + return wp_json_encode($value);
252 + }
253 + return '{}';
254 + }
255 +
256 + /**
210 257 * Maybe create stats table (runs once)
211 258 *
212 259 * @return void
213 260 */
@@ -315,8 +362,19 @@
315 362 'listUrl' => admin_url('admin.php?page=king-addons-fomo&view=list'),
316 363 'hasPro' => self::hasPro(),
317 364 'freeLimit' => self::FREE_LIMIT,
318 365 'upgradeUrl' => 'https://kingaddons.com/pricing/?utm_source=kng-fomo-notifications&utm_medium=plugin',
366 + 'typeDefaults' => [
367 + 'notification_bar' => self::get_type_defaults('notification_bar'),
368 + 'woocommerce_sales' => self::get_type_defaults('woocommerce_sales'),
369 + 'wordpress_comments' => self::get_type_defaults('wordpress_comments'),
370 + 'wporg_downloads' => self::get_type_defaults('wporg_downloads'),
371 + 'reviews' => self::get_type_defaults('reviews'),
372 + 'email_subscription' => self::get_type_defaults('email_subscription'),
373 + 'donations' => self::get_type_defaults('donations'),
374 + 'flashing_tab' => self::get_type_defaults('flashing_tab'),
375 + 'custom_csv' => self::get_type_defaults('custom_csv'),
376 + ],
319 377 'i18n' => [
320 378 'confirmDelete' => __('Are you sure you want to delete this notification?', 'king-addons'),
321 379 'saved' => __('Notification saved successfully!', 'king-addons'),
322 380 'deleted' => __('Notification deleted.', 'king-addons'),
@@ -340,9 +398,10 @@
340 398 */
341 399 public function enqueue_frontend_assets(): void
342 400 {
343 401 // Check if there are active notifications for current page
344 - if (!$this->has_active_notifications()) {
402 + $notifications = $this->get_active_notifications_for_page();
403 + if (empty($notifications)) {
345 404 return;
346 405 }
347 406
348 407 wp_enqueue_style(
@@ -354,16 +413,33 @@
354 413
355 414 wp_enqueue_script(
356 415 'kng-fomo-frontend',
357 416 $this->url . 'assets/frontend.js',
358 - ['jquery'],
417 + [],
359 418 self::VERSION,
360 419 true
361 420 );
362 421
363 - wp_localize_script('kng-fomo-frontend', 'kngFomoFrontend', [
422 + // Prepare notifications data for JS
423 + $notifications_data = [];
424 + $settings = $this->get_settings();
425 + foreach ($notifications as $notification) {
426 + $notifications_data[] = $this->prepare_notification_for_frontend($notification);
427 + }
428 +
429 + wp_localize_script('kng-fomo-frontend', 'kngFomoData', [
364 430 'ajaxUrl' => admin_url('admin-ajax.php'),
365 431 'nonce' => wp_create_nonce('kng_fomo_frontend'),
432 + 'notifications' => $notifications_data,
433 + 'settings' => [
434 + 'position' => 'bottom-left',
435 + 'delayBefore' => 5,
436 + 'displayFor' => 5,
437 + 'delayBetween' => 5,
438 + 'sessionLimit' => 0,
439 + 'soundVolume' => (int)($settings['sound_volume'] ?? 50),
440 + 'loop' => true,
441 + ],
366 442 ]);
367 443 }
368 444
369 445 /**
@@ -394,12 +470,12 @@
394 470 $notification_count = count($notifications);
395 471 $at_limit = !$has_pro && $notification_count >= self::FREE_LIMIT;
396 472 $settings = $this->get_settings();
397 473 $notification = null;
398 - $is_edit = $view === 'edit';
474 + $is_edit = ($view === 'edit' || ($view === 'wizard' && isset($_GET['edit'])));
399 475
400 - if ($is_edit && isset($_GET['id'])) {
401 - $notification = $this->get_notification((int)$_GET['id']);
476 + if ($is_edit && isset($_GET['edit'])) {
477 + $notification = $this->get_notification((int)$_GET['edit']);
402 478 }
403 479
404 480 include $template_path;
405 481 }
@@ -473,9 +549,9 @@
473 549 return [
474 550 'id' => $post->ID,
475 551 'title' => $post->post_title,
476 552 'status' => get_post_meta($post->ID, '_kng_fomo_status', true) ?: 'disabled',
477 - 'type' => get_post_meta($post->ID, '_kng_fomo_type', true) ?: 'notification-bar',
553 + 'type' => get_post_meta($post->ID, '_kng_fomo_type', true) ?: 'notification_bar',
478 554 'source' => get_post_meta($post->ID, '_kng_fomo_source', true) ?: 'manual',
479 555 'source_config' => json_decode(get_post_meta($post->ID, '_kng_fomo_source_config', true) ?: '{}', true),
480 556 'design' => json_decode(get_post_meta($post->ID, '_kng_fomo_design', true) ?: '{}', true),
481 557 'content' => json_decode(get_post_meta($post->ID, '_kng_fomo_content', true) ?: '{}', true),
@@ -500,23 +576,21 @@
500 576 'enabled' => true,
501 577 'tracking_enabled' => true,
502 578 'track_for' => 'everyone',
503 579 'exclude_bots' => true,
504 - 'cache_ttl' => 3600,
580 + 'cache_ttl' => 300,
581 + 'anonymize_names' => false,
582 + 'sound_volume' => 50,
505 583 'modules' => [
506 - 'notification-bar' => true,
507 - 'woocommerce-sales' => true,
508 - 'wordpress-comments' => true,
509 - 'wporg-downloads' => true,
510 - 'wporg-reviews' => false,
511 - 'google-reviews' => false,
512 - 'email-subscription' => false,
513 - 'elearning' => false,
584 + 'notification_bar' => true,
585 + 'woocommerce_sales' => true,
586 + 'wordpress_comments' => true,
587 + 'wporg_downloads' => true,
588 + 'reviews' => false,
589 + 'email_subscription' => false,
514 590 'donations' => false,
515 - 'discount-alert' => false,
516 - 'flashing-tab' => false,
517 - 'custom-csv' => false,
518 - 'page-analytics' => false,
591 + 'flashing_tab' => false,
592 + 'custom_csv' => false,
519 593 ],
520 594 ];
521 595
522 596 $saved = get_option('kng_fomo_settings', []);
@@ -534,24 +608,8 @@
534 608 return update_option('kng_fomo_settings', $settings);
535 609 }
536 610
537 611 /**
538 - * Check if there are active notifications for current page
539 - *
540 - * @return bool
541 - */
542 - private function has_active_notifications(): bool
543 - {
544 - $settings = $this->get_settings();
545 - if (empty($settings['enabled'])) {
546 - return false;
547 - }
548 -
549 - $notifications = $this->get_active_notifications_for_page();
550 - return !empty($notifications);
551 - }
552 -
553 - /**
554 612 * Get active notifications that should display on current page
555 613 *
556 614 * @return array
557 615 */
@@ -588,9 +646,9 @@
588 646 if (!$this->check_device_visibility($visibility)) {
589 647 return false;
590 648 }
591 649
592 - // Check show_on rules
650 + // Legacy show_on rules
593 651 $show_on = $display['show_on'] ?? 'everywhere';
594 652 if ($show_on === 'include' && !empty($display['include_pages'])) {
595 653 if (!$this->is_current_page_in_list($display['include_pages'])) {
596 654 return false;
@@ -600,13 +658,49 @@
600 658 return false;
601 659 }
602 660 }
603 661
604 - // Check display_for (audience)
605 - $display_for = $display['display_for'] ?? 'everyone';
606 - if ($display_for === 'guests' && is_user_logged_in()) {
662 + // Wizard page rules: pages + page_rules
663 + $pages_mode = $display['pages'] ?? 'all';
664 + if ($pages_mode === 'specific') {
665 + $page_rules = $display['page_rules'] ?? [];
666 + if (is_array($page_rules) && !empty($page_rules)) {
667 + $has_include = false;
668 + $include_matched = false;
669 +
670 + foreach ($page_rules as $rule) {
671 + if (!is_array($rule)) {
672 + continue;
673 + }
674 +
675 + $type = sanitize_text_field((string)($rule['type'] ?? 'include'));
676 + $matched = $this->match_page_rule($rule);
677 +
678 + if ($type === 'exclude' && $matched) {
679 + return false;
680 + }
681 +
682 + if ($type === 'include') {
683 + $has_include = true;
684 + if ($matched) {
685 + $include_matched = true;
686 + }
687 + }
688 + }
689 +
690 + if ($has_include && !$include_matched) {
691 + return false;
692 + }
693 + }
694 + }
695 +
696 + // Check display_for / audience (supports both legacy and wizard keys)
697 + $display_for = $display['display_for'] ?? ($display['audience'] ?? 'everyone');
698 +
699 + if (($display_for === 'guests' || $display_for === 'logged_out') && is_user_logged_in()) {
607 700 return false;
608 701 }
702 +
609 703 if ($display_for === 'logged_in' && !is_user_logged_in()) {
610 704 return false;
611 705 }
612 706
@@ -613,8 +707,53 @@
613 707 return true;
614 708 }
615 709
616 710 /**
711 + * Match a single wizard page rule against current request
712 + *
713 + * @param array $rule Page rule
714 + * @return bool
715 + */
716 + private function match_page_rule(array $rule): bool
717 + {
718 + $condition = sanitize_text_field((string)($rule['condition'] ?? 'url_contains'));
719 + $value = trim((string)($rule['value'] ?? ''));
720 +
721 + if ($value === '') {
722 + return false;
723 + }
724 +
725 + if ($condition === 'page' || $condition === 'post') {
726 + if (is_numeric($value)) {
727 + return ((int)get_queried_object_id()) === (int)$value;
728 + }
729 + return false;
730 + }
731 +
732 + $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash((string)$_SERVER['REQUEST_URI']) : '';
733 + $current_url = strtolower(home_url($request_uri));
734 + $needle = strtolower($value);
735 +
736 + if ($condition === 'url_is') {
737 + // Exact full URL
738 + if ($current_url === $needle) {
739 + return true;
740 + }
741 +
742 + // Exact path (with or without leading slash)
743 + $current_path = strtolower((string)wp_parse_url($current_url, PHP_URL_PATH));
744 + $needle_path = strtolower((string)wp_parse_url($needle, PHP_URL_PATH));
745 + if ($needle_path === '' && strpos($needle, '/') === false) {
746 + $needle_path = '/' . ltrim($needle, '/');
747 + }
748 + return $needle_path !== '' && $current_path === $needle_path;
749 + }
750 +
751 + // Default: url_contains
752 + return strpos($current_url, $needle) !== false;
753 + }
754 +
755 + /**
617 756 * Check device visibility based on screen width
618 757 *
619 758 * @param array $visibility Visibility settings
620 759 * @return bool
@@ -643,20 +782,16 @@
643 782 * @return void
644 783 */
645 784 public function render_notifications(): void
646 785 {
786 + // Notifications data is passed via wp_localize_script in enqueue_frontend_assets().
787 + // This method only outputs the container div for popup notifications.
647 788 $notifications = $this->get_active_notifications_for_page();
648 789 if (empty($notifications)) {
649 790 return;
650 791 }
651 792
652 - // Prepare notifications data for JS
653 - $notifications_data = [];
654 - foreach ($notifications as $notification) {
655 - $notifications_data[] = $this->prepare_notification_for_frontend($notification);
656 - }
657 -
658 - echo '<div id="kng-fomo-container" data-notifications="' . esc_attr(wp_json_encode($notifications_data)) . '"></div>';
793 + echo '<div id="kng-fomo-container"></div>';
659 794 }
660 795
661 796 /**
662 797 * Prepare notification data for frontend rendering
@@ -666,19 +801,244 @@
666 801 */
667 802 private function prepare_notification_for_frontend(array $notification): array
668 803 {
669 804 $content = $this->get_notification_content($notification);
805 + $design = $notification['design'];
806 + $customize = $notification['customize'];
807 + $display = $notification['display'];
670 808
809 + // Ensure loop is always set (default true for cycling)
810 + if (!isset($display['loop'])) {
811 + $display['loop'] = true;
812 + }
813 +
671 814 return [
672 815 'id' => $notification['id'],
673 816 'type' => $notification['type'],
674 - 'design' => $notification['design'],
817 + 'design' => $design,
675 818 'content' => $content,
676 - 'customize' => $notification['customize'],
819 + 'customize' => $customize,
820 + 'display' => $display,
821 + // Flat fields for frontend.js convenience
822 + 'title' => $content['title'] ?? '',
823 + 'message' => $content['message'] ?? '',
824 + 'image' => $content['image'] ?? '',
825 + 'image_style' => $content['image_type'] ?? 'product',
826 + 'time_text' => $content['time_text'] ?? '',
827 + 'cta_text' => $content['cta_text'] ?? '',
828 + 'cta_url' => $content['cta_url'] ?? '',
829 + 'click_url' => ($customize['click_action'] ?? 'link') === 'link' ? ($content['cta_url'] ?? '') : '',
830 + 'click_target' => '_self',
831 + 'bg_color' => $design['bg_color'] ?? '#ffffff',
832 + 'text_color' => $design['text_color'] ?? '#1d1d1f',
833 + 'accent_color' => $design['accent_color'] ?? '#0071e3',
834 + 'animation' => $design['animation'] ?? 'slide',
835 + 'display_time' => (($display['duration'] ?? 5) * 1000),
836 + 'sound' => !empty($customize['sound']) ? 'pop' : false,
837 + 'device' => 'all',
838 + 'bar_position' => $design['position'] ?? 'top',
839 + 'page_rules' => $display['page_rules'] ?? null,
840 + // Items for dynamic notifications (WooCommerce, comments, etc.)
841 + 'items' => $content['items'] ?? [],
677 842 ];
678 843 }
679 844
680 845 /**
846 + * Get default content templates and source config for a notification type.
847 + *
848 + * These are used when the user hasn't provided custom templates, and as
849 + * fallback data when real sources return empty.
850 + *
851 + * @param string $type Notification type
852 + * @return array { content_defaults: array, source_config_defaults: array, fallback_items: array }
853 + */
854 + public static function get_type_defaults(string $type): array
855 + {
856 + $defaults = [
857 + 'notification_bar' => [
858 + 'content_defaults' => [
859 + 'title' => '',
860 + 'message' => '',
861 + ],
862 + 'source_config_defaults' => [],
863 + 'fallback_items' => [],
864 + ],
865 + 'woocommerce_sales' => [
866 + 'content_defaults' => [
867 + 'title' => '{{name}}',
868 + 'message' => 'just purchased {{product}}',
869 + ],
870 + 'source_config_defaults' => [
871 + 'order_status' => 'any',
872 + 'time_range' => '7d',
873 + 'limit' => 10,
874 + ],
875 + 'fallback_items' => [
876 + [
877 + 'username' => 'Sarah',
878 + 'product' => 'Premium Bundle',
879 + 'product_url' => '#',
880 + 'product_image' => '',
881 + 'location' => 'New York, US',
882 + 'time' => time() - 180,
883 + 'time_ago' => '3 mins ago',
884 + ],
885 + [
886 + 'username' => 'Michael',
887 + 'product' => 'Starter Pack',
888 + 'product_url' => '#',
889 + 'product_image' => '',
890 + 'location' => 'London, UK',
891 + 'time' => time() - 720,
892 + 'time_ago' => '12 mins ago',
893 + ],
894 + [
895 + 'username' => 'Emma',
896 + 'product' => 'Annual Plan',
897 + 'product_url' => '#',
898 + 'product_image' => '',
899 + 'location' => 'Toronto, CA',
900 + 'time' => time() - 1800,
901 + 'time_ago' => '30 mins ago',
902 + ],
903 + [
904 + 'username' => 'James',
905 + 'product' => 'Pro License',
906 + 'product_url' => '#',
907 + 'product_image' => '',
908 + 'location' => 'Sydney, AU',
909 + 'time' => time() - 3600,
910 + 'time_ago' => '1 hour ago',
911 + ],
912 + [
913 + 'username' => 'Lisa',
914 + 'product' => 'Business Suite',
915 + 'product_url' => '#',
916 + 'product_image' => '',
917 + 'location' => 'Berlin, DE',
918 + 'time' => time() - 7200,
919 + 'time_ago' => '2 hours ago',
920 + ],
921 + ],
922 + ],
923 + 'wordpress_comments' => [
924 + 'content_defaults' => [
925 + 'title' => '{{name}}',
926 + 'message' => 'commented on {{product}}',
927 + ],
928 + 'source_config_defaults' => [
929 + 'post_types' => ['post'],
930 + 'comments_count' => 10,
931 + ],
932 + 'fallback_items' => [
933 + [
934 + 'username' => 'Alex',
935 + 'content' => 'Great article! Very helpful.',
936 + 'post_title' => 'Getting Started Guide',
937 + 'post_url' => '#',
938 + 'avatar' => '',
939 + 'time' => time() - 300,
940 + 'time_ago' => '5 mins ago',
941 + ],
942 + [
943 + 'username' => 'Maria',
944 + 'content' => 'Thanks for sharing this!',
945 + 'post_title' => 'Tips & Tricks',
946 + 'post_url' => '#',
947 + 'avatar' => '',
948 + 'time' => time() - 900,
949 + 'time_ago' => '15 mins ago',
950 + ],
951 + [
952 + 'username' => 'David',
953 + 'content' => 'Exactly what I was looking for.',
954 + 'post_title' => 'Complete Tutorial',
955 + 'post_url' => '#',
956 + 'avatar' => '',
957 + 'time' => time() - 2700,
958 + 'time_ago' => '45 mins ago',
959 + ],
960 + ],
961 + ],
962 + 'wporg_downloads' => [
963 + 'content_defaults' => [
964 + 'title' => '{{name}}',
965 + 'message' => '{{active_installs}} active installs',
966 + ],
967 + 'source_config_defaults' => [
968 + 'wporg_slug' => '',
969 + 'wporg_type' => 'plugin',
970 + 'data_type' => 'downloads',
971 + ],
972 + 'fallback_items' => [],
973 + ],
974 + 'reviews' => [
975 + 'content_defaults' => [
976 + 'title' => '{{name}}',
977 + 'message' => 'left a review on {{product}}',
978 + ],
979 + 'source_config_defaults' => [],
980 + 'fallback_items' => [
981 + [
982 + 'username' => 'John',
983 + 'product' => 'Premium Plugin',
984 + 'post_title' => 'Premium Plugin',
985 + 'product_url' => '#',
986 + 'avatar' => '',
987 + 'time' => time() - 600,
988 + 'time_ago' => '10 mins ago',
989 + ],
990 + ],
991 + ],
992 + 'email_subscription' => [
993 + 'content_defaults' => [
994 + 'title' => '{{name}}',
995 + 'message' => 'just subscribed to the newsletter',
996 + ],
997 + 'source_config_defaults' => [],
998 + 'fallback_items' => [
999 + [
1000 + 'username' => 'Subscriber',
1001 + 'email' => '[email protected]',
1002 + 'avatar' => '',
1003 + 'time' => time() - 120,
1004 + 'time_ago' => '2 mins ago',
1005 + ],
1006 + ],
1007 + ],
1008 + 'donations' => [
1009 + 'content_defaults' => [
1010 + 'title' => '{{name}}',
1011 + 'message' => 'just donated',
1012 + ],
1013 + 'source_config_defaults' => [],
1014 + 'fallback_items' => [
1015 + [
1016 + 'username' => 'Donor',
1017 + 'product' => '$25',
1018 + 'time' => time() - 240,
1019 + 'time_ago' => '4 mins ago',
1020 + ],
1021 + ],
1022 + ],
1023 + 'custom_csv' => [
1024 + 'content_defaults' => [
1025 + 'title' => '{{name}}',
1026 + 'message' => '{{content}}',
1027 + ],
1028 + 'source_config_defaults' => [],
1029 + 'fallback_items' => [],
1030 + ],
1031 + ];
1032 +
1033 + return $defaults[$type] ?? [
1034 + 'content_defaults' => ['title' => '', 'message' => ''],
1035 + 'source_config_defaults' => [],
1036 + 'fallback_items' => [],
1037 + ];
1038 + }
1039 +
1040 + /**
681 1041 * Get notification content with dynamic data
682 1042 *
683 1043 * @param array $notification Notification data
684 1044 * @return array
@@ -686,19 +1046,46 @@
686 1046 private function get_notification_content(array $notification): array
687 1047 {
688 1048 $content = $notification['content'];
689 1049 $source = $notification['source'];
1050 + $type = $notification['type'];
690 1051 $source_config = $notification['source_config'];
691 1052
692 - // For dynamic sources, fetch data
693 - if ($source === 'woocommerce') {
694 - $content['items'] = $this->get_woocommerce_data($source_config);
695 - } elseif ($source === 'comments') {
696 - $content['items'] = $this->get_comments_data($source_config);
697 - } elseif ($source === 'wporg') {
698 - $content['items'] = $this->get_wporg_data($source_config);
1053 + // Overlay type-specific content defaults when user hasn't provided templates.
1054 + $type_defaults = self::get_type_defaults($type);
1055 + $cd = $type_defaults['content_defaults'];
1056 +
1057 + // If title doesn't contain a {{placeholder}}, overlay the type default
1058 + if (!empty($cd['title']) && (empty($content['title']) || strpos($content['title'], '{{') === false)) {
1059 + $content['title'] = $cd['title'];
699 1060 }
1061 + if (!empty($cd['message']) && (empty($content['message']) || strpos($content['message'], '{{') === false)) {
1062 + $content['message'] = $cd['message'];
1063 + }
700 1064
1065 + // For dynamic sources, fetch real data.
1066 + // The wizard sets source = type, so we match both legacy and current values.
1067 + $items = [];
1068 + if ($source === 'woocommerce' || $source === 'woocommerce_sales' || $type === 'woocommerce_sales') {
1069 + $items = $this->get_woocommerce_data($source_config);
1070 + } elseif ($source === 'comments' || $source === 'wordpress_comments' || $type === 'wordpress_comments') {
1071 + $items = $this->get_comments_data($source_config);
1072 + } elseif ($source === 'wporg' || $source === 'wporg_downloads' || $type === 'wporg_downloads') {
1073 + $items = $this->get_wporg_data($source_config);
1074 + }
1075 +
1076 + // Fallback: use demo/sample items when real source returned nothing
1077 + if (empty($items) && !empty($type_defaults['fallback_items'])) {
1078 + $items = $type_defaults['fallback_items'];
1079 + // Mark as demo data so frontend can optionally indicate it
1080 + foreach ($items as &$item) {
1081 + $item['_demo'] = true;
1082 + }
1083 + unset($item);
1084 + }
1085 +
1086 + $content['items'] = $items;
1087 +
701 1088 return $content;
702 1089 }
703 1090
704 1091 /**
@@ -712,14 +1099,35 @@
712 1099 if (!class_exists('WooCommerce')) {
713 1100 return [];
714 1101 }
715 1102
716 - $limit = $config['limit'] ?? 10;
717 - $days = $config['days'] ?? 7;
1103 + $limit = (int)($config['limit'] ?? 10);
718 1104
1105 + $days = isset($config['days']) ? (int)$config['days'] : 0;
1106 + if ($days <= 0 && !empty($config['time_range'])) {
1107 + $time_range = sanitize_text_field((string)$config['time_range']);
1108 + $days_map = [
1109 + '24h' => 1,
1110 + '7d' => 7,
1111 + '30d' => 30,
1112 + ];
1113 + $days = $days_map[$time_range] ?? 7;
1114 + }
1115 + if ($days <= 0) {
1116 + $days = 7;
1117 + }
1118 +
1119 + $order_status = sanitize_text_field((string)($config['order_status'] ?? 'any'));
1120 + $status = ['wc-completed', 'wc-processing'];
1121 + if ($order_status === 'completed') {
1122 + $status = ['wc-completed'];
1123 + } elseif ($order_status === 'processing') {
1124 + $status = ['wc-processing'];
1125 + }
1126 +
719 1127 $args = [
720 1128 'limit' => $limit,
721 - 'status' => ['wc-completed', 'wc-processing'],
1129 + 'status' => $status,
722 1130 'date_created' => '>' . (time() - ($days * DAY_IN_SECONDS)),
723 1131 'orderby' => 'date',
724 1132 'order' => 'DESC',
725 1133 ];
@@ -759,10 +1167,18 @@
759 1167 * @return array
760 1168 */
761 1169 private function get_comments_data(array $config): array
762 1170 {
763 - $limit = $config['limit'] ?? 10;
1171 + $limit = (int)($config['limit'] ?? ($config['comments_count'] ?? 10));
1172 + if ($limit <= 0) {
1173 + $limit = 10;
1174 + }
764 1175 $post_scope = $config['post_scope'] ?? 'all';
1176 + $post_types = $config['post_types'] ?? [];
1177 + if (!is_array($post_types)) {
1178 + $post_types = [$post_types];
1179 + }
1180 + $post_types = array_values(array_filter(array_map('sanitize_text_field', $post_types)));
765 1181
766 1182 $args = [
767 1183 'number' => $limit,
768 1184 'status' => 'approve',
@@ -769,8 +1185,12 @@
769 1185 'orderby' => 'comment_date',
770 1186 'order' => 'DESC',
771 1187 ];
772 1188
1189 + if (!empty($post_types)) {
1190 + $args['post_type'] = $post_types;
1191 + }
1192 +
773 1193 if ($post_scope !== 'all' && is_numeric($post_scope)) {
774 1194 $args['post_id'] = (int)$post_scope;
775 1195 }
776 1196
@@ -799,11 +1219,11 @@
799 1219 * @return array
800 1220 */
801 1221 private function get_wporg_data(array $config): array
802 1222 {
803 - $slug = $config['slug'] ?? '';
804 - $type = $config['product_type'] ?? 'plugin';
805 - $data_type = $config['data_type'] ?? 'downloads';
1223 + $slug = sanitize_text_field((string)($config['slug'] ?? ($config['wporg_slug'] ?? '')));
1224 + $type = sanitize_text_field((string)($config['product_type'] ?? ($config['wporg_type'] ?? 'plugin')));
1225 + $data_type = sanitize_text_field((string)($config['data_type'] ?? 'downloads'));
806 1226
807 1227 if (empty($slug)) {
808 1228 return [];
809 1229 }
@@ -859,10 +1279,15 @@
859 1279 }
860 1280
861 1281 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
862 1282 $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
1283 + // Use separate 'name' field for post_title if provided, otherwise fall back to 'title'
1284 + $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
1285 + if (!empty($name)) {
1286 + $title = $name;
1287 + }
863 1288 $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'disabled';
864 - $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : 'notification-bar';
1289 + $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : 'notification_bar';
865 1290 $source = isset($_POST['source']) ? sanitize_text_field($_POST['source']) : 'manual';
866 1291 $source_config = isset($_POST['source_config']) ? $_POST['source_config'] : '{}';
867 1292 $design = isset($_POST['design']) ? $_POST['design'] : '{}';
868 1293 $content = isset($_POST['content']) ? $_POST['content'] : '{}';
@@ -943,12 +1368,10 @@
943 1368 'display' => json_decode(get_post_meta($id, '_kng_fomo_display', true) ?: '{}', true),
944 1369 'customize' => json_decode(get_post_meta($id, '_kng_fomo_customize', true) ?: '{}', true),
945 1370 ];
946 1371
947 - // Ensure content.title has the post title
948 - if (empty($data['content']['title'])) {
949 - $data['content']['title'] = $post->post_title;
950 - }
1372 + // Provide the notification name separately from content.title
1373 + $data['name'] = $post->post_title;
951 1374
952 1375 wp_send_json_success($data);
953 1376 }
954 1377
@@ -1069,22 +1492,32 @@
1069 1492 if (!current_user_can('manage_options')) {
1070 1493 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1071 1494 }
1072 1495
1073 - $settings = isset($_POST['settings']) ? $_POST['settings'] : [];
1496 + $raw_settings = isset($_POST['settings']) ? $_POST['settings'] : [];
1074 1497
1498 + // If settings were sent as JSON string, decode them
1499 + if (is_string($raw_settings)) {
1500 + $raw_settings = json_decode(stripslashes($raw_settings), true);
1501 + if (!is_array($raw_settings)) {
1502 + $raw_settings = [];
1503 + }
1504 + }
1505 +
1075 1506 // Sanitize
1076 1507 $sanitized = [
1077 - 'enabled' => !empty($settings['enabled']),
1078 - 'tracking_enabled' => !empty($settings['tracking_enabled']),
1079 - 'track_for' => sanitize_text_field($settings['track_for'] ?? 'everyone'),
1080 - 'exclude_bots' => !empty($settings['exclude_bots']),
1081 - 'cache_ttl' => (int)($settings['cache_ttl'] ?? 3600),
1508 + 'enabled' => !empty($raw_settings['enabled']),
1509 + 'tracking_enabled' => !empty($raw_settings['tracking_enabled']),
1510 + 'track_for' => sanitize_text_field($raw_settings['track_for'] ?? 'everyone'),
1511 + 'exclude_bots' => !empty($raw_settings['exclude_bots']),
1512 + 'cache_ttl' => (int)($raw_settings['cache_ttl'] ?? 300),
1513 + 'anonymize_names' => !empty($raw_settings['anonymize_names']),
1514 + 'sound_volume' => (int)($raw_settings['sound_volume'] ?? 50),
1082 1515 'modules' => [],
1083 1516 ];
1084 1517
1085 - if (!empty($settings['modules']) && is_array($settings['modules'])) {
1086 - foreach ($settings['modules'] as $module => $enabled) {
1518 + if (!empty($raw_settings['modules']) && is_array($raw_settings['modules'])) {
1519 + foreach ($raw_settings['modules'] as $module => $enabled) {
1087 1520 $sanitized['modules'][sanitize_key($module)] = !empty($enabled);
1088 1521 }
1089 1522 }
1090 1523
@@ -1099,8 +1532,11 @@
1099 1532 * @return void
1100 1533 */
1101 1534 public function ajax_track_event(): void
1102 1535 {
1536 + // Verify nonce
1537 + check_ajax_referer('kng_fomo_frontend', 'nonce');
1538 +
1103 1539 // Rate limiting
1104 1540 $ip = $_SERVER['REMOTE_ADDR'] ?? '';
1105 1541 $rate_key = 'kng_fomo_rate_' . md5($ip);
1106 1542 $rate = get_transient($rate_key);
@@ -1203,8 +1639,11 @@
1203 1639 global $wpdb;
1204 1640 $table = $wpdb->prefix . self::STATS_TABLE;
1205 1641
1206 1642 $period = isset($_POST['period']) ? sanitize_text_field($_POST['period']) : '7days';
1643 + if (empty($period) && isset($_POST['range'])) {
1644 + $period = sanitize_text_field($_POST['range']);
1645 + }
1207 1646 $notification_id = isset($_POST['notification_id']) ? (int)$_POST['notification_id'] : 0;
1208 1647
1209 1648 // Calculate date range
1210 1649 $end_date = current_time('Y-m-d');
@@ -1209,16 +1648,19 @@
1209 1648 // Calculate date range
1210 1649 $end_date = current_time('Y-m-d');
1211 1650 switch ($period) {
1212 1651 case '30days':
1213 - $start_date = date('Y-m-d', strtotime('-30 days'));
1652 + $start_date = wp_date('Y-m-d', strtotime('-30 days'));
1214 1653 break;
1215 1654 case '90days':
1216 - $start_date = date('Y-m-d', strtotime('-90 days'));
1655 + $start_date = wp_date('Y-m-d', strtotime('-90 days'));
1217 1656 break;
1657 + case 'year':
1658 + $start_date = wp_date('Y-01-01');
1659 + break;
1218 1660 case '7days':
1219 1661 default:
1220 - $start_date = date('Y-m-d', strtotime('-7 days'));
1662 + $start_date = wp_date('Y-m-d', strtotime('-7 days'));
1221 1663 break;
1222 1664 }
1223 1665
1224 1666 // Build query
@@ -1252,9 +1694,9 @@
1252 1694 'clicks' => [],
1253 1695 ];
1254 1696
1255 1697 foreach ($daily as $row) {
1256 - $chart_data['labels'][] = date('M j', strtotime($row->stat_date));
1698 + $chart_data['labels'][] = wp_date('M j', strtotime($row->stat_date));
1257 1699 $chart_data['views'][] = (int)$row->views;
1258 1700 $chart_data['clicks'][] = (int)$row->clicks;
1259 1701 }
1260 1702
@@ -1307,20 +1749,38 @@
1307 1749 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1308 1750 }
1309 1751
1310 1752 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
1311 - $notification = $this->get_notification($id);
1312 1753
1313 - if (!$notification) {
1314 - wp_send_json_error(['message' => __('Notification not found.', 'king-addons')]);
1754 + // Export single notification
1755 + if ($id) {
1756 + $notification = $this->get_notification($id);
1757 +
1758 + if (!$notification) {
1759 + wp_send_json_error(['message' => __('Notification not found.', 'king-addons')]);
1760 + }
1761 +
1762 + // Remove stats and IDs for export
1763 + unset($notification['id'], $notification['views'], $notification['clicks'], $notification['ctr']);
1764 +
1765 + wp_send_json_success([
1766 + 'data' => $notification,
1767 + 'filename' => 'fomo-notification-' . sanitize_title($notification['title']) . '.json',
1768 + ]);
1769 + return;
1315 1770 }
1316 1771
1317 - // Remove stats and IDs for export
1318 - unset($notification['id'], $notification['views'], $notification['clicks'], $notification['ctr']);
1772 + // Export all notifications
1773 + $all = $this->get_all_notifications();
1774 + $export = [];
1775 + foreach ($all as $notification) {
1776 + unset($notification['id'], $notification['views'], $notification['clicks'], $notification['ctr']);
1777 + $export[] = $notification;
1778 + }
1319 1779
1320 1780 wp_send_json_success([
1321 - 'data' => $notification,
1322 - 'filename' => 'fomo-notification-' . sanitize_title($notification['title']) . '.json',
1781 + 'data' => $export,
1782 + 'filename' => 'fomo-notifications-export.json',
1323 1783 ]);
1324 1784 }
1325 1785
1326 1786 /**
@@ -1365,9 +1825,9 @@
1365 1825 }
1366 1826
1367 1827 // Save meta
1368 1828 update_post_meta($post_id, '_kng_fomo_status', 'disabled');
1369 - update_post_meta($post_id, '_kng_fomo_type', $notification['type'] ?? 'notification-bar');
1829 + update_post_meta($post_id, '_kng_fomo_type', $notification['type'] ?? 'notification_bar');
1370 1830 update_post_meta($post_id, '_kng_fomo_source', $notification['source'] ?? 'manual');
1371 1831 update_post_meta($post_id, '_kng_fomo_source_config', wp_json_encode($notification['source_config'] ?? []));
1372 1832 update_post_meta($post_id, '_kng_fomo_design', wp_json_encode($notification['design'] ?? []));
1373 1833 update_post_meta($post_id, '_kng_fomo_content', wp_json_encode($notification['content'] ?? []));
@@ -1436,7 +1896,4 @@
1436 1896
1437 1897 wp_send_json_success(['message' => __('Cache cleared successfully.', 'king-addons')]);
1438 1898 }
1439 1899 }
1440 -
1441 -// Initialize the extension
1442 -Fomo_Notifications::instance();