PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.44
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.44
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
king-addons / includes / extensions / Fomo_Notifications / Fomo_Notifications.php

Fomo_Notifications.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.44, at includes/extensions/Fomo_Notifications/Fomo_Notifications.php

1,443 lines 46.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Fomo Notifications Extension
4 *
5 * Social proof and FOMO notifications system for WordPress.
6 * Creates notification bars, sales popups, review alerts and more.
7 *
8 * @package King_Addons
9 * @since 1.0.0
10 */
11
12 namespace King_Addons;
13
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 /**
19 * Class Fomo_Notifications
20 *
21 * Main class for the Fomo Notifications extension.
22 * Handles CPT registration, admin pages, frontend rendering and analytics.
23 *
24 * @since 1.0.0
25 */
26 final class Fomo_Notifications
27 {
28 /**
29 * Extension version
30 *
31 * @var string
32 */
33 const VERSION = '1.0.0';
34
35 /**
36 * Custom post type name
37 *
38 * @var string
39 */
40 const POST_TYPE = 'kng_fomo_notif';
41
42 /**
43 * Stats table name (without prefix)
44 *
45 * @var string
46 */
47 const STATS_TABLE = 'kng_fomo_stats';
48
49 /**
50 * Free version notification limit
51 *
52 * @var int
53 */
54 const FREE_LIMIT = 3;
55
56 /**
57 * Singleton instance
58 *
59 * @var Fomo_Notifications|null
60 */
61 private static ?Fomo_Notifications $instance = null;
62
63 /**
64 * Extension directory path
65 *
66 * @var string
67 */
68 private string $dir;
69
70 /**
71 * Extension directory URL
72 *
73 * @var string
74 */
75 private string $url;
76
77 /**
78 * Get singleton instance
79 *
80 * @return Fomo_Notifications
81 */
82 public static function instance(): Fomo_Notifications
83 {
84 if (is_null(self::$instance)) {
85 self::$instance = new self();
86 }
87 return self::$instance;
88 }
89
90 /**
91 * Constructor
92 */
93 private function __construct()
94 {
95 $this->dir = plugin_dir_path(__FILE__);
96 $this->url = plugin_dir_url(__FILE__);
97
98 $this->init_hooks();
99 }
100
101 /**
102 * Initialize WordPress hooks
103 *
104 * @return void
105 */
106 private function init_hooks(): void
107 {
108 // Core initialization
109 add_action('init', [$this, 'register_post_type']);
110 add_action('init', [$this, 'register_post_meta']);
111
112 // Admin
113 add_action('admin_menu', [$this, 'register_admin_menu'], 99);
114 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
115
116 // AJAX handlers
117 add_action('wp_ajax_kng_fomo_save_notification', [$this, 'ajax_save_notification']);
118 add_action('wp_ajax_kng_fomo_get_notification', [$this, 'ajax_get_notification']);
119 add_action('wp_ajax_kng_fomo_delete_notification', [$this, 'ajax_delete_notification']);
120 add_action('wp_ajax_kng_fomo_toggle_status', [$this, 'ajax_toggle_status']);
121 add_action('wp_ajax_kng_fomo_duplicate_notification', [$this, 'ajax_duplicate_notification']);
122 add_action('wp_ajax_kng_fomo_save_settings', [$this, 'ajax_save_settings']);
123 add_action('wp_ajax_kng_fomo_get_analytics', [$this, 'ajax_get_analytics']);
124 add_action('wp_ajax_kng_fomo_export_notification', [$this, 'ajax_export_notification']);
125 add_action('wp_ajax_kng_fomo_import_notification', [$this, 'ajax_import_notification']);
126 add_action('wp_ajax_kng_fomo_fetch_wporg_data', [$this, 'ajax_fetch_wporg_data']);
127 add_action('wp_ajax_kng_fomo_purge_cache', [$this, 'ajax_purge_cache']);
128
129 // Frontend
130 add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
131 add_action('wp_footer', [$this, 'render_notifications']);
132
133 // Tracking endpoints (public)
134 add_action('wp_ajax_kng_fomo_track_event', [$this, 'ajax_track_event']);
135 add_action('wp_ajax_nopriv_kng_fomo_track_event', [$this, 'ajax_track_event']);
136
137 // Database setup on init (activation hook won't work here)
138 add_action('admin_init', [$this, 'maybe_create_stats_table']);
139 }
140
141 /**
142 * Register custom post type for notifications
143 *
144 * @return void
145 */
146 public function register_post_type(): void
147 {
148 $labels = [
149 'name' => __('Notifications', 'king-addons'),
150 'singular_name' => __('Notification', 'king-addons'),
151 'add_new' => __('Add New', 'king-addons'),
152 'add_new_item' => __('Add New Notification', 'king-addons'),
153 'edit_item' => __('Edit Notification', 'king-addons'),
154 'new_item' => __('New Notification', 'king-addons'),
155 'view_item' => __('View Notification', 'king-addons'),
156 'search_items' => __('Search Notifications', 'king-addons'),
157 'not_found' => __('No notifications found', 'king-addons'),
158 'not_found_in_trash' => __('No notifications found in Trash', 'king-addons'),
159 ];
160
161 $args = [
162 'labels' => $labels,
163 'public' => false,
164 'publicly_queryable' => false,
165 'show_ui' => false,
166 'show_in_menu' => false,
167 'query_var' => false,
168 'rewrite' => false,
169 'capability_type' => 'post',
170 'has_archive' => false,
171 'hierarchical' => false,
172 'supports' => ['title'],
173 'show_in_rest' => false,
174 ];
175
176 register_post_type(self::POST_TYPE, $args);
177 }
178
179 /**
180 * Register post meta fields
181 *
182 * @return void
183 */
184 public function register_post_meta(): void
185 {
186 $meta_fields = [
187 '_kng_fomo_status',
188 '_kng_fomo_type',
189 '_kng_fomo_source',
190 '_kng_fomo_source_config',
191 '_kng_fomo_design',
192 '_kng_fomo_content',
193 '_kng_fomo_display',
194 '_kng_fomo_customize',
195 '_kng_fomo_views',
196 '_kng_fomo_clicks',
197 ];
198
199 foreach ($meta_fields as $meta_key) {
200 register_post_meta(self::POST_TYPE, $meta_key, [
201 'type' => 'string',
202 'single' => true,
203 'show_in_rest' => false,
204 'sanitize_callback' => 'sanitize_text_field',
205 ]);
206 }
207 }
208
209 /**
210 * Maybe create stats table (runs once)
211 *
212 * @return void
213 */
214 public function maybe_create_stats_table(): void
215 {
216 $version_key = 'kng_fomo_db_version';
217 $current_version = '1.0';
218
219 if (get_option($version_key) !== $current_version) {
220 $this->create_stats_table();
221 update_option($version_key, $current_version);
222 }
223 }
224
225 /**
226 * Create stats table on activation
227 *
228 * @return void
229 */
230 public function create_stats_table(): void
231 {
232 global $wpdb;
233
234 $table_name = $wpdb->prefix . self::STATS_TABLE;
235 $charset_collate = $wpdb->get_charset_collate();
236
237 $sql = "CREATE TABLE IF NOT EXISTS $table_name (
238 id bigint(20) NOT NULL AUTO_INCREMENT,
239 notification_id bigint(20) NOT NULL,
240 stat_date date NOT NULL,
241 views int(11) NOT NULL DEFAULT 0,
242 clicks int(11) NOT NULL DEFAULT 0,
243 PRIMARY KEY (id),
244 UNIQUE KEY notification_date (notification_id, stat_date),
245 KEY notification_id (notification_id),
246 KEY stat_date (stat_date)
247 ) $charset_collate;";
248
249 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
250 dbDelta($sql);
251 }
252
253 /**
254 * Register admin menu pages
255 *
256 * @return void
257 */
258 public function register_admin_menu(): void
259 {
260 // Main page (Dashboard)
261 add_submenu_page(
262 'king-addons',
263 __('Fomo Notifications', 'king-addons'),
264 __('Fomo Notifications', 'king-addons'),
265 'manage_options',
266 'king-addons-fomo',
267 [$this, 'render_admin_page']
268 );
269 }
270
271 /**
272 * Enqueue admin assets
273 *
274 * @param string $hook Current admin page hook
275 * @return void
276 */
277 public function enqueue_admin_assets(string $hook): void
278 {
279 // Check if we're on the Fomo Notifications admin page
280 if (strpos($hook, 'king-addons-fomo') === false &&
281 (!isset($_GET['page']) || $_GET['page'] !== 'king-addons-fomo')) {
282 return;
283 }
284
285 // Chart.js for analytics
286 wp_enqueue_script(
287 'chartjs',
288 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js',
289 [],
290 '4.4.1',
291 true
292 );
293
294 // Admin CSS
295 wp_enqueue_style(
296 'kng-fomo-admin',
297 $this->url . 'assets/admin.css',
298 [],
299 self::VERSION
300 );
301
302 // Admin JS
303 wp_enqueue_script(
304 'kng-fomo-admin',
305 $this->url . 'assets/admin.js',
306 ['jquery', 'wp-util'],
307 self::VERSION,
308 true
309 );
310
311 wp_localize_script('kng-fomo-admin', 'kngFomoAdmin', [
312 'ajaxUrl' => admin_url('admin-ajax.php'),
313 'nonce' => wp_create_nonce('kng_fomo_admin'),
314 'restUrl' => rest_url('kng-fomo/v1/'),
315 'listUrl' => admin_url('admin.php?page=king-addons-fomo&view=list'),
316 'hasPro' => self::hasPro(),
317 'freeLimit' => self::FREE_LIMIT,
318 'upgradeUrl' => 'https://kingaddons.com/pricing/?utm_source=kng-fomo-notifications&utm_medium=plugin',
319 'i18n' => [
320 'confirmDelete' => __('Are you sure you want to delete this notification?', 'king-addons'),
321 'saved' => __('Notification saved successfully!', 'king-addons'),
322 'deleted' => __('Notification deleted.', 'king-addons'),
323 'duplicated' => __('Notification duplicated.', 'king-addons'),
324 'error' => __('An error occurred. Please try again.', 'king-addons'),
325 'limitReached' => __('Free version limit reached. Upgrade to Pro for unlimited notifications.', 'king-addons'),
326 'proFeature' => __('This feature is available in Pro version.', 'king-addons'),
327 'select_type' => __('Please select a notification type.', 'king-addons'),
328 'select_template' => __('Please select a template.', 'king-addons'),
329 'add_content' => __('Please add a title or message.', 'king-addons'),
330 'settings_saved' => __('Settings saved successfully!', 'king-addons'),
331 'exported' => __('Notification exported successfully!', 'king-addons'),
332 ],
333 ]);
334 }
335
336 /**
337 * Enqueue frontend assets
338 *
339 * @return void
340 */
341 public function enqueue_frontend_assets(): void
342 {
343 // Check if there are active notifications for current page
344 if (!$this->has_active_notifications()) {
345 return;
346 }
347
348 wp_enqueue_style(
349 'kng-fomo-frontend',
350 $this->url . 'assets/frontend.css',
351 [],
352 self::VERSION
353 );
354
355 wp_enqueue_script(
356 'kng-fomo-frontend',
357 $this->url . 'assets/frontend.js',
358 ['jquery'],
359 self::VERSION,
360 true
361 );
362
363 wp_localize_script('kng-fomo-frontend', 'kngFomoFrontend', [
364 'ajaxUrl' => admin_url('admin-ajax.php'),
365 'nonce' => wp_create_nonce('kng_fomo_frontend'),
366 ]);
367 }
368
369 /**
370 * Render admin page based on view parameter
371 *
372 * @return void
373 */
374 public function render_admin_page(): void
375 {
376 $view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'dashboard';
377 $template_map = [
378 'dashboard' => 'admin-dashboard.php',
379 'list' => 'admin-list.php',
380 'new' => 'admin-wizard.php',
381 'wizard' => 'admin-wizard.php',
382 'edit' => 'admin-wizard.php',
383 'settings' => 'admin-settings.php',
384 'analytics' => 'admin-analytics.php',
385 ];
386
387 $template = $template_map[$view] ?? 'admin-dashboard.php';
388 $template_path = $this->dir . 'templates/' . $template;
389
390 if (file_exists($template_path)) {
391 // Prepare variables for templates
392 $has_pro = self::hasPro();
393 $notifications = $this->get_all_notifications();
394 $notification_count = count($notifications);
395 $at_limit = !$has_pro && $notification_count >= self::FREE_LIMIT;
396 $settings = $this->get_settings();
397 $notification = null;
398 $is_edit = $view === 'edit';
399
400 if ($is_edit && isset($_GET['id'])) {
401 $notification = $this->get_notification((int)$_GET['id']);
402 }
403
404 include $template_path;
405 }
406 }
407
408 /**
409 * Check if user has Pro version
410 *
411 * @return bool
412 */
413 public static function hasPro(): bool
414 {
415 return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
416 }
417
418 /**
419 * Get all notifications
420 *
421 * @param array $args Query arguments
422 * @return array
423 */
424 public function get_all_notifications(array $args = []): array
425 {
426 $defaults = [
427 'post_type' => self::POST_TYPE,
428 'posts_per_page' => -1,
429 'post_status' => 'any',
430 'orderby' => 'date',
431 'order' => 'DESC',
432 ];
433
434 $args = wp_parse_args($args, $defaults);
435 $posts = get_posts($args);
436
437 $notifications = [];
438 foreach ($posts as $post) {
439 $notifications[] = $this->format_notification($post);
440 }
441
442 return $notifications;
443 }
444
445 /**
446 * Get single notification by ID
447 *
448 * @param int $id Notification ID
449 * @return array|null
450 */
451 public function get_notification(int $id): ?array
452 {
453 $post = get_post($id);
454 if (!$post || $post->post_type !== self::POST_TYPE) {
455 return null;
456 }
457
458 return $this->format_notification($post);
459 }
460
461 /**
462 * Format notification post to array
463 *
464 * @param \WP_Post $post Post object
465 * @return array
466 */
467 private function format_notification(\WP_Post $post): array
468 {
469 $views = (int)get_post_meta($post->ID, '_kng_fomo_views', true);
470 $clicks = (int)get_post_meta($post->ID, '_kng_fomo_clicks', true);
471 $ctr = $views > 0 ? round(($clicks / $views) * 100, 2) : 0;
472
473 return [
474 'id' => $post->ID,
475 'title' => $post->post_title,
476 'status' => get_post_meta($post->ID, '_kng_fomo_status', true) ?: 'disabled',
477 'type' => get_post_meta($post->ID, '_kng_fomo_type', true) ?: 'notification-bar',
478 'source' => get_post_meta($post->ID, '_kng_fomo_source', true) ?: 'manual',
479 'source_config' => json_decode(get_post_meta($post->ID, '_kng_fomo_source_config', true) ?: '{}', true),
480 'design' => json_decode(get_post_meta($post->ID, '_kng_fomo_design', true) ?: '{}', true),
481 'content' => json_decode(get_post_meta($post->ID, '_kng_fomo_content', true) ?: '{}', true),
482 'display' => json_decode(get_post_meta($post->ID, '_kng_fomo_display', true) ?: '{}', true),
483 'customize' => json_decode(get_post_meta($post->ID, '_kng_fomo_customize', true) ?: '{}', true),
484 'views' => $views,
485 'clicks' => $clicks,
486 'ctr' => $ctr,
487 'date' => $post->post_date,
488 'date_formatted' => get_the_date('M j, Y', $post),
489 ];
490 }
491
492 /**
493 * Get extension settings
494 *
495 * @return array
496 */
497 public function get_settings(): array
498 {
499 $defaults = [
500 'enabled' => true,
501 'tracking_enabled' => true,
502 'track_for' => 'everyone',
503 'exclude_bots' => true,
504 'cache_ttl' => 3600,
505 '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,
514 'donations' => false,
515 'discount-alert' => false,
516 'flashing-tab' => false,
517 'custom-csv' => false,
518 'page-analytics' => false,
519 ],
520 ];
521
522 $saved = get_option('kng_fomo_settings', []);
523 return wp_parse_args($saved, $defaults);
524 }
525
526 /**
527 * Save extension settings
528 *
529 * @param array $settings Settings to save
530 * @return bool
531 */
532 public function save_settings(array $settings): bool
533 {
534 return update_option('kng_fomo_settings', $settings);
535 }
536
537 /**
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 * Get active notifications that should display on current page
555 *
556 * @return array
557 */
558 public function get_active_notifications_for_page(): array
559 {
560 $all = $this->get_all_notifications([
561 'meta_key' => '_kng_fomo_status',
562 'meta_value' => 'enabled',
563 ]);
564
565 $active = [];
566 foreach ($all as $notification) {
567 if ($this->should_display_notification($notification)) {
568 $active[] = $notification;
569 }
570 }
571
572 return $active;
573 }
574
575 /**
576 * Check if notification should display on current page
577 *
578 * @param array $notification Notification data
579 * @return bool
580 */
581 private function should_display_notification(array $notification): bool
582 {
583 $display = $notification['display'];
584
585 // Check device visibility
586 $customize = $notification['customize'];
587 $visibility = $customize['visibility'] ?? ['desktop' => true, 'tablet' => true, 'mobile' => true];
588 if (!$this->check_device_visibility($visibility)) {
589 return false;
590 }
591
592 // Check show_on rules
593 $show_on = $display['show_on'] ?? 'everywhere';
594 if ($show_on === 'include' && !empty($display['include_pages'])) {
595 if (!$this->is_current_page_in_list($display['include_pages'])) {
596 return false;
597 }
598 } elseif ($show_on === 'exclude' && !empty($display['exclude_pages'])) {
599 if ($this->is_current_page_in_list($display['exclude_pages'])) {
600 return false;
601 }
602 }
603
604 // Check display_for (audience)
605 $display_for = $display['display_for'] ?? 'everyone';
606 if ($display_for === 'guests' && is_user_logged_in()) {
607 return false;
608 }
609 if ($display_for === 'logged_in' && !is_user_logged_in()) {
610 return false;
611 }
612
613 return true;
614 }
615
616 /**
617 * Check device visibility based on screen width
618 *
619 * @param array $visibility Visibility settings
620 * @return bool
621 */
622 private function check_device_visibility(array $visibility): bool
623 {
624 // Server-side we can't detect device, so return true and let JS handle it
625 return true;
626 }
627
628 /**
629 * Check if current page is in the given list
630 *
631 * @param array $page_list List of page IDs
632 * @return bool
633 */
634 private function is_current_page_in_list(array $page_list): bool
635 {
636 $current_id = get_queried_object_id();
637 return in_array($current_id, $page_list, true);
638 }
639
640 /**
641 * Render notifications on frontend
642 *
643 * @return void
644 */
645 public function render_notifications(): void
646 {
647 $notifications = $this->get_active_notifications_for_page();
648 if (empty($notifications)) {
649 return;
650 }
651
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>';
659 }
660
661 /**
662 * Prepare notification data for frontend rendering
663 *
664 * @param array $notification Notification data
665 * @return array
666 */
667 private function prepare_notification_for_frontend(array $notification): array
668 {
669 $content = $this->get_notification_content($notification);
670
671 return [
672 'id' => $notification['id'],
673 'type' => $notification['type'],
674 'design' => $notification['design'],
675 'content' => $content,
676 'customize' => $notification['customize'],
677 ];
678 }
679
680 /**
681 * Get notification content with dynamic data
682 *
683 * @param array $notification Notification data
684 * @return array
685 */
686 private function get_notification_content(array $notification): array
687 {
688 $content = $notification['content'];
689 $source = $notification['source'];
690 $source_config = $notification['source_config'];
691
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);
699 }
700
701 return $content;
702 }
703
704 /**
705 * Get WooCommerce recent orders data
706 *
707 * @param array $config Source configuration
708 * @return array
709 */
710 private function get_woocommerce_data(array $config): array
711 {
712 if (!class_exists('WooCommerce')) {
713 return [];
714 }
715
716 $limit = $config['limit'] ?? 10;
717 $days = $config['days'] ?? 7;
718
719 $args = [
720 'limit' => $limit,
721 'status' => ['wc-completed', 'wc-processing'],
722 'date_created' => '>' . (time() - ($days * DAY_IN_SECONDS)),
723 'orderby' => 'date',
724 'order' => 'DESC',
725 ];
726
727 $orders = wc_get_orders($args);
728 $items = [];
729
730 foreach ($orders as $order) {
731 $customer_name = $order->get_billing_first_name();
732 if (empty($customer_name)) {
733 $customer_name = __('Someone', 'king-addons');
734 }
735
736 $products = $order->get_items();
737 $product = reset($products);
738 if ($product) {
739 $product_obj = $product->get_product();
740 $items[] = [
741 'username' => $customer_name,
742 'product' => $product->get_name(),
743 'product_url' => $product_obj ? get_permalink($product_obj->get_id()) : '',
744 'product_image' => $product_obj ? wp_get_attachment_url($product_obj->get_image_id()) : '',
745 'location' => $order->get_billing_city() . ', ' . $order->get_billing_country(),
746 'time' => $order->get_date_created()->getTimestamp(),
747 'time_ago' => human_time_diff($order->get_date_created()->getTimestamp(), time()),
748 ];
749 }
750 }
751
752 return $items;
753 }
754
755 /**
756 * Get WordPress comments data
757 *
758 * @param array $config Source configuration
759 * @return array
760 */
761 private function get_comments_data(array $config): array
762 {
763 $limit = $config['limit'] ?? 10;
764 $post_scope = $config['post_scope'] ?? 'all';
765
766 $args = [
767 'number' => $limit,
768 'status' => 'approve',
769 'orderby' => 'comment_date',
770 'order' => 'DESC',
771 ];
772
773 if ($post_scope !== 'all' && is_numeric($post_scope)) {
774 $args['post_id'] = (int)$post_scope;
775 }
776
777 $comments = get_comments($args);
778 $items = [];
779
780 foreach ($comments as $comment) {
781 $items[] = [
782 'username' => $comment->comment_author,
783 'content' => wp_trim_words($comment->comment_content, 15),
784 'post_title' => get_the_title($comment->comment_post_ID),
785 'post_url' => get_permalink($comment->comment_post_ID),
786 'avatar' => get_avatar_url($comment->comment_author_email, ['size' => 64]),
787 'time' => strtotime($comment->comment_date),
788 'time_ago' => human_time_diff(strtotime($comment->comment_date), time()),
789 ];
790 }
791
792 return $items;
793 }
794
795 /**
796 * Get WordPress.org plugin/theme data
797 *
798 * @param array $config Source configuration
799 * @return array
800 */
801 private function get_wporg_data(array $config): array
802 {
803 $slug = $config['slug'] ?? '';
804 $type = $config['product_type'] ?? 'plugin';
805 $data_type = $config['data_type'] ?? 'downloads';
806
807 if (empty($slug)) {
808 return [];
809 }
810
811 $cache_key = 'kng_fomo_wporg_' . md5($slug . $type . $data_type);
812 $cached = get_transient($cache_key);
813 if ($cached !== false) {
814 return $cached;
815 }
816
817 $api_url = $type === 'plugin'
818 ? 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode($slug)
819 : 'https://api.wordpress.org/themes/info/1.2/?action=theme_information&slug=' . urlencode($slug);
820
821 $response = wp_remote_get($api_url, ['timeout' => 10]);
822 if (is_wp_error($response)) {
823 return [];
824 }
825
826 $data = json_decode(wp_remote_retrieve_body($response), true);
827 if (empty($data)) {
828 return [];
829 }
830
831 $items = [];
832 if ($data_type === 'downloads') {
833 $items[] = [
834 'name' => $data['name'] ?? $slug,
835 'downloads' => number_format($data['downloaded'] ?? 0),
836 'active_installs' => number_format($data['active_installs'] ?? 0),
837 'rating' => $data['rating'] ?? 0,
838 'num_ratings' => $data['num_ratings'] ?? 0,
839 ];
840 }
841
842 $cache_ttl = $config['cache_ttl'] ?? 3600;
843 set_transient($cache_key, $items, $cache_ttl);
844
845 return $items;
846 }
847
848 /**
849 * AJAX: Save notification
850 *
851 * @return void
852 */
853 public function ajax_save_notification(): void
854 {
855 check_ajax_referer('kng_fomo_admin', 'nonce');
856
857 if (!current_user_can('manage_options')) {
858 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
859 }
860
861 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
862 $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
863 $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'disabled';
864 $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : 'notification-bar';
865 $source = isset($_POST['source']) ? sanitize_text_field($_POST['source']) : 'manual';
866 $source_config = isset($_POST['source_config']) ? $_POST['source_config'] : '{}';
867 $design = isset($_POST['design']) ? $_POST['design'] : '{}';
868 $content = isset($_POST['content']) ? $_POST['content'] : '{}';
869 $display = isset($_POST['display']) ? $_POST['display'] : '{}';
870 $customize = isset($_POST['customize']) ? $_POST['customize'] : '{}';
871
872 // Check free limit
873 if (!$id && !self::hasPro()) {
874 $count = count($this->get_all_notifications());
875 if ($count >= self::FREE_LIMIT) {
876 wp_send_json_error(['message' => __('Free version limit reached.', 'king-addons')]);
877 }
878 }
879
880 $post_data = [
881 'post_title' => $title ?: __('Untitled Notification', 'king-addons'),
882 'post_type' => self::POST_TYPE,
883 'post_status' => 'publish',
884 ];
885
886 if ($id) {
887 $post_data['ID'] = $id;
888 $post_id = wp_update_post($post_data);
889 } else {
890 $post_id = wp_insert_post($post_data);
891 }
892
893 if (is_wp_error($post_id)) {
894 wp_send_json_error(['message' => $post_id->get_error_message()]);
895 }
896
897 // Save meta
898 update_post_meta($post_id, '_kng_fomo_status', $status);
899 update_post_meta($post_id, '_kng_fomo_type', $type);
900 update_post_meta($post_id, '_kng_fomo_source', $source);
901 update_post_meta($post_id, '_kng_fomo_source_config', $source_config);
902 update_post_meta($post_id, '_kng_fomo_design', $design);
903 update_post_meta($post_id, '_kng_fomo_content', $content);
904 update_post_meta($post_id, '_kng_fomo_display', $display);
905 update_post_meta($post_id, '_kng_fomo_customize', $customize);
906
907 wp_send_json_success([
908 'id' => $post_id,
909 'message' => __('Notification saved successfully!', 'king-addons'),
910 ]);
911 }
912
913 /**
914 * AJAX: Get notification for editing
915 *
916 * @return void
917 */
918 public function ajax_get_notification(): void
919 {
920 check_ajax_referer('kng_fomo_admin', 'nonce');
921
922 if (!current_user_can('manage_options')) {
923 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
924 }
925
926 $id = isset($_POST['notification_id']) ? (int)$_POST['notification_id'] : 0;
927 if (!$id) {
928 wp_send_json_error(['message' => __('Invalid notification ID.', 'king-addons')]);
929 }
930
931 $post = get_post($id);
932 if (!$post || $post->post_type !== self::POST_TYPE) {
933 wp_send_json_error(['message' => __('Notification not found.', 'king-addons')]);
934 }
935
936 $data = [
937 'notification_id' => $id,
938 'type' => get_post_meta($id, '_kng_fomo_type', true) ?: 'notification_bar',
939 'source' => get_post_meta($id, '_kng_fomo_source', true) ?: 'manual',
940 'source_config' => json_decode(get_post_meta($id, '_kng_fomo_source_config', true) ?: '{}', true),
941 'design' => json_decode(get_post_meta($id, '_kng_fomo_design', true) ?: '{}', true),
942 'content' => json_decode(get_post_meta($id, '_kng_fomo_content', true) ?: '{}', true),
943 'display' => json_decode(get_post_meta($id, '_kng_fomo_display', true) ?: '{}', true),
944 'customize' => json_decode(get_post_meta($id, '_kng_fomo_customize', true) ?: '{}', true),
945 ];
946
947 // Ensure content.title has the post title
948 if (empty($data['content']['title'])) {
949 $data['content']['title'] = $post->post_title;
950 }
951
952 wp_send_json_success($data);
953 }
954
955 /**
956 * AJAX: Delete notification
957 *
958 * @return void
959 */
960 public function ajax_delete_notification(): void
961 {
962 check_ajax_referer('kng_fomo_admin', 'nonce');
963
964 if (!current_user_can('manage_options')) {
965 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
966 }
967
968 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
969 if (!$id) {
970 wp_send_json_error(['message' => __('Invalid notification ID.', 'king-addons')]);
971 }
972
973 $result = wp_delete_post($id, true);
974 if (!$result) {
975 wp_send_json_error(['message' => __('Failed to delete notification.', 'king-addons')]);
976 }
977
978 wp_send_json_success(['message' => __('Notification deleted.', 'king-addons')]);
979 }
980
981 /**
982 * AJAX: Toggle notification status
983 *
984 * @return void
985 */
986 public function ajax_toggle_status(): void
987 {
988 check_ajax_referer('kng_fomo_admin', 'nonce');
989
990 if (!current_user_can('manage_options')) {
991 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
992 }
993
994 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
995 $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'disabled';
996
997 if (!$id) {
998 wp_send_json_error(['message' => __('Invalid notification ID.', 'king-addons')]);
999 }
1000
1001 update_post_meta($id, '_kng_fomo_status', $status);
1002
1003 wp_send_json_success(['message' => __('Status updated.', 'king-addons')]);
1004 }
1005
1006 /**
1007 * AJAX: Duplicate notification
1008 *
1009 * @return void
1010 */
1011 public function ajax_duplicate_notification(): void
1012 {
1013 check_ajax_referer('kng_fomo_admin', 'nonce');
1014
1015 if (!current_user_can('manage_options')) {
1016 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1017 }
1018
1019 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
1020 $original = $this->get_notification($id);
1021
1022 if (!$original) {
1023 wp_send_json_error(['message' => __('Notification not found.', 'king-addons')]);
1024 }
1025
1026 // Check free limit
1027 if (!self::hasPro()) {
1028 $count = count($this->get_all_notifications());
1029 if ($count >= self::FREE_LIMIT) {
1030 wp_send_json_error(['message' => __('Free version limit reached.', 'king-addons')]);
1031 }
1032 }
1033
1034 $new_id = wp_insert_post([
1035 'post_title' => $original['title'] . ' ' . __('(Copy)', 'king-addons'),
1036 'post_type' => self::POST_TYPE,
1037 'post_status' => 'publish',
1038 ]);
1039
1040 if (is_wp_error($new_id)) {
1041 wp_send_json_error(['message' => $new_id->get_error_message()]);
1042 }
1043
1044 // Copy meta
1045 update_post_meta($new_id, '_kng_fomo_status', 'disabled');
1046 update_post_meta($new_id, '_kng_fomo_type', $original['type']);
1047 update_post_meta($new_id, '_kng_fomo_source', $original['source']);
1048 update_post_meta($new_id, '_kng_fomo_source_config', wp_json_encode($original['source_config']));
1049 update_post_meta($new_id, '_kng_fomo_design', wp_json_encode($original['design']));
1050 update_post_meta($new_id, '_kng_fomo_content', wp_json_encode($original['content']));
1051 update_post_meta($new_id, '_kng_fomo_display', wp_json_encode($original['display']));
1052 update_post_meta($new_id, '_kng_fomo_customize', wp_json_encode($original['customize']));
1053
1054 wp_send_json_success([
1055 'id' => $new_id,
1056 'message' => __('Notification duplicated.', 'king-addons'),
1057 ]);
1058 }
1059
1060 /**
1061 * AJAX: Save settings
1062 *
1063 * @return void
1064 */
1065 public function ajax_save_settings(): void
1066 {
1067 check_ajax_referer('kng_fomo_admin', 'nonce');
1068
1069 if (!current_user_can('manage_options')) {
1070 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1071 }
1072
1073 $settings = isset($_POST['settings']) ? $_POST['settings'] : [];
1074
1075 // Sanitize
1076 $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),
1082 'modules' => [],
1083 ];
1084
1085 if (!empty($settings['modules']) && is_array($settings['modules'])) {
1086 foreach ($settings['modules'] as $module => $enabled) {
1087 $sanitized['modules'][sanitize_key($module)] = !empty($enabled);
1088 }
1089 }
1090
1091 $this->save_settings($sanitized);
1092
1093 wp_send_json_success(['message' => __('Settings saved.', 'king-addons')]);
1094 }
1095
1096 /**
1097 * AJAX: Track event (view or click)
1098 *
1099 * @return void
1100 */
1101 public function ajax_track_event(): void
1102 {
1103 // Rate limiting
1104 $ip = $_SERVER['REMOTE_ADDR'] ?? '';
1105 $rate_key = 'kng_fomo_rate_' . md5($ip);
1106 $rate = get_transient($rate_key);
1107 if ($rate && $rate > 100) {
1108 wp_send_json_error(['message' => 'Rate limit exceeded']);
1109 }
1110 set_transient($rate_key, ($rate ?: 0) + 1, MINUTE_IN_SECONDS);
1111
1112 $notification_id = isset($_POST['notification_id']) ? (int)$_POST['notification_id'] : 0;
1113 $event_type = isset($_POST['event_type']) ? sanitize_text_field($_POST['event_type']) : '';
1114
1115 if (!$notification_id || !in_array($event_type, ['view', 'click'], true)) {
1116 wp_send_json_error(['message' => 'Invalid request']);
1117 }
1118
1119 $settings = $this->get_settings();
1120 if (empty($settings['tracking_enabled'])) {
1121 wp_send_json_success();
1122 }
1123
1124 // Check audience
1125 $track_for = $settings['track_for'] ?? 'everyone';
1126 if ($track_for === 'guests' && is_user_logged_in()) {
1127 wp_send_json_success();
1128 }
1129 if ($track_for === 'logged_in' && !is_user_logged_in()) {
1130 wp_send_json_success();
1131 }
1132
1133 // Update total counts
1134 $meta_key = $event_type === 'view' ? '_kng_fomo_views' : '_kng_fomo_clicks';
1135 $current = (int)get_post_meta($notification_id, $meta_key, true);
1136 update_post_meta($notification_id, $meta_key, $current + 1);
1137
1138 // Update daily stats
1139 global $wpdb;
1140 $table = $wpdb->prefix . self::STATS_TABLE;
1141 $today = current_time('Y-m-d');
1142 $column = $event_type === 'view' ? 'views' : 'clicks';
1143
1144 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1145 $wpdb->query($wpdb->prepare(
1146 "INSERT INTO $table (notification_id, stat_date, $column) VALUES (%d, %s, 1)
1147 ON DUPLICATE KEY UPDATE $column = $column + 1",
1148 $notification_id,
1149 $today
1150 ));
1151
1152 wp_send_json_success();
1153 }
1154
1155 /**
1156 * Get total stats for all notifications
1157 *
1158 * @return array
1159 */
1160 public function get_total_stats(): array
1161 {
1162 global $wpdb;
1163 $table_name = $wpdb->prefix . self::STATS_TABLE;
1164
1165 // Check if table exists
1166 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
1167 return [
1168 'views' => 0,
1169 'clicks' => 0,
1170 'ctr' => 0,
1171 ];
1172 }
1173
1174 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1175 $stats = $wpdb->get_row("SELECT SUM(views) as views, SUM(clicks) as clicks FROM $table_name", ARRAY_A);
1176
1177 $views = isset($stats['views']) ? (int)$stats['views'] : 0;
1178 $clicks = isset($stats['clicks']) ? (int)$stats['clicks'] : 0;
1179 $ctr = $views > 0 ? round(($clicks / $views) * 100, 2) : 0;
1180
1181 return [
1182 'views' => $views,
1183 'clicks' => $clicks,
1184 'ctr' => $ctr,
1185 'views_change' => 0,
1186 'clicks_change' => 0,
1187 ];
1188 }
1189
1190 /**
1191 * AJAX: Get analytics data
1192 *
1193 * @return void
1194 */
1195 public function ajax_get_analytics(): void
1196 {
1197 check_ajax_referer('kng_fomo_admin', 'nonce');
1198
1199 if (!current_user_can('manage_options')) {
1200 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1201 }
1202
1203 global $wpdb;
1204 $table = $wpdb->prefix . self::STATS_TABLE;
1205
1206 $period = isset($_POST['period']) ? sanitize_text_field($_POST['period']) : '7days';
1207 $notification_id = isset($_POST['notification_id']) ? (int)$_POST['notification_id'] : 0;
1208
1209 // Calculate date range
1210 $end_date = current_time('Y-m-d');
1211 switch ($period) {
1212 case '30days':
1213 $start_date = date('Y-m-d', strtotime('-30 days'));
1214 break;
1215 case '90days':
1216 $start_date = date('Y-m-d', strtotime('-90 days'));
1217 break;
1218 case '7days':
1219 default:
1220 $start_date = date('Y-m-d', strtotime('-7 days'));
1221 break;
1222 }
1223
1224 // Build query
1225 $where = $wpdb->prepare("WHERE stat_date BETWEEN %s AND %s", $start_date, $end_date);
1226 if ($notification_id) {
1227 $where .= $wpdb->prepare(" AND notification_id = %d", $notification_id);
1228 }
1229
1230 // Get totals
1231 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1232 $totals = $wpdb->get_row(
1233 "SELECT SUM(views) as views, SUM(clicks) as clicks FROM $table $where"
1234 );
1235
1236 $total_views = (int)($totals->views ?? 0);
1237 $total_clicks = (int)($totals->clicks ?? 0);
1238 $total_ctr = $total_views > 0 ? round(($total_clicks / $total_views) * 100, 2) : 0;
1239
1240 // Get daily data for chart
1241 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1242 $daily = $wpdb->get_results(
1243 "SELECT stat_date, SUM(views) as views, SUM(clicks) as clicks
1244 FROM $table $where
1245 GROUP BY stat_date
1246 ORDER BY stat_date ASC"
1247 );
1248
1249 $chart_data = [
1250 'labels' => [],
1251 'views' => [],
1252 'clicks' => [],
1253 ];
1254
1255 foreach ($daily as $row) {
1256 $chart_data['labels'][] = date('M j', strtotime($row->stat_date));
1257 $chart_data['views'][] = (int)$row->views;
1258 $chart_data['clicks'][] = (int)$row->clicks;
1259 }
1260
1261 // Get top notifications
1262 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1263 $top_raw = $wpdb->get_results(
1264 "SELECT notification_id, SUM(views) as views, SUM(clicks) as clicks
1265 FROM $table $where
1266 GROUP BY notification_id
1267 ORDER BY clicks DESC
1268 LIMIT 5"
1269 );
1270
1271 $top_notifications = [];
1272 foreach ($top_raw as $row) {
1273 $post = get_post($row->notification_id);
1274 if ($post) {
1275 $ctr = $row->views > 0 ? round(($row->clicks / $row->views) * 100, 2) : 0;
1276 $top_notifications[] = [
1277 'id' => $row->notification_id,
1278 'title' => $post->post_title,
1279 'views' => (int)$row->views,
1280 'clicks' => (int)$row->clicks,
1281 'ctr' => $ctr,
1282 ];
1283 }
1284 }
1285
1286 wp_send_json_success([
1287 'totals' => [
1288 'views' => $total_views,
1289 'clicks' => $total_clicks,
1290 'ctr' => $total_ctr,
1291 ],
1292 'chart' => $chart_data,
1293 'top' => $top_notifications,
1294 ]);
1295 }
1296
1297 /**
1298 * AJAX: Export notification
1299 *
1300 * @return void
1301 */
1302 public function ajax_export_notification(): void
1303 {
1304 check_ajax_referer('kng_fomo_admin', 'nonce');
1305
1306 if (!current_user_can('manage_options')) {
1307 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1308 }
1309
1310 $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
1311 $notification = $this->get_notification($id);
1312
1313 if (!$notification) {
1314 wp_send_json_error(['message' => __('Notification not found.', 'king-addons')]);
1315 }
1316
1317 // Remove stats and IDs for export
1318 unset($notification['id'], $notification['views'], $notification['clicks'], $notification['ctr']);
1319
1320 wp_send_json_success([
1321 'data' => $notification,
1322 'filename' => 'fomo-notification-' . sanitize_title($notification['title']) . '.json',
1323 ]);
1324 }
1325
1326 /**
1327 * AJAX: Import notification
1328 *
1329 * @return void
1330 */
1331 public function ajax_import_notification(): void
1332 {
1333 check_ajax_referer('kng_fomo_admin', 'nonce');
1334
1335 if (!current_user_can('manage_options')) {
1336 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1337 }
1338
1339 $data = isset($_POST['data']) ? $_POST['data'] : '';
1340 if (empty($data)) {
1341 wp_send_json_error(['message' => __('No data to import.', 'king-addons')]);
1342 }
1343
1344 $notification = json_decode(stripslashes($data), true);
1345 if (!$notification || !is_array($notification)) {
1346 wp_send_json_error(['message' => __('Invalid import data.', 'king-addons')]);
1347 }
1348
1349 // Check free limit
1350 if (!self::hasPro()) {
1351 $count = count($this->get_all_notifications());
1352 if ($count >= self::FREE_LIMIT) {
1353 wp_send_json_error(['message' => __('Free version limit reached.', 'king-addons')]);
1354 }
1355 }
1356
1357 $post_id = wp_insert_post([
1358 'post_title' => $notification['title'] ?? __('Imported Notification', 'king-addons'),
1359 'post_type' => self::POST_TYPE,
1360 'post_status' => 'publish',
1361 ]);
1362
1363 if (is_wp_error($post_id)) {
1364 wp_send_json_error(['message' => $post_id->get_error_message()]);
1365 }
1366
1367 // Save meta
1368 update_post_meta($post_id, '_kng_fomo_status', 'disabled');
1369 update_post_meta($post_id, '_kng_fomo_type', $notification['type'] ?? 'notification-bar');
1370 update_post_meta($post_id, '_kng_fomo_source', $notification['source'] ?? 'manual');
1371 update_post_meta($post_id, '_kng_fomo_source_config', wp_json_encode($notification['source_config'] ?? []));
1372 update_post_meta($post_id, '_kng_fomo_design', wp_json_encode($notification['design'] ?? []));
1373 update_post_meta($post_id, '_kng_fomo_content', wp_json_encode($notification['content'] ?? []));
1374 update_post_meta($post_id, '_kng_fomo_display', wp_json_encode($notification['display'] ?? []));
1375 update_post_meta($post_id, '_kng_fomo_customize', wp_json_encode($notification['customize'] ?? []));
1376
1377 wp_send_json_success([
1378 'id' => $post_id,
1379 'message' => __('Notification imported successfully!', 'king-addons'),
1380 ]);
1381 }
1382
1383 /**
1384 * AJAX: Fetch WordPress.org data
1385 *
1386 * @return void
1387 */
1388 public function ajax_fetch_wporg_data(): void
1389 {
1390 check_ajax_referer('kng_fomo_admin', 'nonce');
1391
1392 if (!current_user_can('manage_options')) {
1393 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1394 }
1395
1396 $slug = isset($_POST['slug']) ? sanitize_text_field($_POST['slug']) : '';
1397 $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : 'plugin';
1398
1399 if (empty($slug)) {
1400 wp_send_json_error(['message' => __('Please enter a slug.', 'king-addons')]);
1401 }
1402
1403 $data = $this->get_wporg_data([
1404 'slug' => $slug,
1405 'product_type' => $type,
1406 'data_type' => 'downloads',
1407 ]);
1408
1409 if (empty($data)) {
1410 wp_send_json_error(['message' => __('Could not fetch data. Please check the slug.', 'king-addons')]);
1411 }
1412
1413 wp_send_json_success(['data' => $data]);
1414 }
1415
1416 /**
1417 * AJAX: Purge cache
1418 *
1419 * @return void
1420 */
1421 public function ajax_purge_cache(): void
1422 {
1423 check_ajax_referer('kng_fomo_admin', 'nonce');
1424
1425 if (!current_user_can('manage_options')) {
1426 wp_send_json_error(['message' => __('Permission denied.', 'king-addons')]);
1427 }
1428
1429 global $wpdb;
1430
1431 // Delete all kng_fomo transients
1432 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1433 $wpdb->query(
1434 "DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_kng_fomo_%' OR option_name LIKE '_transient_timeout_kng_fomo_%'"
1435 );
1436
1437 wp_send_json_success(['message' => __('Cache cleared successfully.', 'king-addons')]);
1438 }
1439 }
1440
1441 // Initialize the extension
1442 Fomo_Notifications::instance();
1443