PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
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.49, at includes/extensions/Live_Chat/Live_Chat.php

1,531 lines 54.5 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' => '__return_true',
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' => '__return_true',
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' => '__return_true',
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' => '__return_true',
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' => '__return_true',
678 ]);
679 }
680
681 /**
682 * REST: Submit contact form (Contact Form mode).
683 *
684 * @param \WP_REST_Request $request Request object.
685 * @return \WP_REST_Response
686 */
687 public function rest_submit_contact_form(\WP_REST_Request $request): \WP_REST_Response
688 {
689 global $wpdb;
690
691 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
692 $name = sanitize_text_field($request->get_param('name') ?? '');
693 $email = sanitize_email($request->get_param('email') ?? '');
694 $subject = sanitize_text_field($request->get_param('subject') ?? '');
695 $message = sanitize_textarea_field($request->get_param('message') ?? '');
696 $page_url = esc_url_raw($request->get_param('page_url') ?? '');
697 $referrer = esc_url_raw($request->get_param('referrer') ?? '');
698
699 if (empty($visitor_id) || empty($message)) {
700 return new \WP_REST_Response(['success' => false, 'message' => 'Missing required fields'], 400);
701 }
702
703 // Rate limiting
704 if (!$this->check_rate_limit($visitor_id)) {
705 return new \WP_REST_Response([
706 'success' => false,
707 'message' => __('Too many messages. Please wait a moment.', 'king-addons'),
708 ], 429);
709 }
710
711 $conversations_table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
712 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
713
714 // Create conversation
715 $wpdb->insert($conversations_table, [
716 'visitor_id' => $visitor_id,
717 'visitor_name' => $name,
718 'visitor_email' => $email,
719 'status' => 'open',
720 'last_page_url' => $page_url,
721 'referrer' => $referrer,
722 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
723 'ip_address' => $this->get_client_ip(),
724 'created_at' => current_time('mysql'),
725 'last_message_at' => current_time('mysql'),
726 'unread_admin' => 1,
727 ]);
728
729 $conversation_id = $wpdb->insert_id;
730
731 if (!$conversation_id) {
732 return new \WP_REST_Response(['success' => false, 'message' => 'Failed to create conversation'], 500);
733 }
734
735 // Combine subject and message
736 $full_message = $message;
737 if (!empty($subject)) {
738 $full_message = "[{$subject}]\n\n{$message}";
739 }
740
741 // Insert message
742 $wpdb->insert($messages_table, [
743 'conversation_id' => $conversation_id,
744 'author_type' => 'visitor',
745 'message_text' => $full_message,
746 'created_at' => current_time('mysql'),
747 ]);
748
749 // Close conversation immediately for contact form mode
750 $wpdb->update(
751 $conversations_table,
752 ['status' => 'closed'],
753 ['id' => $conversation_id]
754 );
755
756 // Send email notification
757 $this->send_admin_notification($conversation_id, $name, $email, $full_message);
758
759 return new \WP_REST_Response([
760 'success' => true,
761 'message' => __('Message sent successfully', 'king-addons'),
762 ]);
763 }
764
765 /**
766 * REST: Initialize or restore conversation.
767 *
768 * @param \WP_REST_Request $request Request object.
769 * @return \WP_REST_Response
770 */
771 public function rest_init_conversation(\WP_REST_Request $request): \WP_REST_Response
772 {
773 global $wpdb;
774
775 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
776 $name = sanitize_text_field($request->get_param('name') ?? '');
777 $email = sanitize_email($request->get_param('email') ?? '');
778 $page_url = esc_url_raw($request->get_param('page_url') ?? '');
779 $referrer = esc_url_raw($request->get_param('referrer') ?? '');
780
781 if (empty($visitor_id)) {
782 return new \WP_REST_Response(['error' => 'Invalid visitor ID'], 400);
783 }
784
785 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
786 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
787
788 // Check for existing open conversation
789 $conversation = $wpdb->get_row($wpdb->prepare(
790 "SELECT * FROM $table WHERE visitor_id = %s AND status = 'open' ORDER BY created_at DESC LIMIT 1",
791 $visitor_id
792 ));
793
794 if ($conversation) {
795 // Update visitor info if provided
796 if (!empty($name) || !empty($email)) {
797 $wpdb->update(
798 $table,
799 array_filter([
800 'visitor_name' => $name ?: null,
801 'visitor_email' => $email ?: null,
802 ]),
803 ['id' => $conversation->id]
804 );
805 }
806
807 // Get messages
808 $messages = $wpdb->get_results($wpdb->prepare(
809 "SELECT * FROM $messages_table WHERE conversation_id = %d ORDER BY created_at ASC",
810 $conversation->id
811 ));
812
813 return new \WP_REST_Response([
814 'conversation_id' => $conversation->id,
815 'messages' => $this->format_messages($messages),
816 'unread' => intval($conversation->unread_visitor),
817 ]);
818 }
819
820 // No existing conversation - will be created on first message
821 return new \WP_REST_Response([
822 'conversation_id' => null,
823 'messages' => [],
824 'unread' => 0,
825 ]);
826 }
827
828 /**
829 * REST: Send message from visitor.
830 *
831 * @param \WP_REST_Request $request Request object.
832 * @return \WP_REST_Response
833 */
834 public function rest_send_message(\WP_REST_Request $request): \WP_REST_Response
835 {
836 global $wpdb;
837
838 // Honeypot check
839 $honeypot = $request->get_param('website');
840 if (!empty($honeypot)) {
841 return new \WP_REST_Response(['error' => 'Spam detected'], 403);
842 }
843
844 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
845 $conversation_id = intval($request->get_param('conversation_id') ?? 0);
846 $message = sanitize_textarea_field($request->get_param('message') ?? '');
847 $name = sanitize_text_field($request->get_param('name') ?? '');
848 $email = sanitize_email($request->get_param('email') ?? '');
849 $page_url = esc_url_raw($request->get_param('page_url') ?? '');
850 $referrer = esc_url_raw($request->get_param('referrer') ?? '');
851
852 if (empty($visitor_id) || empty($message)) {
853 return new \WP_REST_Response(['error' => 'Missing required fields'], 400);
854 }
855
856 // Rate limiting
857 if (!$this->check_rate_limit($visitor_id)) {
858 return new \WP_REST_Response(['error' => 'rate_limit'], 429);
859 }
860
861 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
862 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
863
864 // Create conversation if needed
865 if (!$conversation_id) {
866 $wpdb->insert($table, [
867 'visitor_id' => $visitor_id,
868 'visitor_name' => $name,
869 'visitor_email' => $email,
870 'status' => 'open',
871 'last_page_url' => $page_url,
872 'referrer' => $referrer,
873 'user_agent' => sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''),
874 'ip_address' => $this->get_client_ip(),
875 'created_at' => current_time('mysql'),
876 'last_message_at' => current_time('mysql'),
877 'unread_admin' => 1,
878 ]);
879 $conversation_id = $wpdb->insert_id;
880 $is_new = true;
881 } else {
882 // Verify conversation belongs to visitor
883 $conv = $wpdb->get_row($wpdb->prepare(
884 "SELECT id FROM $table WHERE id = %d AND visitor_id = %s",
885 $conversation_id,
886 $visitor_id
887 ));
888
889 if (!$conv) {
890 return new \WP_REST_Response(['error' => 'Invalid conversation'], 403);
891 }
892
893 // Update conversation
894 $wpdb->update($table, [
895 'last_message_at' => current_time('mysql'),
896 'unread_admin' => $wpdb->get_var($wpdb->prepare(
897 "SELECT unread_admin FROM $table WHERE id = %d",
898 $conversation_id
899 )) + 1,
900 ], ['id' => $conversation_id]);
901 $is_new = false;
902 }
903
904 // Insert message
905 $wpdb->insert($messages_table, [
906 'conversation_id' => $conversation_id,
907 'author_type' => 'visitor',
908 'message_text' => $message,
909 'created_at' => current_time('mysql'),
910 ]);
911 $message_id = $wpdb->insert_id;
912
913 // Send email notification to admin
914 if ($is_new && $this->options['notify_new_conversation']) {
915 $this->send_admin_notification($conversation_id, $message, $name, $email);
916 } elseif (!$is_new && $this->options['notify_new_message']) {
917 $this->send_admin_notification($conversation_id, $message, $name, $email, false);
918 }
919
920 // Update rate limit
921 $this->update_rate_limit($visitor_id);
922
923 return new \WP_REST_Response([
924 'success' => true,
925 'conversation_id' => $conversation_id,
926 'message_id' => $message_id,
927 'created_at' => current_time('mysql'),
928 ]);
929 }
930
931 /**
932 * REST: Poll for new messages.
933 *
934 * @param \WP_REST_Request $request Request object.
935 * @return \WP_REST_Response
936 */
937 public function rest_poll_messages(\WP_REST_Request $request): \WP_REST_Response
938 {
939 global $wpdb;
940
941 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
942 $conversation_id = intval($request->get_param('conversation_id') ?? 0);
943 $after_id = intval($request->get_param('after_id') ?? 0);
944
945 if (empty($visitor_id) || !$conversation_id) {
946 return new \WP_REST_Response(['messages' => [], 'unread' => 0]);
947 }
948
949 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
950 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
951
952 // Verify conversation
953 $conv = $wpdb->get_row($wpdb->prepare(
954 "SELECT id, unread_visitor FROM $table WHERE id = %d AND visitor_id = %s",
955 $conversation_id,
956 $visitor_id
957 ));
958
959 if (!$conv) {
960 return new \WP_REST_Response(['messages' => [], 'unread' => 0]);
961 }
962
963 // Get new messages
964 $messages = $wpdb->get_results($wpdb->prepare(
965 "SELECT * FROM $messages_table WHERE conversation_id = %d AND id > %d ORDER BY created_at ASC",
966 $conversation_id,
967 $after_id
968 ));
969
970 return new \WP_REST_Response([
971 'messages' => $this->format_messages($messages),
972 'unread' => intval($conv->unread_visitor),
973 ]);
974 }
975
976 /**
977 * REST: Mark messages as read.
978 *
979 * @param \WP_REST_Request $request Request object.
980 * @return \WP_REST_Response
981 */
982 public function rest_mark_read(\WP_REST_Request $request): \WP_REST_Response
983 {
984 global $wpdb;
985
986 $visitor_id = sanitize_text_field($request->get_param('visitor_id') ?? '');
987 $conversation_id = intval($request->get_param('conversation_id') ?? 0);
988
989 if (empty($visitor_id) || !$conversation_id) {
990 return new \WP_REST_Response(['success' => false]);
991 }
992
993 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
994 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
995
996 // Verify and update
997 $updated = $wpdb->update(
998 $table,
999 ['unread_visitor' => 0],
1000 ['id' => $conversation_id, 'visitor_id' => $visitor_id]
1001 );
1002
1003 // Mark admin messages as read
1004 $wpdb->query($wpdb->prepare(
1005 "UPDATE $messages_table SET is_read = 1 WHERE conversation_id = %d AND author_type = 'admin'",
1006 $conversation_id
1007 ));
1008
1009 return new \WP_REST_Response(['success' => $updated !== false]);
1010 }
1011
1012 /**
1013 * Formats messages for JSON response.
1014 *
1015 * @param array $messages Database rows.
1016 * @return array
1017 */
1018 private function format_messages(array $messages): array
1019 {
1020 $formatted = [];
1021 foreach ($messages as $msg) {
1022 $formatted[] = [
1023 'id' => intval($msg->id),
1024 'type' => $msg->author_type,
1025 'text' => $msg->message_text,
1026 'time' => $msg->created_at,
1027 'is_read' => (bool) $msg->is_read,
1028 ];
1029 }
1030 return $formatted;
1031 }
1032
1033 /**
1034 * Checks rate limit for visitor.
1035 *
1036 * @param string $visitor_id Visitor ID.
1037 * @return bool
1038 */
1039 private function check_rate_limit(string $visitor_id): bool
1040 {
1041 $transient_key = 'ka_chat_rl_' . md5($visitor_id);
1042 $data = get_transient($transient_key);
1043
1044 if (!$data) {
1045 return true;
1046 }
1047
1048 // Check minimum time between messages
1049 if (time() - $data['last'] < self::RATE_LIMIT_SECONDS) {
1050 return false;
1051 }
1052
1053 // Check max messages in window
1054 if ($data['count'] >= self::RATE_LIMIT_MAX_MESSAGES) {
1055 return false;
1056 }
1057
1058 return true;
1059 }
1060
1061 /**
1062 * Updates rate limit counter.
1063 *
1064 * @param string $visitor_id Visitor ID.
1065 * @return void
1066 */
1067 private function update_rate_limit(string $visitor_id): void
1068 {
1069 $transient_key = 'ka_chat_rl_' . md5($visitor_id);
1070 $data = get_transient($transient_key);
1071
1072 if (!$data) {
1073 $data = ['count' => 0, 'last' => 0];
1074 }
1075
1076 $data['count']++;
1077 $data['last'] = time();
1078
1079 set_transient($transient_key, $data, self::RATE_LIMIT_WINDOW);
1080 }
1081
1082 /**
1083 * Gets client IP address.
1084 *
1085 * @return string
1086 */
1087 private function get_client_ip(): string
1088 {
1089 $ip = '';
1090
1091 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
1092 $ip = $_SERVER['HTTP_CLIENT_IP'];
1093 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1094 $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
1095 } elseif (!empty($_SERVER['REMOTE_ADDR'])) {
1096 $ip = $_SERVER['REMOTE_ADDR'];
1097 }
1098
1099 return sanitize_text_field($ip);
1100 }
1101
1102 /**
1103 * Sends email notification to admin.
1104 *
1105 * @param int $conversation_id Conversation ID.
1106 * @param string $message Message text.
1107 * @param string $name Visitor name.
1108 * @param string $email Visitor email.
1109 * @param bool $is_new Whether this is a new conversation.
1110 * @return void
1111 */
1112 private function send_admin_notification(int $conversation_id, string $message, string $name, string $email, bool $is_new = true): void
1113 {
1114 $admin_email = $this->options['admin_email'];
1115 if (empty($admin_email)) {
1116 return;
1117 }
1118
1119 $subject = str_replace(
1120 ['{visitor_name}', '{site_name}'],
1121 [$name ?: __('Visitor', 'king-addons'), get_bloginfo('name')],
1122 $this->options['email_subject_admin']
1123 );
1124
1125 $inbox_url = admin_url('admin.php?page=king-addons-live-chat&conversation=' . $conversation_id);
1126
1127 $body = sprintf(
1128 "%s\n\n%s: %s\n%s: %s\n\n%s:\n%s\n\n%s:\n%s",
1129 $is_new ? __('New support conversation started', 'king-addons') : __('New message in support conversation', 'king-addons'),
1130 __('Name', 'king-addons'),
1131 $name ?: __('Not provided', 'king-addons'),
1132 __('Email', 'king-addons'),
1133 $email ?: __('Not provided', 'king-addons'),
1134 __('Message', 'king-addons'),
1135 $message,
1136 __('View conversation', 'king-addons'),
1137 $inbox_url
1138 );
1139
1140 wp_mail($admin_email, $subject, $body);
1141 }
1142
1143 /**
1144 * Sends email to visitor with admin reply.
1145 *
1146 * @param string $email Visitor email.
1147 * @param string $name Visitor name.
1148 * @param string $message Reply text.
1149 * @return bool
1150 */
1151 public function send_visitor_notification(string $email, string $name, string $message): bool
1152 {
1153 if (empty($email) || !is_email($email)) {
1154 return false;
1155 }
1156
1157 $subject = str_replace(
1158 ['{visitor_name}', '{site_name}'],
1159 [$name ?: __('there', 'king-addons'), get_bloginfo('name')],
1160 $this->options['email_subject_visitor']
1161 );
1162
1163 $body = sprintf(
1164 "%s %s,\n\n%s\n\n--\n%s",
1165 __('Hi', 'king-addons'),
1166 $name ?: '',
1167 $message,
1168 get_bloginfo('name')
1169 );
1170
1171 return wp_mail($email, $subject, $body);
1172 }
1173
1174 /**
1175 * AJAX: Get conversations list.
1176 *
1177 * @return void
1178 */
1179 public function ajax_get_conversations(): void
1180 {
1181 check_ajax_referer('king_live_chat_admin', 'nonce');
1182
1183 if (!current_user_can('manage_options')) {
1184 wp_send_json_error('Unauthorized');
1185 }
1186
1187 global $wpdb;
1188 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1189
1190 $status = sanitize_text_field($_POST['status'] ?? 'all');
1191 $search = sanitize_text_field($_POST['search'] ?? '');
1192 $page = max(1, intval($_POST['page'] ?? 1));
1193 $per_page = 20;
1194 $offset = ($page - 1) * $per_page;
1195
1196 $where = "1=1";
1197 $params = [];
1198
1199 if ($status !== 'all') {
1200 $where .= " AND status = %s";
1201 $params[] = $status;
1202 }
1203
1204 if (!empty($search)) {
1205 $where .= " AND (visitor_name LIKE %s OR visitor_email LIKE %s)";
1206 $search_param = '%' . $wpdb->esc_like($search) . '%';
1207 $params[] = $search_param;
1208 $params[] = $search_param;
1209 }
1210
1211 $total = $wpdb->get_var(
1212 empty($params)
1213 ? "SELECT COUNT(*) FROM $table WHERE $where"
1214 : $wpdb->prepare("SELECT COUNT(*) FROM $table WHERE $where", ...$params)
1215 );
1216
1217 $query = "SELECT * FROM $table WHERE $where ORDER BY last_message_at DESC LIMIT %d OFFSET %d";
1218 $params[] = $per_page;
1219 $params[] = $offset;
1220
1221 $conversations = $wpdb->get_results($wpdb->prepare($query, ...$params));
1222
1223 $formatted = [];
1224 foreach ($conversations as $conv) {
1225 $formatted[] = [
1226 'id' => intval($conv->id),
1227 'name' => $conv->visitor_name ?: __('Anonymous', 'king-addons'),
1228 'email' => $conv->visitor_email,
1229 'status' => $conv->status,
1230 'unread' => intval($conv->unread_admin),
1231 'last_message' => $conv->last_message_at,
1232 'created' => $conv->created_at,
1233 ];
1234 }
1235
1236 wp_send_json_success([
1237 'conversations' => $formatted,
1238 'total' => intval($total),
1239 'pages' => ceil($total / $per_page),
1240 ]);
1241 }
1242
1243 /**
1244 * AJAX: Get single conversation with messages.
1245 *
1246 * @return void
1247 */
1248 public function ajax_get_conversation(): void
1249 {
1250 check_ajax_referer('king_live_chat_admin', 'nonce');
1251
1252 if (!current_user_can('manage_options')) {
1253 wp_send_json_error('Unauthorized');
1254 }
1255
1256 global $wpdb;
1257 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1258
1259 if (!$conversation_id) {
1260 wp_send_json_error('Invalid conversation');
1261 }
1262
1263 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1264 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1265
1266 $conversation = $wpdb->get_row($wpdb->prepare(
1267 "SELECT * FROM $table WHERE id = %d",
1268 $conversation_id
1269 ));
1270
1271 if (!$conversation) {
1272 wp_send_json_error('Conversation not found');
1273 }
1274
1275 // Mark as read
1276 $wpdb->update($table, ['unread_admin' => 0], ['id' => $conversation_id]);
1277 $wpdb->query($wpdb->prepare(
1278 "UPDATE $messages_table SET is_read = 1 WHERE conversation_id = %d AND author_type = 'visitor'",
1279 $conversation_id
1280 ));
1281
1282 $messages = $wpdb->get_results($wpdb->prepare(
1283 "SELECT m.*, u.display_name as admin_name
1284 FROM $messages_table m
1285 LEFT JOIN {$wpdb->users} u ON m.author_user_id = u.ID
1286 WHERE m.conversation_id = %d
1287 ORDER BY m.created_at ASC",
1288 $conversation_id
1289 ));
1290
1291 $formatted_messages = [];
1292 foreach ($messages as $msg) {
1293 $formatted_messages[] = [
1294 'id' => intval($msg->id),
1295 'type' => $msg->author_type,
1296 'text' => $msg->message_text,
1297 'time' => $msg->created_at,
1298 'admin_name' => $msg->admin_name ?: null,
1299 ];
1300 }
1301
1302 wp_send_json_success([
1303 'conversation' => [
1304 'id' => intval($conversation->id),
1305 'name' => $conversation->visitor_name,
1306 'email' => $conversation->visitor_email,
1307 'status' => $conversation->status,
1308 'page_url' => $conversation->last_page_url,
1309 'referrer' => $conversation->referrer,
1310 'user_agent' => $conversation->user_agent,
1311 'ip' => $conversation->ip_address,
1312 'created' => $conversation->created_at,
1313 ],
1314 'messages' => $formatted_messages,
1315 ]);
1316 }
1317
1318 /**
1319 * AJAX: Send admin reply.
1320 *
1321 * @return void
1322 */
1323 public function ajax_send_reply(): void
1324 {
1325 check_ajax_referer('king_live_chat_admin', 'nonce');
1326
1327 if (!current_user_can('manage_options')) {
1328 wp_send_json_error('Unauthorized');
1329 }
1330
1331 global $wpdb;
1332
1333 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1334 $message = sanitize_textarea_field($_POST['message'] ?? '');
1335
1336 if (!$conversation_id || empty($message)) {
1337 wp_send_json_error('Missing required fields');
1338 }
1339
1340 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1341 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1342
1343 // Get conversation
1344 $conversation = $wpdb->get_row($wpdb->prepare(
1345 "SELECT * FROM $table WHERE id = %d",
1346 $conversation_id
1347 ));
1348
1349 if (!$conversation) {
1350 wp_send_json_error('Conversation not found');
1351 }
1352
1353 // Insert message
1354 $wpdb->insert($messages_table, [
1355 'conversation_id' => $conversation_id,
1356 'author_type' => 'admin',
1357 'author_user_id' => get_current_user_id(),
1358 'message_text' => $message,
1359 'created_at' => current_time('mysql'),
1360 ]);
1361 $message_id = $wpdb->insert_id;
1362
1363 // Update conversation
1364 $wpdb->update($table, [
1365 'last_message_at' => current_time('mysql'),
1366 'unread_visitor' => $conversation->unread_visitor + 1,
1367 ], ['id' => $conversation_id]);
1368
1369 // Send email to visitor
1370 if (!empty($conversation->visitor_email)) {
1371 $this->send_visitor_notification(
1372 $conversation->visitor_email,
1373 $conversation->visitor_name,
1374 $message
1375 );
1376 }
1377
1378 $user = wp_get_current_user();
1379
1380 wp_send_json_success([
1381 'message' => [
1382 'id' => $message_id,
1383 'type' => 'admin',
1384 'text' => $message,
1385 'time' => current_time('mysql'),
1386 'admin_name' => $user->display_name,
1387 ],
1388 ]);
1389 }
1390
1391 /**
1392 * AJAX: Update conversation status.
1393 *
1394 * @return void
1395 */
1396 public function ajax_update_status(): void
1397 {
1398 check_ajax_referer('king_live_chat_admin', 'nonce');
1399
1400 if (!current_user_can('manage_options')) {
1401 wp_send_json_error('Unauthorized');
1402 }
1403
1404 global $wpdb;
1405
1406 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1407 $status = sanitize_text_field($_POST['status'] ?? '');
1408
1409 if (!$conversation_id || !in_array($status, ['open', 'closed'], true)) {
1410 wp_send_json_error('Invalid parameters');
1411 }
1412
1413 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1414
1415 $updated = $wpdb->update(
1416 $table,
1417 ['status' => $status],
1418 ['id' => $conversation_id]
1419 );
1420
1421 wp_send_json_success(['updated' => $updated !== false]);
1422 }
1423
1424 /**
1425 * AJAX: Delete conversation.
1426 *
1427 * @return void
1428 */
1429 public function ajax_delete_conversation(): void
1430 {
1431 check_ajax_referer('king_live_chat_admin', 'nonce');
1432
1433 if (!current_user_can('manage_options')) {
1434 wp_send_json_error('Unauthorized');
1435 }
1436
1437 global $wpdb;
1438
1439 $conversation_id = intval($_POST['conversation_id'] ?? 0);
1440
1441 if (!$conversation_id) {
1442 wp_send_json_error('Invalid conversation');
1443 }
1444
1445 $table = $wpdb->prefix . self::TABLE_CONVERSATIONS;
1446 $messages_table = $wpdb->prefix . self::TABLE_MESSAGES;
1447
1448 // Delete messages first
1449 $wpdb->delete($messages_table, ['conversation_id' => $conversation_id]);
1450
1451 // Delete conversation
1452 $deleted = $wpdb->delete($table, ['id' => $conversation_id]);
1453
1454 wp_send_json_success(['deleted' => $deleted !== false]);
1455 }
1456
1457 /**
1458 * Handles settings save.
1459 *
1460 * @return void
1461 */
1462 public function handle_save_settings(): void
1463 {
1464 if (!current_user_can('manage_options')) {
1465 wp_die('Unauthorized');
1466 }
1467
1468 check_admin_referer('king_addons_live_chat_save', 'king_live_chat_nonce');
1469
1470 $options = [];
1471
1472 // General
1473 $options['enabled'] = !empty($_POST['enabled']);
1474 $options['widget_mode'] = in_array($_POST['widget_mode'] ?? 'live_chat', ['live_chat', 'contact_form'])
1475 ? sanitize_text_field($_POST['widget_mode'])
1476 : 'live_chat';
1477 $options['position'] = sanitize_text_field($_POST['position'] ?? 'right');
1478 $options['offset_bottom'] = intval($_POST['offset_bottom'] ?? 20);
1479 $options['offset_side'] = intval($_POST['offset_side'] ?? 20);
1480 $options['z_index'] = intval($_POST['z_index'] ?? 9999);
1481
1482 // Appearance
1483 $options['button_size'] = intval($_POST['button_size'] ?? 60);
1484 $options['button_color'] = sanitize_hex_color($_POST['button_color'] ?? '#0066ff');
1485 $options['header_bg'] = sanitize_hex_color($_POST['header_bg'] ?? '#0066ff');
1486 $options['header_text_color'] = sanitize_hex_color($_POST['header_text_color'] ?? '#ffffff');
1487 $options['chat_bg'] = sanitize_hex_color($_POST['chat_bg'] ?? '#ffffff');
1488 $options['chat_width'] = intval($_POST['chat_width'] ?? 380);
1489 $options['chat_height'] = intval($_POST['chat_height'] ?? 520);
1490 $options['visitor_msg_bg'] = sanitize_hex_color($_POST['visitor_msg_bg'] ?? '#e8f4fd');
1491 $options['visitor_msg_text'] = sanitize_hex_color($_POST['visitor_msg_text'] ?? '#1d1d1f');
1492 $options['admin_msg_bg'] = sanitize_hex_color($_POST['admin_msg_bg'] ?? '#0066ff');
1493 $options['admin_msg_text'] = sanitize_hex_color($_POST['admin_msg_text'] ?? '#ffffff');
1494
1495 // Texts
1496 $options['header_title'] = sanitize_text_field($_POST['header_title'] ?? '');
1497 $options['header_subtitle'] = sanitize_text_field($_POST['header_subtitle'] ?? '');
1498 $options['placeholder'] = sanitize_text_field($_POST['placeholder'] ?? '');
1499 $options['offline_message'] = sanitize_textarea_field($_POST['offline_message'] ?? '');
1500 $options['welcome_message'] = sanitize_textarea_field($_POST['welcome_message'] ?? '');
1501
1502 // Contact Form mode texts
1503 $options['subject_label'] = sanitize_text_field($_POST['subject_label'] ?? __('Subject', 'king-addons'));
1504 $options['message_label'] = sanitize_text_field($_POST['message_label'] ?? __('Your message', 'king-addons'));
1505 $options['submit_button'] = sanitize_text_field($_POST['submit_button'] ?? __('Send Message', 'king-addons'));
1506 $options['success_message'] = sanitize_textarea_field($_POST['success_message'] ?? __('Thank you! Your message has been sent.', 'king-addons'));
1507
1508 // Pre-chat form
1509 $options['require_name'] = !empty($_POST['require_name']);
1510 $options['require_email'] = !empty($_POST['require_email']);
1511 $options['name_label'] = sanitize_text_field($_POST['name_label'] ?? '');
1512 $options['email_label'] = sanitize_text_field($_POST['email_label'] ?? '');
1513 $options['start_chat_button'] = sanitize_text_field($_POST['start_chat_button'] ?? '');
1514
1515 // Email
1516 $options['admin_email'] = sanitize_email($_POST['admin_email'] ?? '');
1517 $options['notify_new_conversation'] = !empty($_POST['notify_new_conversation']);
1518 $options['notify_new_message'] = !empty($_POST['notify_new_message']);
1519 $options['email_subject_admin'] = sanitize_text_field($_POST['email_subject_admin'] ?? '');
1520 $options['email_subject_visitor'] = sanitize_text_field($_POST['email_subject_visitor'] ?? '');
1521
1522 // Polling
1523 $options['poll_interval'] = max(2000, intval($_POST['poll_interval'] ?? self::DEFAULT_POLL_INTERVAL));
1524
1525 update_option(self::OPTION_NAME, $options);
1526
1527 wp_redirect(admin_url('admin.php?page=king-addons-live-chat&tab=settings&saved=1'));
1528 exit;
1529 }
1530 }
1531