PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.76
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.76
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 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Live_Chat / Live_Chat.php

Live_Chat.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.76, at includes/extensions/Live_Chat/Live_Chat.php

1,547 lines 55.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Live Chat & Support Builder extension.
4 *
5 * Provides real-time chat support widget with admin inbox.
6 *
7 * @package King_Addons
8 */
9
10 namespace King_Addons;
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 /**
17 * Main Live Chat class.
18 *
19 * Handles admin inbox, frontend widget, REST API, and email notifications.
20 */
21 final class Live_Chat
22 {
23 /**
24 * Option name for settings.
25 */
26 private const OPTION_NAME = 'king_addons_live_chat_options';
27
28 /**
29 * Cookie name for visitor identification.
30 */
31 public const VISITOR_COOKIE = 'king_support_vid';
32
33 /**
34 * Conversations table name (without prefix).
35 */
36 public const TABLE_CONVERSATIONS = 'king_support_conversations';
37
38 /**
39 * Messages table name (without prefix).
40 */
41 public const TABLE_MESSAGES = 'king_support_messages';
42
43 /**
44 * REST API namespace.
45 */
46 public const API_NAMESPACE = 'king-addons/v1';
47
48 /**
49 * Default polling interval in milliseconds.
50 */
51 private const DEFAULT_POLL_INTERVAL = 4000;
52
53 /**
54 * Rate limit: minimum seconds between messages.
55 */
56 private const RATE_LIMIT_SECONDS = 2;
57
58 /**
59 * Rate limit: max messages per window.
60 */
61 private const RATE_LIMIT_MAX_MESSAGES = 20;
62
63 /**
64 * Rate limit window in seconds.
65 */
66 private const RATE_LIMIT_WINDOW = 600;
67
68 /**
69 * Singleton instance.
70 *
71 * @var Live_Chat|null
72 */
73 private static ?Live_Chat $instance = null;
74
75 /**
76 * Cached options array.
77 *
78 * @var array<string, mixed>
79 */
80 private array $options = [];
81
82 /**
83 * Gets singleton instance.
84 *
85 * @return Live_Chat
86 */
87 public static function instance(): Live_Chat
88 {
89 if (is_null(self::$instance)) {
90 self::$instance = new self();
91 }
92 return self::$instance;
93 }
94
95 /**
96 * Constructor. Registers hooks.
97 */
98 public function __construct()
99 {
100 $this->options = $this->get_options();
101
102 // Activation hook
103 register_activation_hook(KING_ADDONS_PATH . 'king-addons.php', [$this, 'handle_activation']);
104
105 // Admin hooks
106 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
107 add_action('admin_post_king_addons_live_chat_save', [$this, 'handle_save_settings']);
108
109 // Frontend hooks
110 add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
111 add_action('wp_footer', [$this, 'render_frontend_widget']);
112
113 // REST API
114 add_action('rest_api_init', [$this, 'register_rest_routes']);
115
116 // Admin AJAX for inbox
117 add_action('wp_ajax_king_live_chat_get_conversations', [$this, 'ajax_get_conversations']);
118 add_action('wp_ajax_king_live_chat_get_conversation', [$this, 'ajax_get_conversation']);
119 add_action('wp_ajax_king_live_chat_send_reply', [$this, 'ajax_send_reply']);
120 add_action('wp_ajax_king_live_chat_update_status', [$this, 'ajax_update_status']);
121 add_action('wp_ajax_king_live_chat_delete_conversation', [$this, 'ajax_delete_conversation']);
122 }
123
124 /**
125 * Handles plugin activation.
126 *
127 * Creates database tables and default options.
128 *
129 * @return void
130 */
131 public function handle_activation(): void
132 {
133 if (!get_option(self::OPTION_NAME)) {
134 add_option(self::OPTION_NAME, $this->get_default_options());
135 }
136
137 $this->create_tables();
138 }
139
140 /**
141 * Creates database tables.
142 *
143 * @return void
144 */
145 public function create_tables(): void
146 {
147 global $wpdb;
148
149 $charset_collate = $wpdb->get_charset_collate();
150
151 $conversations_table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
152 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
153
154 $sql_conversations = "CREATE TABLE IF NOT EXISTS $conversations_table (
155 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
156 visitor_id varchar(64) NOT NULL,
157 visitor_name varchar(100) DEFAULT '',
158 visitor_email varchar(100) DEFAULT '',
159 status varchar(20) NOT NULL DEFAULT 'open',
160 last_page_url text,
161 referrer text,
162 user_agent text,
163 ip_address varchar(45),
164 created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
165 updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
166 last_message_at datetime DEFAULT NULL,
167 unread_admin int(11) NOT NULL DEFAULT 0,
168 unread_visitor int(11) NOT NULL DEFAULT 0,
169 PRIMARY KEY (id),
170 KEY visitor_id (visitor_id),
171 KEY status (status),
172 KEY last_message_at (last_message_at)
173 ) $charset_collate;";
174
175 $sql_messages = "CREATE TABLE IF NOT EXISTS $messages_table (
176 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
177 conversation_id bigint(20) unsigned NOT NULL,
178 author_type varchar(20) NOT NULL,
179 author_user_id bigint(20) unsigned DEFAULT NULL,
180 message_text text NOT NULL,
181 created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
182 is_read tinyint(1) NOT NULL DEFAULT 0,
183 PRIMARY KEY (id),
184 KEY conversation_id (conversation_id),
185 KEY author_type (author_type),
186 KEY created_at (created_at)
187 ) $charset_collate;";
188
189 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
190 dbDelta($sql_conversations);
191 dbDelta($sql_messages);
192 }
193
194 /**
195 * Gets default options.
196 *
197 * @return array<string, mixed>
198 */
199 private function get_default_options(): array
200 {
201 return [
202 // General
203 'enabled' => false,
204 'widget_mode' => 'live_chat', // live_chat or contact_form
205 'position' => 'right',
206 'offset_bottom' => 20,
207 'offset_side' => 20,
208 'z_index' => 9999,
209
210 // Appearance
211 'button_size' => 60,
212 'button_color' => '#0066ff',
213 'button_icon' => 'chat',
214 'header_bg' => '#0066ff',
215 'header_text_color' => '#ffffff',
216 'chat_bg' => '#ffffff',
217 'chat_width' => 380,
218 'chat_height' => 520,
219
220 // Messages colors
221 'visitor_msg_bg' => '#e8f4fd',
222 'visitor_msg_text' => '#1d1d1f',
223 'admin_msg_bg' => '#0066ff',
224 'admin_msg_text' => '#ffffff',
225
226 // Texts
227 'header_title' => __('Chat with us', 'king-addons'),
228 'header_subtitle' => __('We typically reply within minutes', 'king-addons'),
229 'placeholder' => __('Type your message...', 'king-addons'),
230 'offline_message' => __('We\'re currently offline. Leave a message and we\'ll get back to you.', 'king-addons'),
231 'welcome_message' => __('Hi! How can we help you today?', 'king-addons'),
232
233 // Contact Form Mode texts
234 'subject_label' => __('Subject', 'king-addons'),
235 'message_label' => __('Your message', 'king-addons'),
236 'submit_button' => __('Send Message', 'king-addons'),
237 'success_message' => __('Thank you! Your message has been sent. We\'ll get back to you soon.', 'king-addons'),
238
239 // Pre-chat form
240 'require_name' => true,
241 'require_email' => true,
242 'name_label' => __('Your name', 'king-addons'),
243 'email_label' => __('Your email', 'king-addons'),
244 'start_chat_button' => __('Start Chat', 'king-addons'),
245
246 // Schedule (Pro)
247 'schedule_enabled' => false,
248 'schedule' => [],
249
250 // Email notifications
251 'admin_email' => get_option('admin_email'),
252 'notify_new_conversation' => true,
253 'notify_new_message' => false,
254 'email_subject_admin' => __('New support message from {visitor_name}', 'king-addons'),
255 'email_subject_visitor' => __('Reply from {site_name} support', 'king-addons'),
256
257 // Polling
258 'poll_interval' => self::DEFAULT_POLL_INTERVAL,
259 ];
260 }
261
262 /**
263 * Gets current options merged with defaults.
264 *
265 * @return array<string, mixed>
266 */
267 public function get_options(): array
268 {
269 $saved = get_option(self::OPTION_NAME, []);
270 return wp_parse_args($saved, $this->get_default_options());
271 }
272
273 /**
274 * Checks if premium features are available.
275 *
276 * @return bool
277 */
278 public function is_premium(): bool
279 {
280 return function_exists('king_addons_freemius')
281 && king_addons_freemius()->can_use_premium_code__premium_only();
282 }
283
284 /**
285 * Renders the admin settings/inbox page.
286 *
287 * @return void
288 */
289 public function render_admin_page(): void
290 {
291 if (!current_user_can('manage_options')) {
292 return;
293 }
294
295 $this->options = $this->get_options();
296 $is_premium = $this->is_premium();
297 $options = $this->options;
298
299 // Ensure tables exist
300 $this->create_tables();
301
302 include __DIR__ . '/templates/admin-page.php';
303 }
304
305 /**
306 * Enqueues admin assets.
307 *
308 * @param string $hook Current admin page hook.
309 * @return void
310 */
311 public function enqueue_admin_assets(string $hook): void
312 {
313 if ($hook !== 'king-addons_page_king-addons-live-chat') {
314 return;
315 }
316
317 wp_enqueue_style('wp-color-picker');
318 wp_enqueue_script('wp-color-picker');
319
320 wp_enqueue_style(
321 'king-addons-v3-styles',
322 KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css',
323 [],
324 KING_ADDONS_VERSION
325 );
326
327 wp_enqueue_style(
328 'king-addons-live-chat-admin',
329 KING_ADDONS_URL . 'includes/extensions/Live_Chat/assets/admin.css',
330 ['king-addons-v3-styles'],
331 KING_ADDONS_VERSION
332 );
333
334 wp_enqueue_script(
335 'king-addons-live-chat-admin',
336 KING_ADDONS_URL . 'includes/extensions/Live_Chat/assets/admin.js',
337 ['jquery', 'wp-color-picker'],
338 KING_ADDONS_VERSION,
339 true
340 );
341
342 wp_localize_script('king-addons-live-chat-admin', 'kingLiveChatAdmin', [
343 'ajaxUrl' => admin_url('admin-ajax.php'),
344 'nonce' => wp_create_nonce('king_live_chat_admin'),
345 'strings' => [
346 'confirmDelete' => __('Are you sure you want to delete this conversation?', 'king-addons'),
347 'sending' => __('Sending...', 'king-addons'),
348 'send' => __('Send Reply', 'king-addons'),
349 'noMessages' => __('No messages yet', 'king-addons'),
350 'error' => __('An error occurred. Please try again.', 'king-addons'),
351 ],
352 ]);
353 }
354
355 /**
356 * Enqueues frontend assets.
357 *
358 * @return void
359 */
360 public function enqueue_frontend_assets(): void
361 {
362 if (is_admin() || !$this->options['enabled']) {
363 return;
364 }
365
366 wp_enqueue_style(
367 'king-addons-live-chat',
368 KING_ADDONS_URL . 'includes/extensions/Live_Chat/assets/frontend.css',
369 [],
370 KING_ADDONS_VERSION
371 );
372
373 wp_enqueue_script(
374 'king-addons-live-chat',
375 KING_ADDONS_URL . 'includes/extensions/Live_Chat/assets/frontend.js',
376 [],
377 KING_ADDONS_VERSION,
378 true
379 );
380
381 wp_localize_script('king-addons-live-chat', 'kingLiveChat', [
382 'restUrl' => rest_url(self::API_NAMESPACE . '/support'),
383 'nonce' => wp_create_nonce('wp_rest'),
384 'visitorId' => $this->get_or_create_visitor_id(),
385 'pollInterval' => intval($this->options['poll_interval']),
386 'isOnline' => $this->is_online(),
387 'widgetMode' => $this->options['widget_mode'] ?? 'live_chat',
388 'options' => [
389 'position' => $this->options['position'],
390 'requireName' => $this->options['require_name'],
391 'requireEmail' => $this->options['require_email'],
392 ],
393 'strings' => [
394 'headerTitle' => $this->options['header_title'],
395 'headerSubtitle' => $this->options['header_subtitle'],
396 'placeholder' => $this->options['placeholder'],
397 'offlineMessage' => $this->options['offline_message'],
398 'welcomeMessage' => $this->options['welcome_message'],
399 'nameLabel' => $this->options['name_label'],
400 'emailLabel' => $this->options['email_label'],
401 'startChat' => $this->options['start_chat_button'],
402 'send' => __('Send', 'king-addons'),
403 'typing' => __('typing...', 'king-addons'),
404 'justNow' => __('Just now', 'king-addons'),
405 'errorNetwork' => __('Network error. Please try again.', 'king-addons'),
406 'errorRateLimit' => __('Please wait a moment before sending another message.', 'king-addons'),
407 // Contact Form Mode strings
408 'subjectLabel' => $this->options['subject_label'] ?? __('Subject', 'king-addons'),
409 'messageLabel' => $this->options['message_label'] ?? __('Your message', 'king-addons'),
410 'submitButton' => $this->options['submit_button'] ?? __('Send Message', 'king-addons'),
411 'successMessage' => $this->options['success_message'] ?? __('Thank you! Your message has been sent.', 'king-addons'),
412 ],
413 ]);
414 }
415
416 /**
417 * Renders the frontend chat widget markup.
418 *
419 * @return void
420 */
421 public function render_frontend_widget(): void
422 {
423 if (is_admin() || !$this->options['enabled']) {
424 return;
425 }
426
427 $options = $this->options;
428 $position = $options['position'];
429 $is_online = $this->is_online();
430
431 // Generate inline styles
432 $button_styles = sprintf(
433 '--ka-chat-btn-size: %dpx; --ka-chat-btn-color: %s;',
434 intval($options['button_size']),
435 esc_attr($options['button_color'])
436 );
437
438 $panel_styles = sprintf(
439 '--ka-chat-width: %dpx; --ka-chat-height: %dpx; --ka-chat-header-bg: %s; --ka-chat-header-text: %s; --ka-chat-bg: %s; --ka-chat-visitor-bg: %s; --ka-chat-visitor-text: %s; --ka-chat-admin-bg: %s; --ka-chat-admin-text: %s;',
440 intval($options['chat_width']),
441 intval($options['chat_height']),
442 esc_attr($options['header_bg']),
443 esc_attr($options['header_text_color']),
444 esc_attr($options['chat_bg']),
445 esc_attr($options['visitor_msg_bg']),
446 esc_attr($options['visitor_msg_text']),
447 esc_attr($options['admin_msg_bg']),
448 esc_attr($options['admin_msg_text'])
449 );
450
451 $position_styles = sprintf(
452 'bottom: %dpx; %s: %dpx; z-index: %d;',
453 intval($options['offset_bottom']),
454 $position === 'left' ? 'left' : 'right',
455 intval($options['offset_side']),
456 intval($options['z_index'])
457 );
458
459 $widget_mode = $options['widget_mode'] ?? 'live_chat';
460 ?>
461 <div id="ka-live-chat"
462 class="ka-live-chat ka-live-chat--<?php echo esc_attr($position); ?> ka-live-chat--mode-<?php echo esc_attr($widget_mode); ?>"
463 style="<?php echo esc_attr($position_styles); ?>"
464 data-online="<?php echo $is_online ? 'true' : 'false'; ?>"
465 data-mode="<?php echo esc_attr($widget_mode); ?>">
466
467 <!-- Floating Button -->
468 <button type="button"
469 class="ka-live-chat__button"
470 style="<?php echo esc_attr($button_styles); ?>"
471 aria-label="<?php esc_attr_e('Open chat', 'king-addons'); ?>">
472 <?php if ($widget_mode === 'contact_form'): ?>
473 <svg class="ka-live-chat__icon ka-live-chat__icon--chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
474 <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
475 <polyline points="22,6 12,13 2,6"/>
476 </svg>
477 <?php else: ?>
478 <svg class="ka-live-chat__icon ka-live-chat__icon--chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
479 <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
480 </svg>
481 <?php endif; ?>
482 <svg class="ka-live-chat__icon ka-live-chat__icon--close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
483 <line x1="18" y1="6" x2="6" y2="18"/>
484 <line x1="6" y1="6" x2="18" y2="18"/>
485 </svg>
486 <span class="ka-live-chat__badge" style="display: none;">0</span>
487 </button>
488
489 <!-- Chat Panel -->
490 <div class="ka-live-chat__panel" style="<?php echo esc_attr($panel_styles); ?>">
491 <!-- Header -->
492 <div class="ka-live-chat__header">
493 <div class="ka-live-chat__header-info">
494 <div class="ka-live-chat__header-title"><?php echo esc_html($options['header_title']); ?></div>
495 <div class="ka-live-chat__header-subtitle">
496 <?php if ($widget_mode === 'live_chat'): ?>
497 <span class="ka-live-chat__status-dot <?php echo $is_online ? 'ka-live-chat__status-dot--online' : ''; ?>"></span>
498 <?php endif; ?>
499 <?php echo esc_html($options['header_subtitle']); ?>
500 </div>
501 </div>
502 <button type="button" class="ka-live-chat__close" aria-label="<?php esc_attr_e('Close chat', 'king-addons'); ?>">
503 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
504 <line x1="18" y1="6" x2="6" y2="18"/>
505 <line x1="6" y1="6" x2="18" y2="18"/>
506 </svg>
507 </button>
508 </div>
509
510 <?php if ($widget_mode === 'contact_form'): ?>
511 <!-- Contact Form Mode -->
512 <div class="ka-live-chat__contact-form">
513 <?php if ($options['require_name']): ?>
514 <div class="ka-live-chat__field">
515 <label for="ka-chat-name"><?php echo esc_html($options['name_label']); ?></label>
516 <input type="text" id="ka-chat-name" name="name" required>
517 </div>
518 <?php endif; ?>
519 <?php if ($options['require_email']): ?>
520 <div class="ka-live-chat__field">
521 <label for="ka-chat-email"><?php echo esc_html($options['email_label']); ?></label>
522 <input type="email" id="ka-chat-email" name="email" required>
523 </div>
524 <?php endif; ?>
525 <div class="ka-live-chat__field">
526 <label for="ka-chat-subject"><?php echo esc_html($options['subject_label'] ?? __('Subject', 'king-addons')); ?></label>
527 <input type="text" id="ka-chat-subject" name="subject">
528 </div>
529 <div class="ka-live-chat__field">
530 <label for="ka-chat-message"><?php echo esc_html($options['message_label'] ?? __('Your message', 'king-addons')); ?></label>
531 <textarea id="ka-chat-message" name="message" rows="4" required></textarea>
532 </div>
533 <!-- Honeypot -->
534 <div class="ka-live-chat__hp" aria-hidden="true">
535 <input type="text" name="website" tabindex="-1" autocomplete="off">
536 </div>
537 <button type="button" class="ka-live-chat__submit">
538 <?php echo esc_html($options['submit_button'] ?? __('Send Message', 'king-addons')); ?>
539 </button>
540 </div>
541
542 <!-- Success Message -->
543 <div class="ka-live-chat__success" style="display: none;">
544 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
545 <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
546 <polyline points="22 4 12 14.01 9 11.01"/>
547 </svg>
548 <p><?php echo esc_html($options['success_message'] ?? __('Thank you! Your message has been sent.', 'king-addons')); ?></p>
549 <button type="button" class="ka-live-chat__new-message"><?php esc_html_e('Send another message', 'king-addons'); ?></button>
550 </div>
551
552 <?php else: ?>
553 <!-- Live Chat Mode -->
554 <!-- Pre-chat Form -->
555 <div class="ka-live-chat__prechat">
556 <?php if ($options['require_name']): ?>
557 <div class="ka-live-chat__field">
558 <label for="ka-chat-name"><?php echo esc_html($options['name_label']); ?></label>
559 <input type="text" id="ka-chat-name" name="name" required>
560 </div>
561 <?php endif; ?>
562 <?php if ($options['require_email']): ?>
563 <div class="ka-live-chat__field">
564 <label for="ka-chat-email"><?php echo esc_html($options['email_label']); ?></label>
565 <input type="email" id="ka-chat-email" name="email" required>
566 </div>
567 <?php endif; ?>
568 <!-- Honeypot -->
569 <div class="ka-live-chat__hp" aria-hidden="true">
570 <input type="text" name="website" tabindex="-1" autocomplete="off">
571 </div>
572 <button type="button" class="ka-live-chat__start">
573 <?php echo esc_html($options['start_chat_button']); ?>
574 </button>
575 </div>
576
577 <!-- Messages Area -->
578 <div class="ka-live-chat__messages">
579 <div class="ka-live-chat__messages-list"></div>
580 </div>
581
582 <!-- Input Area -->
583 <div class="ka-live-chat__input">
584 <textarea placeholder="<?php echo esc_attr($options['placeholder']); ?>" rows="1"></textarea>
585 <button type="button" class="ka-live-chat__send" aria-label="<?php esc_attr_e('Send message', 'king-addons'); ?>">
586 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
587 <line x1="22" y1="2" x2="11" y2="13"/>
588 <polygon points="22 2 15 22 11 13 2 9 22 2"/>
589 </svg>
590 </button>
591 </div>
592 <?php endif; ?>
593
594 <!-- Offline Message -->
595 <?php if (!$is_online): ?>
596 <div class="ka-live-chat__offline">
597 <?php echo esc_html($options['offline_message']); ?>
598 </div>
599 <?php endif; ?>
600 </div>
601 </div>
602 <?php
603 }
604
605 /**
606 * Checks if support is currently online.
607 *
608 * @return bool
609 */
610 private function is_online(): bool
611 {
612 if (!$this->options['schedule_enabled'] || !$this->is_premium()) {
613 return true;
614 }
615
616 // Pro: check schedule
617 // TODO: implement schedule check in Pro version
618 return true;
619 }
620
621 /**
622 * Gets or creates visitor ID from cookie.
623 *
624 * @return string
625 */
626 private function get_or_create_visitor_id(): string
627 {
628 if (isset($_COOKIE[self::VISITOR_COOKIE])) {
629 return sanitize_text_field($_COOKIE[self::VISITOR_COOKIE]);
630 }
631
632 $visitor_id = wp_generate_uuid4();
633
634 // Cookie will be set via JavaScript for proper handling
635 return $visitor_id;
636 }
637
638 /**
639 * Registers REST API routes.
640 *
641 * @return void
642 */
643 public function register_rest_routes(): void
644 {
645 // Initialize or restore conversation
646 register_rest_route(self::API_NAMESPACE, '/support/conversation/init', [
647 'methods' => 'POST',
648 'callback' => [$this, 'rest_init_conversation'],
649 'permission_callback' => [$this, 'can_access_public_rest'],
650 ]);
651
652 // Send message
653 register_rest_route(self::API_NAMESPACE, '/support/message/send', [
654 'methods' => 'POST',
655 'callback' => [$this, 'rest_send_message'],
656 'permission_callback' => [$this, 'can_access_public_rest'],
657 ]);
658
659 // Poll for new messages
660 register_rest_route(self::API_NAMESPACE, '/support/messages/poll', [
661 'methods' => 'GET',
662 'callback' => [$this, 'rest_poll_messages'],
663 'permission_callback' => [$this, 'can_access_public_rest'],
664 ]);
665
666 // Mark messages as read
667 register_rest_route(self::API_NAMESPACE, '/support/messages/read', [
668 'methods' => 'POST',
669 'callback' => [$this, 'rest_mark_read'],
670 'permission_callback' => [$this, 'can_access_public_rest'],
671 ]);
672
673 // Contact Form submission
674 register_rest_route(self::API_NAMESPACE, '/support/contact', [
675 'methods' => 'POST',
676 'callback' => [$this, 'rest_submit_contact_form'],
677 'permission_callback' => [$this, 'can_access_public_rest'],
678 ]);
679 }
680
681 public function can_access_public_rest(\WP_REST_Request $request): bool
682 {
683 $nonce = $request->get_header('X-WP-Nonce');
684
685 return is_string($nonce) && wp_verify_nonce($nonce, 'wp_rest');
686 }
687
688 /**
689 * REST: Submit contact form (Contact Form mode).
690 *
691 * @param \WP_REST_Request $request Request object.
692 * @return \WP_REST_Response
693 */
694 public function rest_submit_contact_form(\WP_REST_Request $request): \WP_REST_Response
695 {
696 global $wpdb;
697
698 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
699 $name = sanitize_text_field($request->get_param('name') ?? '');
700 $email = sanitize_email($request->get_param('email') ?? '');
701 $subject = sanitize_text_field($request->get_param('subject') ?? '');
702 $message = sanitize_textarea_field($request->get_param('message') ?? '');
703 $page_url = esc_url_raw($request->get_param('page_url') ?? '');
704 $referrer = esc_url_raw($request->get_param('referrer') ?? '');
705
706 if (empty($visitor_id) || empty($message)) {
707 return new \WP_REST_Response(['success' => false, 'message' => 'Missing required fields'], 400);
708 }
709
710 // Rate limiting
711 if (!$this->check_rate_limit($visitor_id)) {
712 return new \WP_REST_Response([
713 'success' => false,
714 'message' => __('Too many messages. Please wait a moment.', 'king-addons'),
715 ], 429);
716 }
717
718 $conversations_table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
719 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
720
721 // Create conversation
722 $wpdb->insert($conversations_table, [
723 'visitor_id' => $visitor_id,
724 'visitor_name' => $name,
725 'visitor_email' => $email,
726 'status' => 'open',
727 'last_page_url' => $page_url,
728 'referrer' => $referrer,
729 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
730 'ip_address' => $this->get_client_ip(),
731 'created_at' => current_time('mysql'),
732 'last_message_at' => current_time('mysql'),
733 'unread_admin' => 1,
734 ]);
735
736 $conversation_id = $wpdb->insert_id;
737
738 if (!$conversation_id) {
739 return new \WP_REST_Response(['success' => false, 'message' => 'Failed to create conversation'], 500);
740 }
741
742 // Combine subject and message
743 $full_message = $message;
744 if (!empty($subject)) {
745 $full_message = "[{$subject}]\n\n{$message}";
746 }
747
748 // Insert message
749 $wpdb->insert($messages_table, [
750 'conversation_id' => $conversation_id,
751 'author_type' => 'visitor',
752 'message_text' => $full_message,
753 'created_at' => current_time('mysql'),
754 ]);
755
756 // Close conversation immediately for contact form mode
757 $wpdb->update(
758 $conversations_table,
759 ['status' => 'closed'],
760 ['id' => $conversation_id]
761 );
762
763 // Send email notification
764 $this->send_admin_notification($conversation_id, $name, $email, $full_message);
765
766 return new \WP_REST_Response([
767 'success' => true,
768 'message' => __('Message sent successfully', 'king-addons'),
769 ]);
770 }
771
772 /**
773 * REST: Initialize or restore conversation.
774 *
775 * @param \WP_REST_Request $request Request object.
776 * @return \WP_REST_Response
777 */
778 public function rest_init_conversation(\WP_REST_Request $request): \WP_REST_Response
779 {
780 global $wpdb;
781
782 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
783 $name = sanitize_text_field($request->get_param('name') ?? '');
784 $email = sanitize_email($request->get_param('email') ?? '');
785 $page_url = esc_url_raw($request->get_param('page_url') ?? '');
786 $referrer = esc_url_raw($request->get_param('referrer') ?? '');
787
788 if (empty($visitor_id)) {
789 return new \WP_REST_Response(['error' => 'Invalid visitor ID'], 400);
790 }
791
792 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
793 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
794
795 // Check for existing open conversation
796 $conversation = $wpdb->get_row($wpdb->prepare(
797 "SELECT * FROM $table WHERE visitor_id = %s AND status = 'open' ORDER BY created_at DESC LIMIT 1",
798 $visitor_id
799 ));
800
801 if ($conversation) {
802 // Update visitor info if provided
803 if (!empty($name) || !empty($email)) {
804 $wpdb->update(
805 $table,
806 array_filter([
807 'visitor_name' => $name ?: null,
808 'visitor_email' => $email ?: null,
809 ]),
810 ['id' => $conversation->id]
811 );
812 }
813
814 // Get messages
815 $messages = $wpdb->get_results($wpdb->prepare(
816 "SELECT * FROM $messages_table WHERE conversation_id = %d ORDER BY created_at ASC",
817 $conversation->id
818 ));
819
820 return new \WP_REST_Response([
821 'conversation_id' => $conversation->id,
822 'messages' => $this->format_messages($messages),
823 'unread' => intval($conversation->unread_visitor),
824 ]);
825 }
826
827 // No existing conversation - will be created on first message
828 return new \WP_REST_Response([
829 'conversation_id' => null,
830 'messages' => [],
831 'unread' => 0,
832 ]);
833 }
834
835 /**
836 * REST: Send message from visitor.
837 *
838 * @param \WP_REST_Request $request Request object.
839 * @return \WP_REST_Response
840 */
841 public function rest_send_message(\WP_REST_Request $request): \WP_REST_Response
842 {
843 global $wpdb;
844
845 // Honeypot check
846 $honeypot = $request->get_param('website');
847 if (!empty($honeypot)) {
848 return new \WP_REST_Response(['error' => 'Spam detected'], 403);
849 }
850
851 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
852 $conversation_id = intval($request->get_param('conversation_id') ?? 0);
853 $message = sanitize_textarea_field($request->get_param('message') ?? '');
854 $name = sanitize_text_field($request->get_param('name') ?? '');
855 $email = sanitize_email($request->get_param('email') ?? '');
856 $page_url = esc_url_raw($request->get_param('page_url') ?? '');
857 $referrer = esc_url_raw($request->get_param('referrer') ?? '');
858
859 if (empty($visitor_id) || empty($message)) {
860 return new \WP_REST_Response(['error' => 'Missing required fields'], 400);
861 }
862
863 // Rate limiting
864 if (!$this->check_rate_limit($visitor_id)) {
865 return new \WP_REST_Response(['error' => 'rate_limit'], 429);
866 }
867
868 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
869 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
870
871 // Create conversation if needed
872 if (!$conversation_id) {
873 $wpdb->insert($table, [
874 'visitor_id' => $visitor_id,
875 'visitor_name' => $name,
876 'visitor_email' => $email,
877 'status' => 'open',
878 'last_page_url' => $page_url,
879 'referrer' => $referrer,
880 'user_agent' => sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''),
881 'ip_address' => $this->get_client_ip(),
882 'created_at' => current_time('mysql'),
883 'last_message_at' => current_time('mysql'),
884 'unread_admin' => 1,
885 ]);
886 $conversation_id = $wpdb->insert_id;
887 $is_new = true;
888 } else {
889 // Verify conversation belongs to visitor
890 $conv = $wpdb->get_row($wpdb->prepare(
891 "SELECT id FROM $table WHERE id = %d AND visitor_id = %s",
892 $conversation_id,
893 $visitor_id
894 ));
895
896 if (!$conv) {
897 return new \WP_REST_Response(['error' => 'Invalid conversation'], 403);
898 }
899
900 // Update conversation
901 $wpdb->update($table, [
902 'last_message_at' => current_time('mysql'),
903 'unread_admin' => $wpdb->get_var($wpdb->prepare(
904 "SELECT unread_admin FROM $table WHERE id = %d",
905 $conversation_id
906 )) + 1,
907 ], ['id' => $conversation_id]);
908 $is_new = false;
909 }
910
911 // Insert message
912 $wpdb->insert($messages_table, [
913 'conversation_id' => $conversation_id,
914 'author_type' => 'visitor',
915 'message_text' => $message,
916 'created_at' => current_time('mysql'),
917 ]);
918 $message_id = $wpdb->insert_id;
919
920 // Send email notification to admin
921 if ($is_new && $this->options['notify_new_conversation']) {
922 $this->send_admin_notification($conversation_id, $message, $name, $email);
923 } elseif (!$is_new && $this->options['notify_new_message']) {
924 $this->send_admin_notification($conversation_id, $message, $name, $email, false);
925 }
926
927 // Update rate limit
928 $this->update_rate_limit($visitor_id);
929
930 return new \WP_REST_Response([
931 'success' => true,
932 'conversation_id' => $conversation_id,
933 'message_id' => $message_id,
934 'created_at' => current_time('mysql'),
935 ]);
936 }
937
938 /**
939 * REST: Poll for new messages.
940 *
941 * @param \WP_REST_Request $request Request object.
942 * @return \WP_REST_Response
943 */
944 public function rest_poll_messages(\WP_REST_Request $request): \WP_REST_Response
945 {
946 global $wpdb;
947
948 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
949 $conversation_id = intval($request->get_param('conversation_id') ?? 0);
950 $after_id = intval($request->get_param('after_id') ?? 0);
951
952 if (empty($visitor_id) || !$conversation_id) {
953 return new \WP_REST_Response(['messages' => [], 'unread' => 0]);
954 }
955
956 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
957 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
958
959 // Verify conversation
960 $conv = $wpdb->get_row($wpdb->prepare(
961 "SELECT id, unread_visitor FROM $table WHERE id = %d AND visitor_id = %s",
962 $conversation_id,
963 $visitor_id
964 ));
965
966 if (!$conv) {
967 return new \WP_REST_Response(['messages' => [], 'unread' => 0]);
968 }
969
970 // Get new messages
971 $messages = $wpdb->get_results($wpdb->prepare(
972 "SELECT * FROM $messages_table WHERE conversation_id = %d AND id > %d ORDER BY created_at ASC",
973 $conversation_id,
974 $after_id
975 ));
976
977 return new \WP_REST_Response([
978 'messages' => $this->format_messages($messages),
979 'unread' => intval($conv->unread_visitor),
980 ]);
981 }
982
983 /**
984 * REST: Mark messages as read.
985 *
986 * @param \WP_REST_Request $request Request object.
987 * @return \WP_REST_Response
988 */
989 public function rest_mark_read(\WP_REST_Request $request): \WP_REST_Response
990 {
991 global $wpdb;
992
993 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
994 $conversation_id = intval($request->get_param('conversation_id') ?? 0);
995
996 if (empty($visitor_id) || !$conversation_id) {
997 return new \WP_REST_Response(['success' => false]);
998 }
999
1000 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1001 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1002
1003 $conv = $wpdb->get_row($wpdb->prepare(
1004 "SELECT id FROM $table WHERE id = %d AND visitor_id = %s",
1005 $conversation_id,
1006 $visitor_id
1007 ));
1008
1009 if (!$conv) {
1010 return new \WP_REST_Response(['success' => false], 403);
1011 }
1012
1013 $updated = $wpdb->update(
1014 $table,
1015 ['unread_visitor' => 0],
1016 ['id' => $conversation_id]
1017 );
1018
1019 // Mark admin messages as read
1020 $wpdb->query($wpdb->prepare(
1021 "UPDATE $messages_table SET is_read = 1 WHERE conversation_id = %d AND author_type = 'admin'",
1022 $conversation_id
1023 ));
1024
1025 return new \WP_REST_Response(['success' => $updated !== false]);
1026 }
1027
1028 /**
1029 * Formats messages for JSON response.
1030 *
1031 * @param array $messages Database rows.
1032 * @return array
1033 */
1034 private function format_messages(array $messages): array
1035 {
1036 $formatted = [];
1037 foreach ($messages as $msg) {
1038 $formatted[] = [
1039 'id' => intval($msg->id),
1040 'type' => $msg->author_type,
1041 'text' => $msg->message_text,
1042 'time' => $msg->created_at,
1043 'is_read' => (bool) $msg->is_read,
1044 ];
1045 }
1046 return $formatted;
1047 }
1048
1049 /**
1050 * Checks rate limit for visitor.
1051 *
1052 * @param string $visitor_id Visitor ID.
1053 * @return bool
1054 */
1055 private function check_rate_limit(string $visitor_id): bool
1056 {
1057 $transient_key = 'ka_chat_rl_' . md5($visitor_id);
1058 $data = get_transient($transient_key);
1059
1060 if (!$data) {
1061 return true;
1062 }
1063
1064 // Check minimum time between messages
1065 if (time() - $data['last'] < self::RATE_LIMIT_SECONDS) {
1066 return false;
1067 }
1068
1069 // Check max messages in window
1070 if ($data['count'] >= self::RATE_LIMIT_MAX_MESSAGES) {
1071 return false;
1072 }
1073
1074 return true;
1075 }
1076
1077 /**
1078 * Updates rate limit counter.
1079 *
1080 * @param string $visitor_id Visitor ID.
1081 * @return void
1082 */
1083 private function update_rate_limit(string $visitor_id): void
1084 {
1085 $transient_key = 'ka_chat_rl_' . md5($visitor_id);
1086 $data = get_transient($transient_key);
1087
1088 if (!$data) {
1089 $data = ['count' => 0, 'last' => 0];
1090 }
1091
1092 $data['count']++;
1093 $data['last'] = time();
1094
1095 set_transient($transient_key, $data, self::RATE_LIMIT_WINDOW);
1096 }
1097
1098 /**
1099 * Gets client IP address.
1100 *
1101 * @return string
1102 */
1103 private function get_client_ip(): string
1104 {
1105 $ip = '';
1106
1107 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
1108 $ip = $_SERVER['HTTP_CLIENT_IP'];
1109 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1110 $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
1111 } elseif (!empty($_SERVER['REMOTE_ADDR'])) {
1112 $ip = $_SERVER['REMOTE_ADDR'];
1113 }
1114
1115 return sanitize_text_field($ip);
1116 }
1117
1118 /**
1119 * Sends email notification to admin.
1120 *
1121 * @param int $conversation_id Conversation ID.
1122 * @param string $message Message text.
1123 * @param string $name Visitor name.
1124 * @param string $email Visitor email.
1125 * @param bool $is_new Whether this is a new conversation.
1126 * @return void
1127 */
1128 private function send_admin_notification(int $conversation_id, string $message, string $name, string $email, bool $is_new = true): void
1129 {
1130 $admin_email = $this->options['admin_email'];
1131 if (empty($admin_email)) {
1132 return;
1133 }
1134
1135 $subject = str_replace(
1136 ['{visitor_name}', '{site_name}'],
1137 [$name ?: __('Visitor', 'king-addons'), get_bloginfo('name')],
1138 $this->options['email_subject_admin']
1139 );
1140
1141 $inbox_url = admin_url('admin.php?page=king-addons-live-chat&conversation=' . $conversation_id);
1142
1143 $body = sprintf(
1144 "%s\n\n%s: %s\n%s: %s\n\n%s:\n%s\n\n%s:\n%s",
1145 $is_new ? __('New support conversation started', 'king-addons') : __('New message in support conversation', 'king-addons'),
1146 __('Name', 'king-addons'),
1147 $name ?: __('Not provided', 'king-addons'),
1148 __('Email', 'king-addons'),
1149 $email ?: __('Not provided', 'king-addons'),
1150 __('Message', 'king-addons'),
1151 $message,
1152 __('View conversation', 'king-addons'),
1153 $inbox_url
1154 );
1155
1156 wp_mail($admin_email, $subject, $body);
1157 }
1158
1159 /**
1160 * Sends email to visitor with admin reply.
1161 *
1162 * @param string $email Visitor email.
1163 * @param string $name Visitor name.
1164 * @param string $message Reply text.
1165 * @return bool
1166 */
1167 public function send_visitor_notification(string $email, string $name, string $message): bool
1168 {
1169 if (empty($email) || !is_email($email)) {
1170 return false;
1171 }
1172
1173 $subject = str_replace(
1174 ['{visitor_name}', '{site_name}'],
1175 [$name ?: __('there', 'king-addons'), get_bloginfo('name')],
1176 $this->options['email_subject_visitor']
1177 );
1178
1179 $body = sprintf(
1180 "%s %s,\n\n%s\n\n--\n%s",
1181 __('Hi', 'king-addons'),
1182 $name ?: '',
1183 $message,
1184 get_bloginfo('name')
1185 );
1186
1187 return wp_mail($email, $subject, $body);
1188 }
1189
1190 /**
1191 * AJAX: Get conversations list.
1192 *
1193 * @return void
1194 */
1195 public function ajax_get_conversations(): void
1196 {
1197 check_ajax_referer('king_live_chat_admin', 'nonce');
1198
1199 if (!current_user_can('manage_options')) {
1200 wp_send_json_error('Unauthorized');
1201 }
1202
1203 global $wpdb;
1204 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1205
1206 $status = sanitize_text_field($_POST['status'] ?? 'all');
1207 $search = sanitize_text_field($_POST['search'] ?? '');
1208 $page = max(1, intval($_POST['page'] ?? 1));
1209 $per_page = 20;
1210 $offset = ($page - 1) * $per_page;
1211
1212 $where = "1=1";
1213 $params = [];
1214
1215 if ($status !== 'all') {
1216 $where .= " AND status = %s";
1217 $params[] = $status;
1218 }
1219
1220 if (!empty($search)) {
1221 $where .= " AND (visitor_name LIKE %s OR visitor_email LIKE %s)";
1222 $search_param = '%' . $wpdb->esc_like($search) . '%';
1223 $params[] = $search_param;
1224 $params[] = $search_param;
1225 }
1226
1227 $total = $wpdb->get_var(
1228 empty($params)
1229 ? "SELECT COUNT(*) FROM $table WHERE $where"
1230 : $wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where", ...$params)
1231 );
1232
1233 $query = "SELECT * FROM $table WHERE $where ORDER BY last_message_at DESC LIMIT %d OFFSET %d";
1234 $params[] = $per_page;
1235 $params[] = $offset;
1236
1237 $conversations = $wpdb->get_results($wpdb->prepare($query, ...$params));
1238
1239 $formatted = [];
1240 foreach ($conversations as $conv) {
1241 $formatted[] = [
1242 'id' => intval($conv->id),
1243 'name' => $conv->visitor_name ?: __('Anonymous', 'king-addons'),
1244 'email' => $conv->visitor_email,
1245 'status' => $conv->status,
1246 'unread' => intval($conv->unread_admin),
1247 'last_message' => $conv->last_message_at,
1248 'created' => $conv->created_at,
1249 ];
1250 }
1251
1252 wp_send_json_success([
1253 'conversations' => $formatted,
1254 'total' => intval($total),
1255 'pages' => ceil($total / $per_page),
1256 ]);
1257 }
1258
1259 /**
1260 * AJAX: Get single conversation with messages.
1261 *
1262 * @return void
1263 */
1264 public function ajax_get_conversation(): void
1265 {
1266 check_ajax_referer('king_live_chat_admin', 'nonce');
1267
1268 if (!current_user_can('manage_options')) {
1269 wp_send_json_error('Unauthorized');
1270 }
1271
1272 global $wpdb;
1273 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1274
1275 if (!$conversation_id) {
1276 wp_send_json_error('Invalid conversation');
1277 }
1278
1279 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1280 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1281
1282 $conversation = $wpdb->get_row($wpdb->prepare(
1283 "SELECT * FROM $table WHERE id = %d",
1284 $conversation_id
1285 ));
1286
1287 if (!$conversation) {
1288 wp_send_json_error('Conversation not found');
1289 }
1290
1291 // Mark as read
1292 $wpdb->update($table, ['unread_admin' => 0], ['id' => $conversation_id]);
1293 $wpdb->query($wpdb->prepare(
1294 "UPDATE $messages_table SET is_read = 1 WHERE conversation_id = %d AND author_type = 'visitor'",
1295 $conversation_id
1296 ));
1297
1298 $messages = $wpdb->get_results($wpdb->prepare(
1299 "SELECT m.*, u.display_name as admin_name
1300 FROM $messages_table m
1301 LEFT JOIN {$wpdb->users} u ON m.author_user_id = u.ID
1302 WHERE m.conversation_id = %d
1303 ORDER BY m.created_at ASC",
1304 $conversation_id
1305 ));
1306
1307 $formatted_messages = [];
1308 foreach ($messages as $msg) {
1309 $formatted_messages[] = [
1310 'id' => intval($msg->id),
1311 'type' => $msg->author_type,
1312 'text' => $msg->message_text,
1313 'time' => $msg->created_at,
1314 'admin_name' => $msg->admin_name ?: null,
1315 ];
1316 }
1317
1318 wp_send_json_success([
1319 'conversation' => [
1320 'id' => intval($conversation->id),
1321 'name' => $conversation->visitor_name,
1322 'email' => $conversation->visitor_email,
1323 'status' => $conversation->status,
1324 'page_url' => $conversation->last_page_url,
1325 'referrer' => $conversation->referrer,
1326 'user_agent' => $conversation->user_agent,
1327 'ip' => $conversation->ip_address,
1328 'created' => $conversation->created_at,
1329 ],
1330 'messages' => $formatted_messages,
1331 ]);
1332 }
1333
1334 /**
1335 * AJAX: Send admin reply.
1336 *
1337 * @return void
1338 */
1339 public function ajax_send_reply(): void
1340 {
1341 check_ajax_referer('king_live_chat_admin', 'nonce');
1342
1343 if (!current_user_can('manage_options')) {
1344 wp_send_json_error('Unauthorized');
1345 }
1346
1347 global $wpdb;
1348
1349 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1350 $message = sanitize_textarea_field($_POST['message'] ?? '');
1351
1352 if (!$conversation_id || empty($message)) {
1353 wp_send_json_error('Missing required fields');
1354 }
1355
1356 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1357 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1358
1359 // Get conversation
1360 $conversation = $wpdb->get_row($wpdb->prepare(
1361 "SELECT * FROM $table WHERE id = %d",
1362 $conversation_id
1363 ));
1364
1365 if (!$conversation) {
1366 wp_send_json_error('Conversation not found');
1367 }
1368
1369 // Insert message
1370 $wpdb->insert($messages_table, [
1371 'conversation_id' => $conversation_id,
1372 'author_type' => 'admin',
1373 'author_user_id' => get_current_user_id(),
1374 'message_text' => $message,
1375 'created_at' => current_time('mysql'),
1376 ]);
1377 $message_id = $wpdb->insert_id;
1378
1379 // Update conversation
1380 $wpdb->update($table, [
1381 'last_message_at' => current_time('mysql'),
1382 'unread_visitor' => $conversation->unread_visitor + 1,
1383 ], ['id' => $conversation_id]);
1384
1385 // Send email to visitor
1386 if (!empty($conversation->visitor_email)) {
1387 $this->send_visitor_notification(
1388 $conversation->visitor_email,
1389 $conversation->visitor_name,
1390 $message
1391 );
1392 }
1393
1394 $user = wp_get_current_user();
1395
1396 wp_send_json_success([
1397 'message' => [
1398 'id' => $message_id,
1399 'type' => 'admin',
1400 'text' => $message,
1401 'time' => current_time('mysql'),
1402 'admin_name' => $user->display_name,
1403 ],
1404 ]);
1405 }
1406
1407 /**
1408 * AJAX: Update conversation status.
1409 *
1410 * @return void
1411 */
1412 public function ajax_update_status(): void
1413 {
1414 check_ajax_referer('king_live_chat_admin', 'nonce');
1415
1416 if (!current_user_can('manage_options')) {
1417 wp_send_json_error('Unauthorized');
1418 }
1419
1420 global $wpdb;
1421
1422 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1423 $status = sanitize_text_field($_POST['status'] ?? '');
1424
1425 if (!$conversation_id || !in_array($status, ['open', 'closed'], true)) {
1426 wp_send_json_error('Invalid parameters');
1427 }
1428
1429 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1430
1431 $updated = $wpdb->update(
1432 $table,
1433 ['status' => $status],
1434 ['id' => $conversation_id]
1435 );
1436
1437 wp_send_json_success(['updated' => $updated !== false]);
1438 }
1439
1440 /**
1441 * AJAX: Delete conversation.
1442 *
1443 * @return void
1444 */
1445 public function ajax_delete_conversation(): void
1446 {
1447 check_ajax_referer('king_live_chat_admin', 'nonce');
1448
1449 if (!current_user_can('manage_options')) {
1450 wp_send_json_error('Unauthorized');
1451 }
1452
1453 global $wpdb;
1454
1455 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1456
1457 if (!$conversation_id) {
1458 wp_send_json_error('Invalid conversation');
1459 }
1460
1461 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1462 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1463
1464 // Delete messages first
1465 $wpdb->delete($messages_table, ['conversation_id' => $conversation_id]);
1466
1467 // Delete conversation
1468 $deleted = $wpdb->delete($table, ['id' => $conversation_id]);
1469
1470 wp_send_json_success(['deleted' => $deleted !== false]);
1471 }
1472
1473 /**
1474 * Handles settings save.
1475 *
1476 * @return void
1477 */
1478 public function handle_save_settings(): void
1479 {
1480 if (!current_user_can('manage_options')) {
1481 wp_die('Unauthorized');
1482 }
1483
1484 check_admin_referer('king_addons_live_chat_save', 'king_live_chat_nonce');
1485
1486 $options = [];
1487
1488 // General
1489 $options['enabled'] = !empty($_POST['enabled']);
1490 $options['widget_mode'] = in_array($_POST['widget_mode'] ?? 'live_chat', ['live_chat', 'contact_form'])
1491 ? sanitize_text_field($_POST['widget_mode'])
1492 : 'live_chat';
1493 $options['position'] = sanitize_text_field($_POST['position'] ?? 'right');
1494 $options['offset_bottom'] = intval($_POST['offset_bottom'] ?? 20);
1495 $options['offset_side'] = intval($_POST['offset_side'] ?? 20);
1496 $options['z_index'] = intval($_POST['z_index'] ?? 9999);
1497
1498 // Appearance
1499 $options['button_size'] = intval($_POST['button_size'] ?? 60);
1500 $options['button_color'] = sanitize_hex_color($_POST['button_color'] ?? '#0066ff');
1501 $options['header_bg'] = sanitize_hex_color($_POST['header_bg'] ?? '#0066ff');
1502 $options['header_text_color'] = sanitize_hex_color($_POST['header_text_color'] ?? '#ffffff');
1503 $options['chat_bg'] = sanitize_hex_color($_POST['chat_bg'] ?? '#ffffff');
1504 $options['chat_width'] = intval($_POST['chat_width'] ?? 380);
1505 $options['chat_height'] = intval($_POST['chat_height'] ?? 520);
1506 $options['visitor_msg_bg'] = sanitize_hex_color($_POST['visitor_msg_bg'] ?? '#e8f4fd');
1507 $options['visitor_msg_text'] = sanitize_hex_color($_POST['visitor_msg_text'] ?? '#1d1d1f');
1508 $options['admin_msg_bg'] = sanitize_hex_color($_POST['admin_msg_bg'] ?? '#0066ff');
1509 $options['admin_msg_text'] = sanitize_hex_color($_POST['admin_msg_text'] ?? '#ffffff');
1510
1511 // Texts
1512 $options['header_title'] = sanitize_text_field($_POST['header_title'] ?? '');
1513 $options['header_subtitle'] = sanitize_text_field($_POST['header_subtitle'] ?? '');
1514 $options['placeholder'] = sanitize_text_field($_POST['placeholder'] ?? '');
1515 $options['offline_message'] = sanitize_textarea_field($_POST['offline_message'] ?? '');
1516 $options['welcome_message'] = sanitize_textarea_field($_POST['welcome_message'] ?? '');
1517
1518 // Contact Form mode texts
1519 $options['subject_label'] = sanitize_text_field($_POST['subject_label'] ?? __('Subject', 'king-addons'));
1520 $options['message_label'] = sanitize_text_field($_POST['message_label'] ?? __('Your message', 'king-addons'));
1521 $options['submit_button'] = sanitize_text_field($_POST['submit_button'] ?? __('Send Message', 'king-addons'));
1522 $options['success_message'] = sanitize_textarea_field($_POST['success_message'] ?? __('Thank you! Your message has been sent.', 'king-addons'));
1523
1524 // Pre-chat form
1525 $options['require_name'] = !empty($_POST['require_name']);
1526 $options['require_email'] = !empty($_POST['require_email']);
1527 $options['name_label'] = sanitize_text_field($_POST['name_label'] ?? '');
1528 $options['email_label'] = sanitize_text_field($_POST['email_label'] ?? '');
1529 $options['start_chat_button'] = sanitize_text_field($_POST['start_chat_button'] ?? '');
1530
1531 // Email
1532 $options['admin_email'] = sanitize_email($_POST['admin_email'] ?? '');
1533 $options['notify_new_conversation'] = !empty($_POST['notify_new_conversation']);
1534 $options['notify_new_message'] = !empty($_POST['notify_new_message']);
1535 $options['email_subject_admin'] = sanitize_text_field($_POST['email_subject_admin'] ?? '');
1536 $options['email_subject_visitor'] = sanitize_text_field($_POST['email_subject_visitor'] ?? '');
1537
1538 // Polling
1539 $options['poll_interval'] = max(2000, intval($_POST['poll_interval'] ?? self::DEFAULT_POLL_INTERVAL));
1540
1541 update_option(self::OPTION_NAME, $options);
1542
1543 wp_redirect(admin_url('admin.php?page=king-addons-live-chat&tab=settings&saved=1'));
1544 exit;
1545 }
1546 }
1547