Changeset 3558865
- Timestamp:
- 06/03/2026 06:37:16 AM (5 months ago)
- Location:
- akukita-chat-with-ai-translation/trunk
- Files:
-
- 3 added
- 14 edited
-
akukita-chat.php (modified) (10 diffs)
-
assets/css/chat.css (modified) (7 diffs)
-
assets/js/chat.js (modified) (11 diffs)
-
assets/js/guest.js (modified) (4 diffs)
-
includes/class-admin.php (modified) (2 diffs)
-
includes/class-database.php (modified) (2 diffs)
-
includes/class-deactivation-feedback.php (added)
-
includes/class-guest.php (modified) (1 diff)
-
includes/class-rest-api.php (modified) (11 diffs)
-
includes/class-setup-wizard.php (added)
-
includes/class-shortcode.php (modified) (3 diffs)
-
readme.txt (modified) (8 diffs)
-
templates/admin-settings.php (modified) (1 diff)
-
templates/chat-widget.php (modified) (2 diffs)
-
templates/register-modal.php (modified) (1 diff)
-
templates/setup-wizard.php (added)
-
uninstall.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
akukita-chat-with-ai-translation/trunk/akukita-chat.php
r3551668 r3558865 10 10 * @wordpress-plugin 11 11 * Plugin Name: Akukita Chat with AI Translation 12 * Plugin URI: https://akukita.es/ plugins/akukita-chat/12 * Plugin URI: https://akukita.es/wordpress-plugins/akukita-chat-with-ai-translation/ 13 13 * Description: Real-time multilingual chat where every message is automatically translated into each user's preferred language. Supports MyMemory (free), DeepL, and OpenAI. 14 * Version: 1. 1.214 * Version: 1.2.0 15 15 * Requires at least: 6.0 16 16 * Requires PHP: 7.4 … … 28 28 29 29 // Plugin constants. 30 define( 'AKCH_VERSION', '1. 1.2' );30 define( 'AKCH_VERSION', '1.2.0' ); 31 31 define( 'AKCH_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 32 32 define( 'AKCH_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 46 46 'guest', 47 47 'freemium', 48 'review-notice' 48 'review-notice', 49 'setup-wizard', 50 'deactivation-feedback' 49 51 ); 50 52 foreach ( $akch_modules as $akch_module ) { … … 133 135 AKCH_Admin::init(); 134 136 AKCH_Review_Notice::init(); 137 AKCH_Setup_Wizard::init(); 138 if ( is_admin() ) { 139 AKCH_Deactivation_Feedback::init(); 140 } 135 141 } 136 142 137 143 /** 138 144 * Enqueue front-end CSS and JS. 139 * JS is only loaded for logged-in users — guests see a self-contained register modal. 145 * In 'account' mode, non-logged-in visitors see a register modal. 146 * In 'guest' or 'registered' mode, the full chat is loaded and the 147 * visitor can participate as a guest with just a display name. 140 148 * 141 149 * @return void … … 151 159 ); 152 160 153 // Registration modal data — passed via wp_add_inline_script so no inline <script> tag 154 // is needed in the template. PHP values (nonce, AJAX URL, i18n) are injected here. 155 if ( ! is_user_logged_in() ) { 161 // Account mode (legacy): non-logged-in visitors see the register modal. 162 if ( ! is_user_logged_in() && ! AKCH_Guest::guests_allowed() ) { 156 163 $akch_reg_data = array( 157 164 'ajaxUrl' => admin_url( 'admin-ajax.php' ), … … 177 184 } 178 185 186 // Logged-in users AND guests (when guest mode is on) get the full chat. 179 187 wp_enqueue_style( 180 188 'akch-chat', … … 197 205 /** 198 206 * Output akchData as an inline <script> in <head>. 199 * Only for logged-in users — non-logged-in users see the registration modal. 207 * Runs for logged-in users always, and for guests when guest mode is on. 208 * In account mode, non-logged-in visitors bail out (they see the modal). 200 209 * 201 210 * @return void 202 211 */ 203 212 public function inline_data() { 204 if ( ! is_user_logged_in() ) { 213 $logged_in = is_user_logged_in(); 214 215 // Bail only in account mode for non-logged-in visitors (register modal). 216 if ( ! $logged_in && ! AKCH_Guest::guests_allowed() ) { 205 217 return; 206 218 } 207 219 208 $user = wp_get_current_user(); 209 $user_lang = get_user_meta( $user->ID, 'akch_preferred_language', true ); 210 if ( ! $user_lang ) { 211 $user_lang = 'en'; 220 if ( $logged_in ) { 221 $user = wp_get_current_user(); 222 $user_id = $user->ID; 223 $user_name = $user->display_name; 224 $user_lang = get_user_meta( $user->ID, 'akch_preferred_language', true ); 225 if ( ! $user_lang ) { 226 $user_lang = get_option( 'akch_default_language', 'en' ); 227 } 228 $is_guest = false; 229 $is_admin = current_user_can( 'manage_options' ); 230 } else { 231 $user_id = AKCH_Guest::current_id(); 232 $user_name = AKCH_Guest::current_name(); 233 $user_lang = AKCH_Guest::current_lang(); 234 $is_guest = true; 235 $is_admin = false; 212 236 } 213 237 … … 217 241 'nonce' => wp_create_nonce( 'akch_nonce' ), 218 242 'restNonce' => wp_create_nonce( 'wp_rest' ), 219 'userId' => $user ->ID,220 'userName' => $user ->display_name,243 'userId' => $user_id, 244 'userName' => $user_name, 221 245 'userLang' => $user_lang, 222 'isGuest' => false, 223 'isRegistered' => true, 224 'isAdmin' => current_user_can( 'manage_options' ), 246 'isGuest' => $is_guest, 247 'needsName' => ( $is_guest && '' === trim( (string) $user_name ) ), 248 'isRegistered' => ! $is_guest, 249 'isAdmin' => $is_admin, 225 250 'pollInterval' => 3000, 226 251 'i18n' => self::get_i18n( $user_lang ), … … 253 278 $base = array( 254 279 'connecting' => __( 'Connecting…', 'akukita-chat-with-ai-translation' ), 280 'welcome' => __( 'Welcome to the Chat', 'akukita-chat-with-ai-translation' ), 281 'welcomeSub' => __( 'Messages are translated automatically for everyone.', 'akukita-chat-with-ai-translation' ), 282 'yourName' => __( 'Your name', 'akukita-chat-with-ai-translation' ), 283 'yourLang' => __( 'Your language', 'akukita-chat-with-ai-translation' ), 284 'enterChat' => __( 'Enter Chat', 'akukita-chat-with-ai-translation' ), 285 'nameTooShort' => __( 'Name must be at least 2 characters.', 'akukita-chat-with-ai-translation' ), 255 286 'translating' => __( 'Translating…', 'akukita-chat-with-ai-translation' ), 256 287 'you' => __( 'You', 'akukita-chat-with-ai-translation' ), … … 356 387 357 388 // Activation / deactivation hooks must be registered at file scope, not inside a class constructor. 358 register_activation_hook( __FILE__, array( 'AKCH_Database', 'install' ));389 register_activation_hook( __FILE__, 'akch_on_activate' ); 359 390 register_deactivation_hook( __FILE__, array( 'Akukita_Chat', 'deactivate_plugin' ) ); 360 391 392 /** 393 * Activation callback: install tables and flag the one-time setup redirect. 394 * 395 * @return void 396 */ 397 function akch_on_activate() { 398 AKCH_Database::install(); 399 if ( class_exists( 'AKCH_Setup_Wizard' ) ) { 400 AKCH_Setup_Wizard::on_activation(); 401 } 402 } 403 404 /** 405 * Add a "Settings" link on the Plugins list row for this plugin. 406 * 407 * @param string[] $links Existing action links. 408 * @return string[] 409 */ 410 function akch_plugin_action_links( $links ) { 411 $settings = '<a href="' . esc_url( admin_url( 'admin.php?page=akukita-chat-with-ai-translation' ) ) . '">' 412 . esc_html__( 'Settings', 'akukita-chat-with-ai-translation' ) . '</a>'; 413 array_unshift( $links, $settings ); 414 return $links; 415 } 416 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'akch_plugin_action_links' ); 417 361 418 Akukita_Chat::get_instance(); -
akukita-chat-with-ai-translation/trunk/assets/css/chat.css
r3525122 r3558865 119 119 .akch-select { 120 120 width: 100%; 121 background: var(--akch-surface-2);121 background: #ffffff; 122 122 border: 1px solid var(--akch-border); 123 color: var(--akch-text);123 color: #1a1a1a; 124 124 border-radius: var(--akch-radius-sm); 125 125 padding: 6px 10px; … … 129 129 outline: none; 130 130 transition: border-color var(--akch-transition); 131 } 132 .akch-select:focus { border-color: var(--akch-accent); } 131 -webkit-appearance: menulist; 132 appearance: menulist; 133 } 134 .akch-select:focus { 135 border-color: var(--akch-accent); 136 } 137 /* Force readable colours in the dropdown across OS themes. 138 Without this, native browser themes (especially dark mode on macOS / Linux) 139 render options with white-on-white or black-on-black combinations. */ 140 .akch-select option { 141 background-color: #ffffff; 142 color: #1a1a1a; 143 } 133 144 134 145 /* ── Room List ── */ … … 509 520 .akch-message-input { 510 521 flex: 1; 511 background: var(--akch-surface-2);522 background: #ffffff; 512 523 border: 1px solid var(--akch-border); 513 524 border-radius: var(--akch-radius); 514 525 padding: 10px 14px; 515 color: var(--akch-text);526 color: #1a1a1a; 516 527 font-family: var(--akch-font); 517 528 font-size: 14px; … … 528 539 box-shadow: 0 0 0 3px rgba(79,110,247,0.15); 529 540 } 530 .akch-message-input::placeholder { color: var(--akch-muted); } 541 /* Placeholder needs an explicit grey — using var(--akch-muted) here would 542 render almost invisible on the new white background. */ 543 .akch-message-input::placeholder { color: #8c8c95; opacity: 1; } 544 .akch-message-input::-webkit-input-placeholder { color: #8c8c95; opacity: 1; } 545 .akch-message-input::-moz-placeholder { color: #8c8c95; opacity: 1; } 531 546 .akch-message-input:disabled { opacity: 0.5; cursor: not-allowed; } 532 547 … … 656 671 .akch-input { 657 672 width: 100%; 658 background: var(--akch-bg);673 background: #ffffff; 659 674 border: 1px solid var(--akch-border); 660 675 border-radius: var(--akch-radius-sm); 661 676 padding: 9px 12px; 662 color: var(--akch-text);677 color: #1a1a1a; 663 678 font-family: var(--akch-font); 664 679 font-size: 14px; … … 667 682 } 668 683 .akch-input:focus { border-color: var(--akch-accent); } 684 .akch-input::placeholder { color: #8c8c95; opacity: 1; } 669 685 670 686 .akch-modal-actions { … … 1007 1023 .akch-auth-card { padding: 28px 20px; } 1008 1024 } 1025 1026 /* ── Guest name gate (guest mode) ───────────────────────────────────────────── 1027 Rendered as a full-viewport fixed overlay so it always appears, regardless 1028 of how the host theme styles or positions the shortcode container. Themes 1029 like Divi often wrap shortcodes in elements with overflow:hidden, transform, 1030 or filter, all of which break position:absolute children. Using fixed + 1031 !important on the layout properties + id selector for higher specificity 1032 defeats those conflicts. 1033 */ 1034 #akch-guest-gate.akch-guest-gate{ 1035 position:fixed !important; 1036 top:0 !important; right:0 !important; bottom:0 !important; left:0 !important; 1037 width:100vw !important; 1038 height:100vh !important; 1039 z-index:999999 !important; 1040 display:flex !important; 1041 align-items:center !important; 1042 justify-content:center !important; 1043 background:rgba(15,17,23,.78) !important; 1044 backdrop-filter:blur(4px); 1045 -webkit-backdrop-filter:blur(4px); 1046 margin:0 !important; 1047 padding:16px !important; 1048 box-sizing:border-box !important; 1049 } 1050 .akch-guest-gate-card{ 1051 background:#ffffff !important; 1052 color:#1a1a1a !important; 1053 border:1px solid #e2e2e7 !important; 1054 border-radius:14px !important; 1055 padding:26px 28px !important; 1056 max-width:360px !important; 1057 width:90% !important; 1058 box-shadow:0 12px 40px rgba(0,0,0,.35) !important; 1059 text-align:left !important; 1060 font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; 1061 } 1062 .akch-guest-gate-card h3{margin:0 0 6px !important;font-size:18px !important;color:#1a1a1a !important;} 1063 .akch-guest-gate-card p{margin:0 0 16px !important;color:#646970 !important;font-size:13px !important;} 1064 .akch-guest-gate-card label{ 1065 display:block !important;font-size:12px !important;font-weight:600 !important; 1066 margin:10px 0 4px !important;color:#3c434a !important; 1067 } 1068 .akch-guest-gate-card input, 1069 .akch-guest-gate-card select{ 1070 width:100% !important;padding:9px 11px !important;border:1px solid #d5d5db !important; 1071 border-radius:8px !important;font-size:14px !important;box-sizing:border-box !important; 1072 background:#ffffff !important;color:#1a1a1a !important; 1073 -webkit-appearance:menulist !important; appearance:menulist !important; 1074 margin:0 !important; 1075 } 1076 .akch-guest-gate-card select option{background:#ffffff !important;color:#1a1a1a !important;} 1077 .akch-guest-enter{ 1078 margin-top:16px !important;width:100% !important;padding:11px !important; 1079 border:0 !important;border-radius:8px !important;cursor:pointer !important; 1080 background:linear-gradient(135deg,#4f6ef7,#7c3aed) !important; 1081 color:#ffffff !important;font-size:14px !important;font-weight:600 !important; 1082 } 1083 .akch-guest-enter:disabled{opacity:.6 !important;cursor:default !important;} 1084 .akch-guest-gate-err{margin:8px 0 0 !important;font-size:13px !important;color:#d63638 !important;} 1085 1086 /* When the gate is open, prevent the page below from scrolling */ 1087 body.akch-gate-open{overflow:hidden !important;} 1088 1089 /* Shortcode container — keep relative positioning for other in-widget overlays */ 1090 #akch-app{position:relative;} -
akukita-chat-with-ai-translation/trunk/assets/js/chat.js
r3525122 r3558865 18 18 userId: parseInt(akchData.userId), 19 19 userName: akchData.userName, 20 // IDs of messages this client has just sent. Used to ignore them when 21 // the poll returns them before the optimistic temp element has been 22 // updated. Cleared on room change. 23 ownIds: new Set(), 20 24 }; 21 25 … … 37 41 headers: this._headers(), 38 42 credentials: 'same-origin', 39 }).then(r => { 40 if (!r.ok) throw new Error('HTTP ' + r.status); 43 }).then(async r => { 44 if (!r.ok) { 45 const err = await r.json().catch(() => ({})); 46 const e = new Error(err.message || ('HTTP ' + r.status)); 47 e.code = err.code || ('http_' + r.status); 48 e.status = r.status; 49 throw e; 50 } 41 51 return r.json(); 42 52 }); … … 48 58 body: JSON.stringify(body), 49 59 credentials: 'same-origin', 50 }).then(r => { 51 if (!r.ok) throw new Error('HTTP ' + r.status); 60 }).then(async r => { 61 if (!r.ok) { 62 const err = await r.json().catch(() => ({})); 63 const e = new Error(err.message || ('HTTP ' + r.status)); 64 e.code = err.code || ('http_' + r.status); 65 e.status = r.status; 66 throw e; 67 } 52 68 return r.json(); 53 69 }); … … 58 74 headers: this._headers(), 59 75 credentials: 'same-origin', 60 }).then(r => { 61 if (!r.ok) throw new Error('HTTP ' + r.status); 76 }).then(async r => { 77 if (!r.ok) { 78 const err = await r.json().catch(() => ({})); 79 const e = new Error(err.message || ('HTTP ' + r.status)); 80 e.code = err.code || ('http_' + r.status); 81 e.status = r.status; 82 throw e; 83 } 62 84 return r.json(); 63 85 }); … … 131 153 ${senderEl} 132 154 <span class="akch-message-time">${time}</span> 133 <span class="akch-message-lang" style="font-size:10px;color:#6b7280;font-family:monospace">${ msg.original_lang || ''}</span>155 <span class="akch-message-lang" style="font-size:10px;color:#6b7280;font-family:monospace">${escapeHTML(msg.original_lang || '')}</span> 134 156 </div> 135 157 <div class="akch-bubble">${displayText}</div> … … 154 176 155 177 messages.forEach(msg => { 178 // Skip if the message is already in the DOM (catches the normal 179 // re-render case) or if it's one we just sent ourselves (catches 180 // the race where the poll returns our own message before the 181 // optimistic temp element has had its data-id swapped). 156 182 if (document.querySelector(`.akch-message[data-id="${msg.id}"]`)) return; 183 if (state.ownIds && state.ownIds.has(String(msg.id))) return; 184 157 185 const html = buildMessageHTML(msg); 158 186 if (prepend) { … … 240 268 state.currentRoomName = roomName; 241 269 state.lastMessageId = 0; 270 state.ownIds.clear(); 242 271 243 272 // Update UI … … 301 330 302 331 try { 332 const hp = document.getElementById('akch-website'); 303 333 const saved = await api.post(`rooms/${state.currentRoomId}/messages`, { 304 334 text: text, 305 335 lang: state.userLang, 336 website: hp ? hp.value : '', 306 337 }); 307 // Replace temp with real 338 339 // Fix for duplicate-on-first-message bug: 340 // 1. Register the real id in ownIds FIRST so any in-flight or 341 // upcoming poll will skip it in appendMessages. 342 // 2. Bump lastMessageId so subsequent polls don't re-fetch it. 343 // 3. THEN swap the temp data-id for the real one. 344 const realId = parseInt(saved.id); 345 state.ownIds.add(String(realId)); 346 if (realId > state.lastMessageId) state.lastMessageId = realId; 347 308 348 const tempEl = document.querySelector(`.akch-message[data-id="${tempId}"]`); 309 349 if (tempEl) tempEl.setAttribute('data-id', saved.id); 310 if (parseInt(saved.id) > state.lastMessageId) state.lastMessageId = parseInt(saved.id);311 350 } catch (e) { 312 351 console.error('[AKCH] Send error:', e); 352 353 // If the server says the guest hasn't entered a name yet, force 354 // the name gate to appear (this also recovers from cached pages 355 // where akchData.needsName was stale). 356 if (e && (e.code === 'no_name' || e.status === 400)) { 357 const tempEl = document.querySelector(`.akch-message[data-id="${tempId}"]`); 358 if (tempEl) tempEl.remove(); 359 input.value = text; // restore the text the user typed 360 if (akchData) akchData.needsName = true; 361 if (typeof showGuestGate === 'function') { 362 showGuestGate(); 363 } 364 state.sending = false; 365 btn.disabled = false; 366 return; 367 } 368 369 // Other errors: keep the optimistic bubble dimmed and tooltip the message. 313 370 const tempEl = document.querySelector(`.akch-message[data-id="${tempId}"]`); 314 371 if (tempEl) { 315 372 tempEl.querySelector('.akch-bubble').style.opacity = '0.5'; 316 tempEl.querySelector('.akch-bubble').title = 'Failed to send'; 373 tempEl.querySelector('.akch-bubble').title = e.message || 'Failed to send'; 374 } 375 376 // Surface a toast so the user actually sees why the message failed. 377 if (typeof showToast === 'function') { 378 showToast(e.message || 'Could not send message.'); 317 379 } 318 380 } finally { … … 541 603 // ── Init ────────────────────────────────────────────────────────────────── 542 604 function init() { 605 // Build banner so we can verify in the browser console which version 606 // of the plugin is actually running (helps when troubleshooting cache). 607 console.log('[AKCH] chat.js running — build 1.2.0+guestfix'); 608 console.log('[AKCH] akchData on init:', { 609 userId: akchData && akchData.userId, 610 userName: akchData && akchData.userName, 611 isGuest: akchData && akchData.isGuest, 612 needsName: akchData && akchData.needsName, 613 }); 614 543 615 // Re-read identity from akchData (set by wp_head inline script) 544 616 state.userId = (akchData && akchData.userId) || state.userId || 0; … … 546 618 state.userLang = (akchData && akchData.userLang) || 'en'; 547 619 620 // ── Robust "needs name" detection ───────────────────────────────────── 621 // Two checks, because akchData.needsName can be stale on cached HTML: 622 // 1) The server told us so via akchData.needsName. 623 // 2) We're a guest (negative or zero userId) and our cookie that 624 // stores the chosen name is empty or missing on the client. 625 const cookieName = readCookie('akch_guest_name'); 626 const isGuestId = !(state.userId > 0); // logged-in users have positive ids 627 const needsName = (akchData && akchData.needsName) 628 || (isGuestId && (!cookieName || cookieName.length < 2)); 629 630 if (needsName) { 631 console.log('[AKCH] Showing guest name gate (server=' + 632 (akchData && akchData.needsName) + ', cookieName="' + cookieName + '")'); 633 showGuestGate(); 634 return; 635 } 636 637 startChat(); 638 } 639 640 // Read a cookie value by name from document.cookie. 641 function readCookie(name) { 642 const parts = document.cookie.split(';'); 643 for (let i = 0; i < parts.length; i++) { 644 const trimmed = parts[i].trim(); 645 if (trimmed.indexOf(name + '=') === 0) { 646 return decodeURIComponent(trimmed.substring(name.length + 1)); 647 } 648 } 649 return ''; 650 } 651 652 function startChat() { 548 653 // Sync the language dropdown to the correct value 549 654 const sel = document.getElementById('akch-user-lang'); … … 580 685 } 581 686 687 // ── Guest name gate ─────────────────────────────────────────────────────── 688 689 /** 690 * Build the guest gate DOM from scratch when the server-rendered markup 691 * is missing (e.g. stripped by a page builder). We attach it to <body> 692 * directly so no ancestor styles can hide it. 693 */ 694 function buildGuestGateFallback() { 695 if (!document.body) return null; 696 697 const gate = document.createElement('div'); 698 gate.id = 'akch-guest-gate'; 699 gate.className = 'akch-guest-gate'; 700 701 const i = (akchData && akchData.i18n) || {}; 702 const welcomeTxt = i.welcome || 'Welcome to the Chat'; 703 const welcomeSubTxt = i.welcomeSub || 'Messages are translated automatically for everyone.'; 704 const yourNameTxt = i.yourName || 'Your name'; 705 const yourLangTxt = i.yourLang || 'Your language'; 706 const enterTxt = i.enterChat || 'Enter Chat'; 707 708 // Minimum viable language list — English as fallback. The real list 709 // is normally in the template; in fallback mode we read it from the 710 // existing #akch-user-lang select if present, else show English only. 711 let langOptions = '<option value="en">English</option>'; 712 const existingLangSel = document.getElementById('akch-user-lang'); 713 if (existingLangSel && existingLangSel.options.length > 0) { 714 langOptions = Array.from(existingLangSel.options) 715 .map(o => `<option value="${escapeHTML(o.value)}">${escapeHTML(o.text)}</option>`) 716 .join(''); 717 } 718 719 gate.innerHTML = ` 720 <div class="akch-guest-gate-card"> 721 <h3>${escapeHTML(welcomeTxt)}</h3> 722 <p>${escapeHTML(welcomeSubTxt)}</p> 723 <label for="akch-guest-name">${escapeHTML(yourNameTxt)}</label> 724 <input type="text" id="akch-guest-name" maxlength="40" autocomplete="off" placeholder="${escapeHTML(yourNameTxt)}" /> 725 <label for="akch-guest-lang">${escapeHTML(yourLangTxt)}</label> 726 <select id="akch-guest-lang">${langOptions}</select> 727 <p class="akch-guest-gate-err" id="akch-guest-gate-err" style="display:none;color:#d63638;font-size:13px;"></p> 728 <button type="button" id="akch-guest-enter" class="akch-guest-enter">${escapeHTML(enterTxt)}</button> 729 </div>`; 730 731 document.body.appendChild(gate); 732 return gate; 733 } 734 735 function showGuestGate() { 736 let gate = document.getElementById('akch-guest-gate'); 737 738 // If the gate is missing (Divi or other page builders sometimes strip 739 // markup from inside shortcodes when they post-process the content), 740 // build it from scratch and append it to body so we can still ask for 741 // the visitor's name. 742 if (!gate) { 743 console.warn('[AKCH] #akch-guest-gate not found in DOM. Building it dynamically.'); 744 gate = buildGuestGateFallback(); 745 if (!gate) { 746 console.error('[AKCH] Could not build guest gate fallback. Bailing.'); 747 startChat(); 748 return; 749 } 750 } else { 751 console.log('[AKCH] #akch-guest-gate found in DOM. Promoting to fixed overlay.'); 752 } 753 754 // Some themes (notably Divi) wrap shortcode output in containers that 755 // clip overflow, apply transforms, or override z-index — which made 756 // the absolute-inside-#akch-app overlay invisible. To bypass all of 757 // that, we promote the gate to a fixed full-screen modal anchored to 758 // <body>. !important defends against theme stylesheets that load 759 // after our own. 760 if (gate.parentNode !== document.body) { 761 document.body.appendChild(gate); 762 } 763 gate.style.cssText = [ 764 'position: fixed !important', 765 'top: 0 !important', 766 'left: 0 !important', 767 'right: 0 !important', 768 'bottom: 0 !important', 769 'width: 100% !important', 770 'height: 100% !important', 771 'z-index: 2147483647 !important', 772 'display: flex !important', 773 'align-items: center !important', 774 'justify-content: center !important', 775 'background: rgba(0,0,0,0.55) !important', 776 'backdrop-filter: blur(3px)', 777 ].join(';'); 778 779 // Keep the card itself styled (in case theme rules also leak in). 780 const card = gate.querySelector('.akch-guest-gate-card'); 781 if (card) { 782 card.style.cssText = [ 783 'background: #fff', 784 'border-radius: 14px', 785 'padding: 26px 28px', 786 'max-width: 360px', 787 'width: 90%', 788 'box-shadow: 0 12px 40px rgba(0,0,0,0.3)', 789 'text-align: left', 790 'color: #1a1a1a', 791 'position: relative', 792 ].join(';'); 793 } 794 795 const nameInput = gate.querySelector('#akch-guest-name'); 796 const langSel = gate.querySelector('#akch-guest-lang'); 797 const enterBtn = gate.querySelector('#akch-guest-enter'); 798 const errEl = gate.querySelector('#akch-guest-gate-err'); 799 800 console.log('[AKCH] Guest gate elements:', { 801 nameInput: !!nameInput, 802 langSel: !!langSel, 803 enterBtn: !!enterBtn, 804 errEl: !!errEl, 805 }); 806 807 if (nameInput) { 808 try { nameInput.focus(); } catch (_) {} 809 } 810 811 function submit() { 812 console.log('[AKCH] Guest gate submit() called'); 813 const name = (nameInput && nameInput.value || '').trim(); 814 const lang = (langSel && langSel.value) || state.userLang || 'en'; 815 if (name.length < 2) { 816 if (errEl) { errEl.textContent = (akchData.i18n && akchData.i18n.nameTooShort) || 'Name must be at least 2 characters.'; errEl.style.display = 'block'; } 817 return; 818 } 819 if (enterBtn) enterBtn.disabled = true; 820 821 const body = new URLSearchParams(); 822 body.append('action', 'akch_set_guest_name'); 823 body.append('nonce', (akchData && akchData.nonce) || ''); 824 body.append('name', name); 825 body.append('lang', lang); 826 827 fetch((akchData && akchData.ajaxUrl) || '', { 828 method: 'POST', 829 credentials: 'same-origin', 830 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 831 body: body.toString(), 832 }).then(r => r.json()).then(function (res) { 833 console.log('[AKCH] set_guest_name response:', res); 834 if (res && res.success) { 835 state.userName = res.data.name; 836 state.userLang = res.data.lang; 837 state.userId = parseInt(res.data.userId) || state.userId; 838 akchData.userName = res.data.name; 839 akchData.userLang = res.data.lang; 840 akchData.userId = res.data.userId; 841 akchData.needsName = false; 842 gate.style.display = 'none'; 843 if (gate.parentNode) gate.parentNode.removeChild(gate); 844 startChat(); 845 } else { 846 if (enterBtn) enterBtn.disabled = false; 847 if (errEl) { errEl.textContent = (res && res.data && res.data.message) || 'Error'; errEl.style.display = 'block'; } 848 } 849 }).catch(function (err) { 850 console.error('[AKCH] set_guest_name network error:', err); 851 if (enterBtn) enterBtn.disabled = false; 852 if (errEl) { errEl.textContent = (akchData.i18n && akchData.i18n.networkError) || 'Network error.'; errEl.style.display = 'block'; } 853 }); 854 } 855 856 // Use event delegation on the gate itself so the click handler works 857 // even if the inner button is recreated, restyled, or wrapped by other 858 // scripts after the gate is built. 859 gate.addEventListener('click', function (ev) { 860 const target = ev.target; 861 if (target && (target.id === 'akch-guest-enter' || target.closest('#akch-guest-enter'))) { 862 ev.preventDefault(); 863 submit(); 864 } 865 }); 866 // Direct listener too, as belt-and-braces in case delegation is blocked. 867 if (enterBtn) { 868 enterBtn.addEventListener('click', function (ev) { 869 ev.preventDefault(); 870 submit(); 871 }); 872 console.log('[AKCH] Click handler attached to #akch-guest-enter'); 873 } else { 874 console.error('[AKCH] #akch-guest-enter NOT found — click handler not attached'); 875 } 876 if (nameInput) { 877 nameInput.addEventListener('keydown', function (e) { 878 if (e.key === 'Enter') { e.preventDefault(); submit(); } 879 }); 880 } 881 } 882 582 883 // Expose globally 583 884 window.akchChatInit = init; -
akukita-chat-with-ai-translation/trunk/assets/js/guest.js
r3525122 r3558865 1 1 /** 2 2 * Akukita Chat with AI Translation — guest.js 1.5.0 3 * 4 * The guest system has been removed. This file: 5 * 1. Removes any old guest overlay injected by cached old versions of this script 6 * 2. Boots the chat for logged-in users (calls akchChatInit when ready) 7 * 8 * Non-logged-in users see register-modal.php (server-rendered, self-contained). 3 * 4 * Two responsibilities: 5 * 1. Remove any old "guest overlay" injected by cached previous versions 6 * of this script (early 1.x betas had a different guest UI). 7 * 2. Boot chat.js whenever the chat widget is rendered on the page. 8 * This applies to BOTH logged-in users AND guests when the site is 9 * running in 'guest' access mode. chat.js handles the rest (showing 10 * the guest name gate via akchData.needsName, loading rooms, polling). 11 * 12 * In 'account' access mode, non-logged-in visitors get register-modal.php 13 * instead and #akch-app is not rendered, so this code is a no-op for them. 9 14 */ 10 15 (function() { … … 26 31 } 27 32 28 function isLoggedIn() { 29 return window.akchData && akchData.userId && parseInt(akchData.userId) > 0; 33 /** 34 * Should we start the chat on this page? 35 * 36 * We boot whenever: 37 * - The chat widget container exists in the DOM (#akch-app), AND 38 * - The inline akchData payload is present (set by wp_head). 39 * 40 * This correctly covers both logged-in users (positive userId) and 41 * guests (negative userId issued by AKCH_Guest::guest_id_from_token). 42 * The previous check rejected guests because parseInt(userId) > 0 is 43 * false for them — that bug stopped chat.js from initialising for 44 * guest-mode visitors. 45 */ 46 function shouldBoot() { 47 return !!document.getElementById('akch-app') 48 && typeof window.akchData !== 'undefined'; 30 49 } 31 50 … … 34 53 removeOldOverlay(); 35 54 36 // For logged-in users: boot chat (chat.js exposes akchChatInit) 37 if (isLoggedIn()) { 38 // chat.js may not be parsed yet if it loads after guest.js 39 // Wait for it with a short poll 40 if (typeof window.akchChatInit === 'function') { 41 bootChat(); 42 } else { 43 var attempts = 0; 44 var poll = setInterval(function() { 45 attempts++; 46 if (typeof window.akchChatInit === 'function') { 47 clearInterval(poll); 48 bootChat(); 49 } else if (attempts > 50) { 50 clearInterval(poll); // Give up after 5s 51 } 52 }, 100); 53 } 55 if (!shouldBoot()) { 56 // No chat on this page (or running in account mode for a 57 // non-logged-in visitor — they get the registration modal). 58 return; 54 59 } 55 // Non-logged-in users: nothing to do — register-modal.php handles everything 60 61 // chat.js may not be parsed yet if it loads after guest.js; 62 // poll briefly until akchChatInit becomes available, then call it. 63 if (typeof window.akchChatInit === 'function') { 64 bootChat(); 65 } else { 66 var attempts = 0; 67 var poll = setInterval(function() { 68 attempts++; 69 if (typeof window.akchChatInit === 'function') { 70 clearInterval(poll); 71 bootChat(); 72 } else if (attempts > 50) { 73 clearInterval(poll); // Give up after 5s 74 } 75 }, 100); 76 } 56 77 } 57 78 … … 78 99 'use strict'; 79 100 80 // Only run when the registration modal is present in the DOM.81 if ( ! document.getElementById( 'akch- register-modal' ) ) {101 // Only run when the registration/login modal is present in the DOM. 102 if ( ! document.getElementById( 'akch-auth-modal' ) ) { 82 103 return; 83 104 } -
akukita-chat-with-ai-translation/trunk/includes/class-admin.php
r3525122 r3558865 69 69 'akch_messages_per_load' => 'absint', 70 70 'akch_history_days' => 'absint', 71 'akch_access_mode' => 'sanitize_text_field', 71 72 ]; 72 73 foreach ( $settings as $option => $sanitize ) { … … 98 99 if ( isset( $_POST['akch_save_settings'] ) ) { 99 100 check_admin_referer( 'akch_settings_save' ); 100 foreach ( [ 'akch_translation_provider', 'akch_openai_key', 'akch_deepl_key', 'akch_mymemory_key', 'akch_default_language', 'akch_messages_per_load', 'akch_history_days' ] as $key ) {101 foreach ( [ 'akch_translation_provider', 'akch_openai_key', 'akch_deepl_key', 'akch_mymemory_key', 'akch_default_language', 'akch_messages_per_load', 'akch_history_days', 'akch_access_mode' ] as $key ) { 101 102 if ( isset( $_POST[ $key ] ) ) { 102 103 update_option( $key, sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) ); -
akukita-chat-with-ai-translation/trunk/includes/class-database.php
r3525122 r3558865 77 77 add_option( 'akch_translation_provider', 'mymemory' ); 78 78 add_option( 'akch_default_language', 'en' ); 79 // Default 'account': only logged-in users can chat. 80 // Admins can switch to 'guest' (anyone) or 'registered' from Settings. 81 // This preserves the pre-1.2.0 behaviour for users upgrading. 82 add_option( 'akch_access_mode', 'account' ); 79 83 add_option( 'akch_messages_per_load', 50 ); 80 84 add_option( 'akch_history_days', 30 ); … … 326 330 } 327 331 332 /** 333 * Soft-delete a message (sets is_deleted = 1). Used for moderation. 334 * 335 * @param int $message_id Message ID. 336 * @return void 337 */ 338 public static function soft_delete_message( $message_id ) { 339 global $wpdb; 340 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 341 $wpdb->update( 342 $wpdb->prefix . 'akch_chat_messages', 343 [ 'is_deleted' => 1 ], 344 [ 'id' => (int) $message_id ] 345 ); 346 } 347 328 348 // -- Participants ---------------------------------------------------------- 329 349 -
akukita-chat-with-ai-translation/trunk/includes/class-guest.php
r3525122 r3558865 9 9 */ 10 10 11 if ( ! defined( 'ABSPATH' ) ) exit; 11 if ( ! defined( 'ABSPATH' ) ) { 12 exit; 13 } 12 14 13 15 /** 14 * User Registration Handler16 * Identity / access handler. 15 17 * 16 * Replaces the old guest system. 17 * - Logged-in users: access chat directly 18 * - Non-logged-in users: see a registration/login modal 19 * - Handles WP user registration via AJAX 20 * - Handles language preference via akch_lang cookie 18 * Access modes (option akch_access_mode): 19 * • 'guest' — Anyone can chat after entering a display name (default). 20 * • 'registered' — Only logged-in WordPress users can chat. 21 * • 'account' — Visitors must create/log in to a WP account (legacy modal). 22 * 23 * Guests are not WordPress users. Each guest gets a random token stored in the 24 * akch_guest_id cookie. A stable negative integer derived from that token acts 25 * as their user_id in the chat tables, keeping guests from colliding with each 26 * other or with real (positive) WP user IDs. 21 27 */ 22 28 class AKCH_Guest { 23 29 24 public static function init() { 25 // WP registration via AJAX (no login required) 26 add_action( 'wp_ajax_nopriv_akch_register_user', [ __CLASS__, 'ajax_register_user' ] ); 27 add_action( 'wp_ajax_nopriv_akch_get_i18n', [ __CLASS__, 'ajax_get_i18n' ] ); 28 add_action( 'wp_ajax_akch_get_i18n', [ __CLASS__, 'ajax_get_i18n' ] ); 29 } 30 31 // -- Identity -- only WP users now ----------------------------------------- 32 33 public static function is_wp_user() { return is_user_logged_in(); } 34 public static function is_guest() { return false; } // guests removed 35 public static function can_chat() { return is_user_logged_in(); } 36 37 public static function current_id() { 38 return is_user_logged_in() ? get_current_user_id() : 0; 39 } 40 41 public static function current_name() { 42 return is_user_logged_in() ? wp_get_current_user()->display_name : ''; 43 } 44 45 public static function current_lang() { 46 if ( is_user_logged_in() ) { 47 return get_user_meta( get_current_user_id(), 'akch_preferred_language', true ) ?: 'en'; 48 } 49 // For non-logged-in users, read language cookie (set by registration modal) 50 return sanitize_text_field( wp_unslash( $_COOKIE['akch_lang'] ?? 'en' ) ); 51 } 52 53 public static function update_language( $lang ) { 54 if ( is_user_logged_in() ) { 55 update_user_meta( get_current_user_id(), 'akch_preferred_language', $lang ); 56 } 57 // Also save in cookie for persistence 58 if ( ! headers_sent() ) { 59 setcookie( 'akch_lang', $lang, [ 60 'expires' => time() + ( 30 * DAY_IN_SECONDS ), 61 'path' => COOKIEPATH ?: '/', 62 'secure' => is_ssl(), 63 'httponly' => false, 64 'samesite' => 'Lax', 65 ] ); 66 } 67 $_COOKIE['akch_lang'] = $lang; 68 } 69 70 // -- WP User Registration -------------------------------------------------- 71 72 public static function ajax_register_user() { 73 // Verify registration nonce (generated by the registration modal template). 74 check_ajax_referer( 'akch_register_nonce', 'nonce' ); 75 76 // Check if registration is enabled in WP settings 77 if ( ! get_option( 'users_can_register' ) ) { 78 wp_send_json_error( [ 'message' => __( 'User registration is currently disabled. Please contact the administrator.', 'akukita-chat-with-ai-translation' ) ] ); 79 return; 80 } 81 82 // Rate limit 83 $ip = md5( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? 'x' ) ) ); 84 $tk = 'akch_reg_' . $ip; 85 $count = (int) get_transient( $tk ); 86 if ( $count > 5 ) { 87 wp_send_json_error( [ 'message' => __( 'Too many attempts. Please wait a few minutes.', 'akukita-chat-with-ai-translation' ) ] ); 88 return; 89 } 90 set_transient( $tk, $count + 1, 300 ); // 5 attempts per 5 minutes 91 92 $username = sanitize_user( wp_unslash( isset( $_POST['username'] ) ? $_POST['username'] : '' ) ); 93 $email = sanitize_email( wp_unslash( isset( $_POST['email'] ) ? $_POST['email'] : '' ) ); 94 $lang = sanitize_text_field( wp_unslash( isset( $_POST['lang'] ) ? $_POST['lang'] : 'en' ) ); 95 96 // Validate 97 if ( empty( $username ) ) { 98 wp_send_json_error( [ 'field' => 'username', 'message' => __( 'Username is required.', 'akukita-chat-with-ai-translation' ) ] ); 99 return; 100 } 101 if ( ! validate_username( $username ) ) { 102 wp_send_json_error( [ 'field' => 'username', 'message' => __( 'Username contains invalid characters.', 'akukita-chat-with-ai-translation' ) ] ); 103 return; 104 } 105 if ( username_exists( $username ) ) { 106 wp_send_json_error( [ 'field' => 'username', 'message' => __( 'This username is already taken.', 'akukita-chat-with-ai-translation' ) ] ); 107 return; 108 } 109 if ( empty( $email ) || ! is_email( $email ) ) { 110 wp_send_json_error( [ 'field' => 'email', 'message' => __( 'Please enter a valid email address.', 'akukita-chat-with-ai-translation' ) ] ); 111 return; 112 } 113 if ( email_exists( $email ) ) { 114 wp_send_json_error( [ 'field' => 'email', 'message' => __( 'This email is already registered. Try logging in.', 'akukita-chat-with-ai-translation' ) ] ); 115 return; 116 } 117 118 // Create user -- WP generates a random password and sends it by email 119 $user_id = wp_create_user( $username, wp_generate_password( 12 ), $email ); 120 121 if ( is_wp_error( $user_id ) ) { 122 wp_send_json_error( [ 'message' => $user_id->get_error_message() ] ); 123 return; 124 } 125 126 // Save language preference 127 $supported = array_keys( AKCH_AI_Translation::supported_languages() ); 128 if ( in_array( $lang, $supported, true ) ) { 129 update_user_meta( $user_id, 'akch_preferred_language', $lang ); 130 } 131 132 // Send password email 133 wp_new_user_notification( $user_id, null, 'user' ); 134 135 // Auto-login the new user 136 wp_set_current_user( $user_id ); 137 wp_set_auth_cookie( $user_id, true ); 138 139 $user = get_user_by( 'ID', $user_id ); 140 141 wp_send_json_success( [ 142 'message' => __( 'Account created! You are now logged in.', 'akukita-chat-with-ai-translation' ), 143 'userId' => $user_id, 144 'userName' => $user->display_name, 145 'lang' => $lang, 146 'restNonce' => wp_create_nonce( 'wp_rest' ), 147 'nonce' => wp_create_nonce( 'akch_nonce' ), 148 ] ); 149 } 150 151 public static function ajax_get_i18n() { 152 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public endpoint returning only localisation strings, no state change. 153 $lang = sanitize_text_field( wp_unslash( $_POST['lang'] ?? 'en' ) ); 154 wp_send_json_success( Akukita_Chat::get_i18n( $lang ) ); 155 } 156 157 // Kept for backwards compatibility 158 public static function start_session() {} 30 const COOKIE_TOKEN = 'akch_guest_id'; 31 const COOKIE_NAME = 'akch_guest_name'; 32 33 /** 34 * Register hooks. 35 * 36 * @return void 37 */ 38 public static function init() { 39 add_action( 'init', array( __CLASS__, 'ensure_guest_cookie' ) ); 40 add_action( 'wp_ajax_nopriv_akch_register_user', array( __CLASS__, 'ajax_register_user' ) ); 41 add_action( 'wp_ajax_nopriv_akch_get_i18n', array( __CLASS__, 'ajax_get_i18n' ) ); 42 add_action( 'wp_ajax_akch_get_i18n', array( __CLASS__, 'ajax_get_i18n' ) ); 43 add_action( 'wp_ajax_nopriv_akch_set_guest_name', array( __CLASS__, 'ajax_set_guest_name' ) ); 44 } 45 46 /** 47 * AJAX: store a guest's chosen display name (and language) in cookies. 48 * 49 * @return void 50 */ 51 public static function ajax_set_guest_name() { 52 check_ajax_referer( 'akch_nonce', 'nonce' ); 53 54 if ( ! self::guests_allowed() || is_user_logged_in() ) { 55 wp_send_json_error( array( 'message' => __( 'Not available.', 'akukita-chat-with-ai-translation' ) ), 403 ); 56 } 57 58 $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; 59 $name = trim( $name ); 60 if ( mb_strlen( $name ) < 2 ) { 61 wp_send_json_error( array( 'message' => __( 'Name must be at least 2 characters.', 'akukita-chat-with-ai-translation' ) ) ); 62 } 63 if ( mb_strlen( $name ) > 40 ) { 64 $name = mb_substr( $name, 0, 40 ); 65 } 66 67 $lang = isset( $_POST['lang'] ) ? sanitize_text_field( wp_unslash( $_POST['lang'] ) ) : ''; 68 $supported = array_keys( AKCH_AI_Translation::supported_languages() ); 69 if ( ! in_array( $lang, $supported, true ) ) { 70 $lang = get_option( 'akch_default_language', 'en' ); 71 } 72 73 if ( ! headers_sent() ) { 74 setcookie( 75 self::COOKIE_NAME, 76 $name, 77 array( 78 'expires' => time() + ( 180 * DAY_IN_SECONDS ), 79 'path' => COOKIEPATH ? COOKIEPATH : '/', 80 'secure' => is_ssl(), 81 'httponly' => false, 82 'samesite' => 'Lax', 83 ) 84 ); 85 } 86 $_COOKIE[ self::COOKIE_NAME ] = $name; 87 self::update_language( $lang ); 88 89 wp_send_json_success( 90 array( 91 'name' => $name, 92 'lang' => $lang, 93 'userId' => self::current_id(), 94 ) 95 ); 96 } 97 98 // -- Access mode ----------------------------------------------------------- 99 100 /** 101 * Current access mode. 102 * 103 * @return string One of 'guest', 'registered', 'account'. 104 */ 105 public static function access_mode() { 106 $mode = get_option( 'akch_access_mode', 'account' ); 107 if ( ! in_array( $mode, array( 'guest', 'registered', 'account' ), true ) ) { 108 $mode = 'account'; 109 } 110 return $mode; 111 } 112 113 /** 114 * Whether non-logged-in guests are allowed to chat. 115 * 116 * @return bool 117 */ 118 public static function guests_allowed() { 119 return 'guest' === self::access_mode(); 120 } 121 122 /** 123 * Whether the current visitor may participate in chat. 124 * 125 * @return bool 126 */ 127 public static function can_chat() { 128 if ( is_user_logged_in() ) { 129 return true; 130 } 131 return self::guests_allowed(); 132 } 133 134 // -- Guest cookie / token -------------------------------------------------- 135 136 /** 137 * Create the guest token cookie on first visit (frontend, guests only). 138 * 139 * @return void 140 */ 141 public static function ensure_guest_cookie() { 142 if ( is_admin() || is_user_logged_in() ) { 143 return; 144 } 145 if ( ! self::guests_allowed() ) { 146 return; 147 } 148 if ( ! empty( $_COOKIE[ self::COOKIE_TOKEN ] ) ) { 149 return; 150 } 151 if ( headers_sent() ) { 152 return; 153 } 154 155 $token = wp_generate_password( 20, false ); 156 setcookie( 157 self::COOKIE_TOKEN, 158 $token, 159 array( 160 'expires' => time() + ( 180 * DAY_IN_SECONDS ), 161 'path' => COOKIEPATH ? COOKIEPATH : '/', 162 'secure' => is_ssl(), 163 'httponly' => false, 164 'samesite' => 'Lax', 165 ) 166 ); 167 $_COOKIE[ self::COOKIE_TOKEN ] = $token; 168 } 169 170 /** 171 * Read the current guest token, if any. 172 * 173 * @return string 174 */ 175 public static function guest_token() { 176 return isset( $_COOKIE[ self::COOKIE_TOKEN ] ) 177 ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_TOKEN ] ) ) 178 : ''; 179 } 180 181 /** 182 * Derive a stable negative integer ID from a guest token. 183 * 184 * @param string $token Guest token. 185 * @return int Negative integer, or 0 when no token. 186 */ 187 public static function guest_id_from_token( $token ) { 188 if ( '' === $token ) { 189 return 0; 190 } 191 return - (int) sprintf( '%u', crc32( $token ) ); 192 } 193 194 // -- Identity -------------------------------------------------------------- 195 196 /** 197 * Whether the current participant is a guest (not a WP user). 198 * 199 * @return bool 200 */ 201 public static function is_guest() { 202 return ! is_user_logged_in() && self::guests_allowed() && '' !== self::guest_token(); 203 } 204 205 /** 206 * Current participant numeric ID (positive WP id, or negative guest id). 207 * 208 * @return int 209 */ 210 public static function current_id() { 211 if ( is_user_logged_in() ) { 212 return get_current_user_id(); 213 } 214 return self::guest_id_from_token( self::guest_token() ); 215 } 216 217 /** 218 * Current participant display name. 219 * 220 * @return string 221 */ 222 public static function current_name() { 223 if ( is_user_logged_in() ) { 224 return wp_get_current_user()->display_name; 225 } 226 return isset( $_COOKIE[ self::COOKIE_NAME ] ) 227 ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_NAME ] ) ) 228 : ''; 229 } 230 231 /** 232 * Current participant preferred language. 233 * 234 * @return string 235 */ 236 public static function current_lang() { 237 if ( is_user_logged_in() ) { 238 $lang = get_user_meta( get_current_user_id(), 'akch_preferred_language', true ); 239 return $lang ? $lang : get_option( 'akch_default_language', 'en' ); 240 } 241 if ( isset( $_COOKIE['akch_lang'] ) ) { 242 return sanitize_text_field( wp_unslash( $_COOKIE['akch_lang'] ) ); 243 } 244 return get_option( 'akch_default_language', 'en' ); 245 } 246 247 /** 248 * Persist the participant's language (user meta for WP users, cookie always). 249 * 250 * @param string $lang Language code. 251 * @return void 252 */ 253 public static function update_language( $lang ) { 254 if ( is_user_logged_in() ) { 255 update_user_meta( get_current_user_id(), 'akch_preferred_language', $lang ); 256 } 257 if ( ! headers_sent() ) { 258 setcookie( 259 'akch_lang', 260 $lang, 261 array( 262 'expires' => time() + ( 30 * DAY_IN_SECONDS ), 263 'path' => COOKIEPATH ? COOKIEPATH : '/', 264 'secure' => is_ssl(), 265 'httponly' => false, 266 'samesite' => 'Lax', 267 ) 268 ); 269 } 270 $_COOKIE['akch_lang'] = $lang; 271 } 272 273 // -- WP User Registration (only used in 'account' mode) -------------------- 274 275 /** 276 * AJAX: register a new WP user (legacy 'account' mode). 277 * 278 * @return void 279 */ 280 public static function ajax_register_user() { 281 check_ajax_referer( 'akch_register_nonce', 'nonce' ); 282 283 if ( ! get_option( 'users_can_register' ) ) { 284 wp_send_json_error( array( 'message' => __( 'User registration is currently disabled. Please contact the administrator.', 'akukita-chat-with-ai-translation' ) ) ); 285 return; 286 } 287 288 $ip = md5( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? 'x' ) ) ); 289 $tk = 'akch_reg_' . $ip; 290 $count = (int) get_transient( $tk ); 291 if ( $count > 5 ) { 292 wp_send_json_error( array( 'message' => __( 'Too many attempts. Please wait a few minutes.', 'akukita-chat-with-ai-translation' ) ) ); 293 return; 294 } 295 set_transient( $tk, $count + 1, 300 ); 296 297 $username = sanitize_user( wp_unslash( isset( $_POST['username'] ) ? $_POST['username'] : '' ) ); 298 $email = sanitize_email( wp_unslash( isset( $_POST['email'] ) ? $_POST['email'] : '' ) ); 299 $lang = sanitize_text_field( wp_unslash( isset( $_POST['lang'] ) ? $_POST['lang'] : 'en' ) ); 300 301 if ( empty( $username ) ) { 302 wp_send_json_error( array( 'field' => 'username', 'message' => __( 'Username is required.', 'akukita-chat-with-ai-translation' ) ) ); 303 return; 304 } 305 if ( ! validate_username( $username ) ) { 306 wp_send_json_error( array( 'field' => 'username', 'message' => __( 'Username contains invalid characters.', 'akukita-chat-with-ai-translation' ) ) ); 307 return; 308 } 309 if ( username_exists( $username ) ) { 310 wp_send_json_error( array( 'field' => 'username', 'message' => __( 'This username is already taken.', 'akukita-chat-with-ai-translation' ) ) ); 311 return; 312 } 313 if ( empty( $email ) || ! is_email( $email ) ) { 314 wp_send_json_error( array( 'field' => 'email', 'message' => __( 'Please enter a valid email address.', 'akukita-chat-with-ai-translation' ) ) ); 315 return; 316 } 317 if ( email_exists( $email ) ) { 318 wp_send_json_error( array( 'field' => 'email', 'message' => __( 'This email is already registered. Try logging in.', 'akukita-chat-with-ai-translation' ) ) ); 319 return; 320 } 321 322 $user_id = wp_create_user( $username, wp_generate_password( 12 ), $email ); 323 324 if ( is_wp_error( $user_id ) ) { 325 wp_send_json_error( array( 'message' => $user_id->get_error_message() ) ); 326 return; 327 } 328 329 $supported = array_keys( AKCH_AI_Translation::supported_languages() ); 330 if ( in_array( $lang, $supported, true ) ) { 331 update_user_meta( $user_id, 'akch_preferred_language', $lang ); 332 } 333 334 wp_new_user_notification( $user_id, null, 'user' ); 335 336 wp_set_current_user( $user_id ); 337 wp_set_auth_cookie( $user_id, true ); 338 339 $user = get_user_by( 'ID', $user_id ); 340 341 wp_send_json_success( array( 342 'message' => __( 'Account created! You are now logged in.', 'akukita-chat-with-ai-translation' ), 343 'userId' => $user_id, 344 'userName' => $user->display_name, 345 'lang' => $lang, 346 'restNonce' => wp_create_nonce( 'wp_rest' ), 347 'nonce' => wp_create_nonce( 'akch_nonce' ), 348 ) ); 349 } 350 351 /** 352 * AJAX: return localisation strings for a language. 353 * 354 * @return void 355 */ 356 public static function ajax_get_i18n() { 357 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public endpoint returning only localisation strings, no state change. 358 $lang = sanitize_text_field( wp_unslash( $_POST['lang'] ?? 'en' ) ); 359 wp_send_json_success( Akukita_Chat::get_i18n( $lang ) ); 360 } 361 362 /** 363 * Backwards-compatibility no-op. 364 * 365 * @return void 366 */ 367 public static function start_session() {} 368 369 /** 370 * Backwards-compatibility helper. 371 * 372 * @return bool 373 */ 374 public static function is_wp_user() { 375 return is_user_logged_in(); 376 } 159 377 } -
akukita-chat-with-ai-translation/trunk/includes/class-rest-api.php
r3525122 r3558865 39 39 ] ); 40 40 41 register_rest_route( $ns, '/messages/(?P<id>\d+)', [ 42 [ 'methods' => 'DELETE', 'callback' => [ __CLASS__, 'delete_message' ], 'permission_callback' => [ __CLASS__, 'require_admin' ] ], 43 ] ); 44 41 45 register_rest_route( $ns, '/rooms/(?P<id>\d+)/join', [ 42 46 'methods' => 'POST', … … 72 76 // -- Permission callbacks -------------------------------------------------- 73 77 74 /** Requires a logged-in WP user */78 /** Requires a logged-in WP user, or a guest when guest mode is enabled. */ 75 79 public static function require_chatter() { 76 if ( is_user_logged_in() ) return true; 80 if ( AKCH_Guest::can_chat() ) { 81 return true; 82 } 77 83 return new WP_Error( 'not_authorized', __( 'You must be logged in to chat.', 'akukita-chat-with-ai-translation' ), [ 'status' => 401 ] ); 78 84 } … … 101 107 */ 102 108 public static function can_access_room( WP_REST_Request $request ) { 103 if ( ! is_user_logged_in() ) {109 if ( ! AKCH_Guest::can_chat() ) { 104 110 return new WP_Error( 'not_authorized', __( 'You must be logged in to chat.', 'akukita-chat-with-ai-translation' ), [ 'status' => 401 ] ); 105 111 } … … 166 172 } 167 173 174 /** 175 * Soft-delete a single message (moderation). Admin only. 176 * 177 * @param WP_REST_Request $req Request. 178 * @return WP_REST_Response 179 */ 180 public static function delete_message( $req ) { 181 AKCH_Database::soft_delete_message( (int) $req['id'] ); 182 return rest_ensure_response( [ 'deleted' => true ] ); 183 } 184 168 185 // -- Message endpoints ----------------------------------------------------- 169 186 … … 173 190 $before_id = (int) ( $req->get_param( 'before_id' ) ?: 0 ); 174 191 $after_id = (int) ( $req->get_param( 'after_id' ) ?: 0 ); 175 $user_lang = get_user_meta( get_current_user_id(), 'akch_preferred_language', true) ?: 'en';192 $user_lang = AKCH_Guest::current_lang() ?: 'en'; 176 193 177 194 $messages = AKCH_Database::get_messages( $room_id, $limit, $before_id, $after_id ); … … 209 226 $lang = sanitize_text_field( $req->get_param( 'lang' ) ?: '' ); 210 227 228 // -- Anti-spam: honeypot. A hidden field that humans never fill in. 229 $honeypot = (string) $req->get_param( 'website' ); 230 if ( '' !== trim( $honeypot ) ) { 231 // Pretend success so bots don't learn they were caught. 232 return rest_ensure_response( array( 'id' => 0, 'room_id' => $room_id, 'display_text' => $text, 'sent_at' => current_time( 'mysql' ) ) ); 233 } 234 211 235 if ( empty( $text ) ) { 212 236 return new WP_Error( 'empty_message', __( 'Message cannot be empty.', 'akukita-chat-with-ai-translation' ), [ 'status' => 400 ] ); 213 237 } 214 238 239 // -- Anti-spam: maximum length. 240 $max_len = (int) apply_filters( 'akch_max_message_length', 2000 ); 241 if ( function_exists( 'mb_strlen' ) ? mb_strlen( $text ) > $max_len : strlen( $text ) > $max_len ) { 242 return new WP_Error( 'too_long', __( 'Message is too long.', 'akukita-chat-with-ai-translation' ), [ 'status' => 400 ] ); 243 } 244 245 $user_id = AKCH_Guest::current_id(); 246 $user_name = AKCH_Guest::current_name(); 247 $is_guest = AKCH_Guest::is_guest(); 248 249 // Guests must have provided a display name. 250 if ( $is_guest && '' === trim( $user_name ) ) { 251 return new WP_Error( 'no_name', __( 'Please enter a name before chatting.', 'akukita-chat-with-ai-translation' ), [ 'status' => 400 ] ); 252 } 253 if ( ! $is_guest && ! is_user_logged_in() ) { 254 return new WP_Error( 'not_authorized', __( 'You must be logged in to chat.', 'akukita-chat-with-ai-translation' ), [ 'status' => 401 ] ); 255 } 256 if ( '' === trim( $user_name ) ) { 257 $user_name = is_user_logged_in() ? wp_get_current_user()->display_name : __( 'Guest', 'akukita-chat-with-ai-translation' ); 258 } 259 260 // -- Anti-spam: per-identity rate limit (flood control). 261 $rl_key = 'akch_rl_' . md5( (string) $user_id . '|' . self::client_ip() ); 262 $last = (int) get_transient( $rl_key ); 263 $now = time(); 264 $min_gap = (int) apply_filters( 'akch_min_seconds_between_messages', 2 ); 265 if ( $last && ( $now - $last ) < $min_gap ) { 266 return new WP_Error( 'too_fast', __( 'You are sending messages too quickly. Please wait a moment.', 'akukita-chat-with-ai-translation' ), [ 'status' => 429 ] ); 267 } 268 set_transient( $rl_key, $now, 60 ); 269 215 270 if ( empty( $lang ) ) { 216 271 $lang = AKCH_AI_Translation::detect_language( $text ); 217 272 } 218 219 $user_id = get_current_user_id();220 $user = wp_get_current_user();221 $user_name = $user->display_name;222 273 223 274 /** … … 227 278 * @since 1.0.3 228 279 * @param bool $allowed Whether posting is allowed. Default true. 229 * @param int $user_id WP user ID .280 * @param int $user_id WP user ID (or negative guest id). 230 281 */ 231 282 if ( ! apply_filters( 'akch_can_post_message', true, $user_id ) ) { … … 284 335 public static function join_room( $req ) { 285 336 $room_id = (int) $req['id']; 286 $user_id = get_current_user_id();287 $language = sanitize_text_field( $req->get_param( 'language' ) ?: ( get_user_meta( $user_id, 'akch_preferred_language', true ) ?: 'en') );337 $user_id = AKCH_Guest::current_id(); 338 $language = sanitize_text_field( $req->get_param( 'language' ) ?: AKCH_Guest::current_lang() ); 288 339 289 340 /** … … 310 361 } 311 362 312 AKCH_Database::join_room( $room_id, $user_id, wp_get_current_user()->display_name, $language ); 313 314 if ( is_user_logged_in() ) { 315 update_user_meta( $user_id, 'akch_preferred_language', $language ); 316 } 363 AKCH_Database::join_room( $room_id, $user_id, AKCH_Guest::current_name() ?: __( 'Guest', 'akukita-chat-with-ai-translation' ), $language ); 364 365 AKCH_Guest::update_language( $language ); 317 366 318 367 return rest_ensure_response( [ 'joined' => true ] ); … … 320 369 321 370 public static function ping( $req ) { 322 AKCH_Database::ping_participant( (int) $req['id'], get_current_user_id() );371 AKCH_Database::ping_participant( (int) $req['id'], AKCH_Guest::current_id() ); 323 372 return rest_ensure_response( [ 'ok' => true ] ); 324 373 } … … 332 381 public static function set_language( $req ) { 333 382 $lang = sanitize_text_field( $req->get_param( 'language' ) ); 334 update_user_meta( get_current_user_id(), 'akch_preferred_language',$lang );383 AKCH_Guest::update_language( $lang ); 335 384 return rest_ensure_response( [ 'language' => $lang, 'ok' => true ] ); 385 } 386 387 /** 388 * Best-effort client IP for rate limiting. Used only as a coarse spam 389 * signal, never stored or displayed. 390 * 391 * @return string 392 */ 393 private static function client_ip() { 394 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; 395 return $ip ? $ip : 'unknown'; 336 396 } 337 397 -
akukita-chat-with-ai-translation/trunk/includes/class-shortcode.php
r3525122 r3558865 23 23 $user_lang = AKCH_Guest::current_lang(); 24 24 25 // Non-logged-in users ONLY see the registration modal — nothing else. 26 if ( ! is_user_logged_in() ) { 25 // The chat output contains per-visitor data (cookies, user IDs, nonces). 26 // Tell page-cache plugins and CDNs not to cache the rendered page. 27 self::mark_uncacheable(); 28 29 // Account mode: non-logged-in visitors get the registration modal. 30 if ( ! is_user_logged_in() && ! AKCH_Guest::guests_allowed() ) { 27 31 ob_start(); 28 32 include AKCH_PLUGIN_DIR . 'templates/register-modal.php'; … … 39 43 $atts = shortcode_atts( [ 'id' => 0, 'height' => 500 ], $atts, 'akukita_chat_room' ); 40 44 41 // Non-logged-in users ONLY see the registration modal 42 if ( ! is_user_logged_in() ) { 45 self::mark_uncacheable(); 46 47 // Account mode: non-logged-in visitors get the registration modal. 48 if ( ! is_user_logged_in() && ! AKCH_Guest::guests_allowed() ) { 43 49 $languages = AKCH_AI_Translation::supported_languages(); 44 50 $user_lang = AKCH_Guest::current_lang(); … … 60 66 return ob_get_clean(); 61 67 } 68 69 /** 70 * Signals to page-cache plugins, hosts and CDNs that the current page 71 * must NOT be served from cache. 72 * 73 * The chat output is inherently per-visitor: it embeds the user/guest id, 74 * fresh nonces and (for guests) the current cookie state. Serving a cached 75 * page to a different visitor breaks guest sessions, makes nonces invalid 76 * and causes "Please enter a name" loops because the cached HTML refers 77 * to someone else's cookie. 78 * 79 * Covers: WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache, 80 * Cloudflare APO, hosting-level edge caches. Each plugin honours one or 81 * more of these signals. 82 * 83 * @return void 84 */ 85 private static function mark_uncacheable() { 86 // WordPress core: disable internal caches for this request. 87 if ( ! defined( 'DONOTCACHEPAGE' ) ) { 88 define( 'DONOTCACHEPAGE', true ); 89 } 90 if ( ! defined( 'DONOTCACHEOBJECT' ) ) { 91 define( 'DONOTCACHEOBJECT', true ); 92 } 93 if ( ! defined( 'DONOTCACHEDB' ) ) { 94 define( 'DONOTCACHEDB', true ); 95 } 96 97 // HTTP headers (CDN-level, reverse proxies, Cloudflare). 98 if ( ! headers_sent() ) { 99 nocache_headers(); 100 } 101 } 62 102 } -
akukita-chat-with-ai-translation/trunk/readme.txt
r3551668 r3558865 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 1. 1.27 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 21 21 = Why people install Akukita Chat = 22 22 23 * 🌍 **70+ languages, zero setup** — Works out of the box with the free MyMemory provider. No registration, no credit card. 23 * 🧙 **Guided setup in 60 seconds** — A built-in setup wizard runs the moment you activate the plugin: pick your translation engine, get a ready-to-use chat page automatically created for you, and you're done. 24 * 👥 **Open the chat to anyone (no account required)** — Switch on guest mode and visitors join with just a display name and a language. Perfect for public communities, support chats and landing pages. Spam protection (rate limiting, length limits and a honeypot) is always on. 25 * 🌍 **70+ languages, zero configuration** — Works out of the box with the free MyMemory provider. No registration, no credit card. 24 26 * 💬 **Multi-room chat** — Create as many topic-based rooms as you want from the admin dashboard. 25 * ⚡ **Real-time** — Messages appear instantly via WebSocket, no page reload.27 * ⚡ **Real-time** — Messages appear instantly, no page reload. 26 28 * 🧠 **3 translation engines** — Free (MyMemory), high-quality (DeepL), or premium AI (OpenAI GPT-4o-mini). You choose. 27 29 * 🚀 **Smart caching** — Each phrase is translated once, then served instantly to everyone. Saves quota and lightens server load. … … 42 44 43 45 1. Install and activate the plugin. 44 2. Pick a translation provider in **Akukita Chat → Settings** (the free MyMemory is selected by default — no API key needed). 45 3. Create your first room in **Akukita Chat → Rooms**. 46 4. Paste `[akukita_chat]` on any page or post. 47 5. Done. Your users can now chat across languages. 46 2. The setup wizard opens automatically and asks two quick questions: which translation engine you want and where to place the chat. It then creates a ready-to-use chat page for you. 47 3. (Optional) Decide who can chat in **Akukita Chat → Settings → Who can chat**: only logged-in users (default), anyone with a display name (guest mode), or visitors who first create a WordPress account. 48 4. Share the chat URL with your audience. That's it. 48 49 49 50 = Shortcodes = … … 113 114 = Do users need a WordPress account? = 114 115 115 Yes, but they don't have to create it manually. When someone visits the chat page without being logged in, a registration modal appears where they can create their account in 10 seconds. This requires **Settings → General → Anyone can register** to be enabled.116 No. By default the chat is open to guests: a visitor enters a display name and their language and can start chatting right away — no account, no email. You can change this under **Akukita Chat → Settings → Who can chat**, where you can instead require visitors to be logged in, or require them to create a WordPress account. Spam protection (rate limiting, message length limits and a honeypot) is always on for guest chat. 116 117 117 118 = What about chat history? = … … 125 126 = Is it compatible with caching plugins (WP Rocket, LiteSpeed, W3 Total Cache)? = 126 127 127 Yes, but exclude the page containing `[akukita_chat]` from full-page cache, or configure your caching plugin to vary cache by login status. The chat output depends on whether the user is logged in.128 Yes, but you must exclude the page containing `[akukita_chat]` from full-page cache. The chat output and its security tokens depend on the visitor, so serving a cached copy to guests can make sending messages fail. Add the chat page to your caching plugin's "never cache" list, or configure it to vary cache by login status. 128 129 129 130 = Does it work with Multisite? = … … 171 172 * Privacy Policy: https://openai.com/policies/row-privacy-policy/ 172 173 174 **4. Deactivation feedback** (optional — only when you submit it) 175 When you deactivate the plugin, an optional form asks why. If — and only if — you fill it in and click "Submit & deactivate", the reason you select, any comment you type, and a reply email (only if you choose to provide one) are emailed to the plugin developer at akukita.es. Choosing "Skip & deactivate" sends nothing. No site URL beyond the standard email signature, and no other site or admin data, is collected. 176 177 * Developer: https://akukita.es 178 173 179 == Privacy == 174 180 … … 176 182 177 183 == Changelog == 184 185 = 1.2.0 = 186 187 **Headline features** 188 189 * 🧙 **New: Guided setup wizard.** Activating the plugin now opens a one-screen wizard that asks for your translation engine and creates a ready-to-use chat page for you — first-run time is under a minute. 190 * 👥 **New: Open chat for guests, no account required.** Switch "Who can chat" to *Anyone* in Settings and visitors join the chat by entering just a display name and a language. Per-identity rate limiting, message length limits and a honeypot field protect open chats from spam, always on. 191 * 💬 **New: Optional deactivation feedback.** When you deactivate the plugin, an opt-in modal asks why. Skip it and nothing is sent. If you submit, only the reason and any text you type leave your site — no URL, no WP version, no other site data. Helps the plugin keep improving. 192 193 **Improvements** 194 195 * Message moderation: administrators can remove individual messages. 196 * "Settings" link added to the row of the Plugins screen. 197 * Clearer labels and an active-mode banner in **Settings → Who can chat**. 198 * The registration / login modal now opens the WooCommerce "My Account" page automatically when WooCommerce is active. A new `akch_login_url` filter lets you point the button anywhere. 199 * Uninstall now removes every plugin option, transient and user meta entry cleanly, including the new 1.2.0 keys. 200 * Pages that contain the chat shortcode are now marked as uncacheable for WP Rocket, LiteSpeed, W3 Total Cache, WP Super Cache and Cloudflare. Avoids one guest's session being served from cache to a different visitor. 201 * All chat input fields (textarea, language picker, registration inputs) now use a white background with dark text and explicit placeholder colours, for consistent readability across OS themes. 202 203 **Bug fixes** 204 205 * The first message sent in an empty room could appear twice on the sender's screen due to a race between the optimistic render and the polling loop. Fixed with a sender-side ID set and a corrected ordering of state updates. 206 * The "Log in" tab in the registration modal did not respond to clicks because the JS bound to an outdated element id. Fixed. 207 * The guest name gate did not initialise on guest-mode pages because the bootstrap only ran for users with positive IDs. It now runs whenever the chat widget is rendered. 208 * Server-side error messages were being swallowed by the front-end API helper. Errors now surface as toasts and, if the server reports a missing name, the name gate reappears automatically with the typed text preserved. 209 * The guest gate now also renders correctly inside themes that aggressively rewrite shortcode output (e.g. Divi), thanks to a fixed-position fallback overlay that attaches directly to `<body>` with a maximum z-index. 178 210 179 211 = 1.1.2 = … … 224 256 == Upgrade Notice == 225 257 258 = 1.2.0 = 259 Major release: new setup wizard, open chat for guests (no account required), opt-in deactivation feedback. Your existing access settings are preserved — chat continues to require login until you explicitly enable guests in Settings → Who can chat. 260 226 261 = 1.1.2 = 227 262 Fixes real-time message sync for the single-room shortcode. Recommended update for everyone using [akukita_chat_room]. -
akukita-chat-with-ai-translation/trunk/templates/admin-settings.php
r3525122 r3558865 116 116 <table class="form-table"> 117 117 <tr> 118 <th><?php esc_html_e( 'Who can chat', 'akukita-chat-with-ai-translation' ); ?></th> 119 <td> 120 <?php $akch_mode = get_option( 'akch_access_mode', 'account' ); ?> 121 <select name="akch_access_mode"> 122 <option value="guest" <?php selected( $akch_mode, 'guest' ); ?>> 123 <?php esc_html_e( 'Anyone — guests can chat with just a name (no account)', 'akukita-chat-with-ai-translation' ); ?> 124 </option> 125 <option value="registered" <?php selected( $akch_mode, 'registered' ); ?>> 126 <?php esc_html_e( 'Logged-in users only — visitors see nothing if not logged in', 'akukita-chat-with-ai-translation' ); ?> 127 </option> 128 <option value="account" <?php selected( $akch_mode, 'account' ); ?>> 129 <?php esc_html_e( 'Require an account — visitors see a registration / login form', 'akukita-chat-with-ai-translation' ); ?> 130 </option> 131 </select> 132 <p class="description"> 133 <?php if ( 'guest' === $akch_mode ) : ?> 134 <strong style="color:#1d7d3a;">✓ <?php esc_html_e( 'Guest mode is active.', 'akukita-chat-with-ai-translation' ); ?></strong> 135 <?php esc_html_e( 'Anyone visiting the chat page enters a display name and language and can chat immediately. Spam protection (rate limiting, length limits and a honeypot) is always on.', 'akukita-chat-with-ai-translation' ); ?> 136 <?php elseif ( 'registered' === $akch_mode ) : ?> 137 <strong><?php esc_html_e( 'Logged-in only.', 'akukita-chat-with-ai-translation' ); ?></strong> 138 <?php esc_html_e( 'Visitors who are not logged in will see nothing on the chat page. Choose "Anyone" above to let guests participate.', 'akukita-chat-with-ai-translation' ); ?> 139 <?php else : ?> 140 <strong><?php esc_html_e( 'Account required.', 'akukita-chat-with-ai-translation' ); ?></strong> 141 <?php esc_html_e( 'Non-logged-in visitors see a registration / login form before they can chat. To allow chatting without registering, choose "Anyone" above.', 'akukita-chat-with-ai-translation' ); ?> 142 <?php endif; ?> 143 </p> 144 </td> 145 </tr> 146 <tr> 118 147 <th><?php esc_html_e( 'Default Language', 'akukita-chat-with-ai-translation' ); ?></th> 119 148 <td> -
akukita-chat-with-ai-translation/trunk/templates/chat-widget.php
r3525122 r3558865 29 29 data-height="<?php echo esc_attr( $atts['height'] ); ?>" 30 30 data-show-rooms="<?php echo esc_attr( $atts['show_rooms'] ); ?>"> 31 32 <?php /* Guest name prompt — shown by chat.js only when akchData.needsName is true. */ ?> 33 <div id="akch-guest-gate" class="akch-guest-gate" style="display:none;"> 34 <div class="akch-guest-gate-card"> 35 <h3><?php echo esc_html( $i18n['welcome'] ?? 'Welcome to the Chat' ); ?></h3> 36 <p><?php echo esc_html( $i18n['welcomeSub'] ?? 'Messages are translated automatically for everyone.' ); ?></p> 37 <label for="akch-guest-name"><?php echo esc_html( $i18n['yourName'] ?? 'Your name' ); ?></label> 38 <input type="text" id="akch-guest-name" maxlength="40" autocomplete="off" 39 placeholder="<?php echo esc_attr( $i18n['yourName'] ?? 'Your name' ); ?>" /> 40 <label for="akch-guest-lang"><?php echo esc_html( $i18n['yourLang'] ?? 'Your language' ); ?></label> 41 <select id="akch-guest-lang"> 42 <?php foreach ( $languages as $code => $name ) : ?> 43 <option value="<?php echo esc_attr( $code ); ?>" <?php selected( $code, $user_lang ); ?>> 44 <?php echo esc_html( $name ); ?> 45 </option> 46 <?php endforeach; ?> 47 </select> 48 <p class="akch-guest-gate-err" id="akch-guest-gate-err" style="display:none;color:#d63638;font-size:13px;"></p> 49 <button type="button" id="akch-guest-enter" class="akch-guest-enter"> 50 <?php echo esc_html( $i18n['enterChat'] ?? 'Enter Chat' ); ?> 51 </button> 52 </div> 53 </div> 31 54 32 55 <?php if ( $atts['show_rooms'] ) : ?> … … 101 124 placeholder="<?php echo esc_attr( $i18n['typeMessage'] ?? 'Type a message…' ); ?>" 102 125 rows="1" disabled></textarea> 126 <?php /* Honeypot: hidden from humans, bots tend to fill it. Do not remove. */ ?> 127 <input type="text" id="akch-website" name="website" tabindex="-1" autocomplete="off" 128 aria-hidden="true" style="position:absolute;left:-9999px;top:-9999px;height:0;width:0;opacity:0;" /> 103 129 <button id="akch-send-btn" class="akch-send-btn" disabled> 104 130 <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"> -
akukita-chat-with-ai-translation/trunk/templates/register-modal.php
r3525122 r3558865 11 11 12 12 $can_register = (bool) get_option( 'users_can_register' ); 13 $login_url = wp_login_url( get_permalink() ); 13 14 // Prefer the site's own account page (e.g. WooCommerce's /mi-cuenta) when 15 // available. Fall back to wp_login_url() so the modal still works on sites 16 // without WooCommerce. Site owners can override the URL with the 17 // 'akch_login_url' filter. 18 $akch_default_login = wp_login_url( get_permalink() ); 19 if ( function_exists( 'wc_get_page_permalink' ) ) { 20 $akch_wc_my_account = wc_get_page_permalink( 'myaccount' ); 21 if ( $akch_wc_my_account ) { 22 $akch_default_login = $akch_wc_my_account; 23 } 24 } 25 /** 26 * Filters the login URL used by the chat registration modal. 27 * 28 * @since 1.2.0 29 * @param string $url The destination URL for the "Go to login page" button. 30 */ 31 $login_url = apply_filters( 'akch_login_url', $akch_default_login ); 14 32 $register_url = wp_registration_url(); 15 33 $lang_cookie = sanitize_text_field( wp_unslash( $_COOKIE['akch_lang'] ?? '' ) ); -
akukita-chat-with-ai-translation/trunk/uninstall.php
r3525122 r3558865 4 4 * 5 5 * Fired when the plugin is deleted via the WordPress admin. 6 * Removes all plugin tables and options from the database.6 * Removes all plugin tables, options and transients from the database. 7 7 * 8 8 * IMPORTANT: WordPress executes this file directly without loading the plugin, … … 38 38 39 39 // ── Delete plugin options ───────────────────────────────────────────────────── 40 // Complete list of every option the plugin may have created across its 41 // lifetime. New options must be added here so uninstall leaves no leftovers. 40 42 $akch_options = array( 43 // Database / core settings. 41 44 'akch_db_version', 42 45 'akch_translation_provider', … … 48 51 'akch_history_days', 49 52 'akch_show_powered_by', 53 // Access mode (introduced in 1.2.0 with guest chat). 54 'akch_access_mode', 55 // Setup wizard (introduced in 1.2.0). 56 'akch_wizard_done', 57 'akch_chat_page_id', 58 // Review notice (introduced in 1.1.0). 59 'akch_install_date', 60 'akch_review_notice_dismissed', 50 61 ); 51 62 52 63 foreach ( $akch_options as $akch_option ) { 53 64 delete_option( $akch_option ); 65 // Also remove from multisite network options just in case. 66 delete_site_option( $akch_option ); 54 67 } 68 69 // ── Delete plugin transients ────────────────────────────────────────────────── 70 // Rate-limit transients use a per-user key (akch_rl_*). Clean them all so 71 // no rows are left behind in wp_options after deletion. 72 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared 73 $wpdb->query( 74 $wpdb->prepare( 75 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", 76 '_transient_akch_%', 77 '_transient_timeout_akch_%' 78 ) 79 ); 80 81 // User meta added by the plugin. 82 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 83 $wpdb->delete( $wpdb->usermeta, array( 'meta_key' => 'akch_preferred_language' ) ); 55 84 56 85 // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)