Plugin Directory

Changeset 3558865


Ignore:
Timestamp:
06/03/2026 06:37:16 AM (5 months ago)
Author:
akukitadvt
Message:

v1.2.0 — Guest chat, setup wizard, deactivation feedback, accessibility and bug fixes

Location:
akukita-chat-with-ai-translation/trunk
Files:
3 added
14 edited

Legend:

Unmodified
Added
Removed
  • akukita-chat-with-ai-translation/trunk/akukita-chat.php

    r3551668 r3558865  
    1010 * @wordpress-plugin
    1111 * 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/
    1313 * 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.2
     14 * Version:           1.2.0
    1515 * Requires at least: 6.0
    1616 * Requires PHP:      7.4
    … …  
    2828
    2929// Plugin constants.
    30 define( 'AKCH_VERSION',     '1.1.2' );
     30define( 'AKCH_VERSION',     '1.2.0' );
    3131define( 'AKCH_PLUGIN_DIR',  plugin_dir_path( __FILE__ ) );
    3232define( 'AKCH_PLUGIN_URL',  plugin_dir_url( __FILE__ ) );
    … …  
    4646        'guest',
    4747        'freemium',
    48         'review-notice'
     48        'review-notice',
     49        'setup-wizard',
     50        'deactivation-feedback'
    4951);
    5052foreach ( $akch_modules as $akch_module ) {
    … …  
    133135                AKCH_Admin::init();
    134136                AKCH_Review_Notice::init();
     137                AKCH_Setup_Wizard::init();
     138                if ( is_admin() ) {
     139                        AKCH_Deactivation_Feedback::init();
     140                }
    135141        }
    136142
    137143        /**
    138144         * 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.
    140148         *
    141149         * @return void
    … …  
    151159                );
    152160
    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() ) {
    156163                        $akch_reg_data = array(
    157164                                'ajaxUrl'  => admin_url( 'admin-ajax.php' ),
    … …  
    177184                }
    178185
     186                // Logged-in users AND guests (when guest mode is on) get the full chat.
    179187                wp_enqueue_style(
    180188                        'akch-chat',
    … …  
    197205        /**
    198206         * 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).
    200209         *
    201210         * @return void
    202211         */
    203212        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() ) {
    205217                        return;
    206218                }
    207219
    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;
    212236                }
    213237
    … …  
    217241                        'nonce'        => wp_create_nonce( 'akch_nonce' ),
    218242                        'restNonce'    => wp_create_nonce( 'wp_rest' ),
    219                         'userId'       => $user->ID,
    220                         'userName'     => $user->display_name,
     243                        'userId'       => $user_id,
     244                        'userName'     => $user_name,
    221245                        '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,
    225250                        'pollInterval' => 3000,
    226251                        'i18n'         => self::get_i18n( $user_lang ),
    … …  
    253278                $base = array(
    254279                        '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' ),
    255286                        'translating'    => __( 'Translating…', 'akukita-chat-with-ai-translation' ),
    256287                        'you'            => __( 'You', 'akukita-chat-with-ai-translation' ),
    … …  
    356387
    357388// Activation / deactivation hooks must be registered at file scope, not inside a class constructor.
    358 register_activation_hook( __FILE__, array( 'AKCH_Database', 'install' ) );
     389register_activation_hook( __FILE__, 'akch_on_activate' );
    359390register_deactivation_hook( __FILE__, array( 'Akukita_Chat', 'deactivate_plugin' ) );
    360391
     392/**
     393 * Activation callback: install tables and flag the one-time setup redirect.
     394 *
     395 * @return void
     396 */
     397function 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 */
     410function 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}
     416add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), 'akch_plugin_action_links' );
     417
    361418Akukita_Chat::get_instance();
  • akukita-chat-with-ai-translation/trunk/assets/css/chat.css

    r3525122 r3558865  
    119119.akch-select {
    120120    width: 100%;
    121     background: var(--akch-surface-2);
     121    background: #ffffff;
    122122    border: 1px solid var(--akch-border);
    123     color: var(--akch-text);
     123    color: #1a1a1a;
    124124    border-radius: var(--akch-radius-sm);
    125125    padding: 6px 10px;
    … …  
    129129    outline: none;
    130130    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}
    133144
    134145/* ── Room List ── */
    … …  
    509520.akch-message-input {
    510521    flex: 1;
    511     background: var(--akch-surface-2);
     522    background: #ffffff;
    512523    border: 1px solid var(--akch-border);
    513524    border-radius: var(--akch-radius);
    514525    padding: 10px 14px;
    515     color: var(--akch-text);
     526    color: #1a1a1a;
    516527    font-family: var(--akch-font);
    517528    font-size: 14px;
    … …  
    528539    box-shadow: 0 0 0 3px rgba(79,110,247,0.15);
    529540}
    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; }
    531546.akch-message-input:disabled { opacity: 0.5; cursor: not-allowed; }
    532547
    … …  
    656671.akch-input {
    657672    width: 100%;
    658     background: var(--akch-bg);
     673    background: #ffffff;
    659674    border: 1px solid var(--akch-border);
    660675    border-radius: var(--akch-radius-sm);
    661676    padding: 9px 12px;
    662     color: var(--akch-text);
     677    color: #1a1a1a;
    663678    font-family: var(--akch-font);
    664679    font-size: 14px;
    … …  
    667682}
    668683.akch-input:focus { border-color: var(--akch-accent); }
     684.akch-input::placeholder { color: #8c8c95; opacity: 1; }
    669685
    670686.akch-modal-actions {
    … …  
    10071023    .akch-auth-card { padding: 28px 20px; }
    10081024}
     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 */
     1087body.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  
    1818        userId:          parseInt(akchData.userId),
    1919        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(),
    2024    };
    2125
    … …  
    3741                headers: this._headers(),
    3842                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                }
    4151                return r.json();
    4252            });
    … …  
    4858                body:    JSON.stringify(body),
    4959                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                }
    5268                return r.json();
    5369            });
    … …  
    5874                headers: this._headers(),
    5975                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                }
    6284                return r.json();
    6385            });
    … …  
    131153                    ${senderEl}
    132154                    <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>
    134156                </div>
    135157                <div class="akch-bubble">${displayText}</div>
    … …  
    154176
    155177        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).
    156182            if (document.querySelector(`.akch-message[data-id="${msg.id}"]`)) return;
     183            if (state.ownIds && state.ownIds.has(String(msg.id))) return;
     184
    157185            const html = buildMessageHTML(msg);
    158186            if (prepend) {
    … …  
    240268        state.currentRoomName = roomName;
    241269        state.lastMessageId   = 0;
     270        state.ownIds.clear();
    242271
    243272        // Update UI
    … …  
    301330
    302331        try {
     332            const hp = document.getElementById('akch-website');
    303333            const saved = await api.post(`rooms/${state.currentRoomId}/messages`, {
    304334                text: text,
    305335                lang: state.userLang,
     336                website: hp ? hp.value : '',
    306337            });
    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
    308348            const tempEl = document.querySelector(`.akch-message[data-id="${tempId}"]`);
    309349            if (tempEl) tempEl.setAttribute('data-id', saved.id);
    310             if (parseInt(saved.id) > state.lastMessageId) state.lastMessageId = parseInt(saved.id);
    311350        } catch (e) {
    312351            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.
    313370            const tempEl = document.querySelector(`.akch-message[data-id="${tempId}"]`);
    314371            if (tempEl) {
    315372                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.');
    317379            }
    318380        } finally {
    … …  
    541603    // ── Init ──────────────────────────────────────────────────────────────────
    542604    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
    543615        // Re-read identity from akchData (set by wp_head inline script)
    544616        state.userId   = (akchData && akchData.userId)   || state.userId   || 0;
    … …  
    546618        state.userLang = (akchData && akchData.userLang) || 'en';
    547619
     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() {
    548653        // Sync the language dropdown to the correct value
    549654        const sel = document.getElementById('akch-user-lang');
    … …  
    580685    }
    581686
     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
    582883    // Expose globally
    583884    window.akchChatInit = init;
  • akukita-chat-with-ai-translation/trunk/assets/js/guest.js

    r3525122 r3558865  
    11/**
    22 * 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.
    914 */
    1015(function() {
    … …  
    2631    }
    2732
    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';
    3049    }
    3150
    … …  
    3453        removeOldOverlay();
    3554
    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;
    5459        }
    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        }
    5677    }
    5778
    … …  
    7899        'use strict';
    79100
    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' ) ) {
    82103                return;
    83104        }
  • akukita-chat-with-ai-translation/trunk/includes/class-admin.php

    r3525122 r3558865  
    6969            'akch_messages_per_load'    => 'absint',
    7070            'akch_history_days'         => 'absint',
     71            'akch_access_mode'          => 'sanitize_text_field',
    7172        ];
    7273        foreach ( $settings as $option => $sanitize ) {
    … …  
    9899        if ( isset( $_POST['akch_save_settings'] ) ) {
    99100            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 ) {
    101102                if ( isset( $_POST[ $key ] ) ) {
    102103                    update_option( $key, sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) );
  • akukita-chat-with-ai-translation/trunk/includes/class-database.php

    r3525122 r3558865  
    7777        add_option( 'akch_translation_provider', 'mymemory' );
    7878        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' );
    7983        add_option( 'akch_messages_per_load', 50 );
    8084        add_option( 'akch_history_days', 30 );
    … …  
    326330    }
    327331
     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
    328348    // -- Participants ----------------------------------------------------------
    329349
  • akukita-chat-with-ai-translation/trunk/includes/class-guest.php

    r3525122 r3558865  
    99 */
    1010
    11 if ( ! defined( 'ABSPATH' ) ) exit;
     11if ( ! defined( 'ABSPATH' ) ) {
     12        exit;
     13}
    1214
    1315/**
    14  * User Registration Handler
     16 * Identity / access handler.
    1517 *
    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.
    2127 */
    2228class AKCH_Guest {
    2329
    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        }
    159377}
  • akukita-chat-with-ai-translation/trunk/includes/class-rest-api.php

    r3525122 r3558865  
    3939        ] );
    4040
     41        register_rest_route( $ns, '/messages/(?P<id>\d+)', [
     42            [ 'methods' => 'DELETE', 'callback' => [ __CLASS__, 'delete_message' ], 'permission_callback' => [ __CLASS__, 'require_admin' ] ],
     43        ] );
     44
    4145        register_rest_route( $ns, '/rooms/(?P<id>\d+)/join', [
    4246            'methods'             => 'POST',
    … …  
    7276    // -- Permission callbacks --------------------------------------------------
    7377
    74     /** Requires a logged-in WP user */
     78    /** Requires a logged-in WP user, or a guest when guest mode is enabled. */
    7579    public static function require_chatter() {
    76         if ( is_user_logged_in() ) return true;
     80        if ( AKCH_Guest::can_chat() ) {
     81            return true;
     82        }
    7783        return new WP_Error( 'not_authorized', __( 'You must be logged in to chat.', 'akukita-chat-with-ai-translation' ), [ 'status' => 401 ] );
    7884    }
    … …  
    101107     */
    102108    public static function can_access_room( WP_REST_Request $request ) {
    103         if ( ! is_user_logged_in() ) {
     109        if ( ! AKCH_Guest::can_chat() ) {
    104110            return new WP_Error( 'not_authorized', __( 'You must be logged in to chat.', 'akukita-chat-with-ai-translation' ), [ 'status' => 401 ] );
    105111        }
    … …  
    166172    }
    167173
     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
    168185    // -- Message endpoints -----------------------------------------------------
    169186
    … …  
    173190        $before_id = (int) ( $req->get_param( 'before_id' ) ?: 0 );
    174191        $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';
    176193
    177194        $messages = AKCH_Database::get_messages( $room_id, $limit, $before_id, $after_id );
    … …  
    209226        $lang    = sanitize_text_field( $req->get_param( 'lang' ) ?: '' );
    210227
     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
    211235        if ( empty( $text ) ) {
    212236            return new WP_Error( 'empty_message', __( 'Message cannot be empty.', 'akukita-chat-with-ai-translation' ), [ 'status' => 400 ] );
    213237        }
    214238
     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
    215270        if ( empty( $lang ) ) {
    216271            $lang = AKCH_AI_Translation::detect_language( $text );
    217272        }
    218 
    219         $user_id   = get_current_user_id();
    220         $user       = wp_get_current_user();
    221         $user_name = $user->display_name;
    222273
    223274        /**
    … …  
    227278         * @since 1.0.3
    228279         * @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).
    230281         */
    231282        if ( ! apply_filters( 'akch_can_post_message', true, $user_id ) ) {
    … …  
    284335    public static function join_room( $req ) {
    285336        $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() );
    288339
    289340        /**
    … …  
    310361        }
    311362
    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 );
    317366
    318367        return rest_ensure_response( [ 'joined' => true ] );
    … …  
    320369
    321370    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() );
    323372        return rest_ensure_response( [ 'ok' => true ] );
    324373    }
    … …  
    332381    public static function set_language( $req ) {
    333382        $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 );
    335384        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';
    336396    }
    337397
  • akukita-chat-with-ai-translation/trunk/includes/class-shortcode.php

    r3525122 r3558865  
    2323        $user_lang = AKCH_Guest::current_lang();
    2424
    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() ) {
    2731            ob_start();
    2832            include AKCH_PLUGIN_DIR . 'templates/register-modal.php';
    … …  
    3943        $atts = shortcode_atts( [ 'id' => 0, 'height' => 500 ], $atts, 'akukita_chat_room' );
    4044
    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() ) {
    4349            $languages = AKCH_AI_Translation::supported_languages();
    4450            $user_lang = AKCH_Guest::current_lang();
    … …  
    6066        return ob_get_clean();
    6167    }
     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    }
    62102}
  • akukita-chat-with-ai-translation/trunk/readme.txt

    r3551668 r3558865  
    55Tested up to: 7.0
    66Requires PHP: 7.4
    7 Stable tag: 1.1.2
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2121= Why people install Akukita Chat =
    2222
    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.
    2426* 💬 **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.
    2628* 🧠 **3 translation engines** — Free (MyMemory), high-quality (DeepL), or premium AI (OpenAI GPT-4o-mini). You choose.
    2729* 🚀 **Smart caching** — Each phrase is translated once, then served instantly to everyone. Saves quota and lightens server load.
    … …  
    4244
    43451. 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.
     462. 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.
     473. (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.
     484. Share the chat URL with your audience. That's it.
    4849
    4950= Shortcodes =
    … …  
    113114= Do users need a WordPress account? =
    114115
    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.
     116No. 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.
    116117
    117118= What about chat history? =
    … …  
    125126= Is it compatible with caching plugins (WP Rocket, LiteSpeed, W3 Total Cache)? =
    126127
    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.
     128Yes, 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.
    128129
    129130= Does it work with Multisite? =
    … …  
    171172* Privacy Policy: https://openai.com/policies/row-privacy-policy/
    172173
     174**4. Deactivation feedback** (optional — only when you submit it)
     175When 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
    173179== Privacy ==
    174180
    … …  
    176182
    177183== 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.
    178210
    179211= 1.1.2 =
    … …  
    224256== Upgrade Notice ==
    225257
     258= 1.2.0 =
     259Major 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
    226261= 1.1.2 =
    227262Fixes 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  
    116116        <table class="form-table">
    117117            <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>
    118147                <th><?php esc_html_e( 'Default Language', 'akukita-chat-with-ai-translation' ); ?></th>
    119148                <td>
  • akukita-chat-with-ai-translation/trunk/templates/chat-widget.php

    r3525122 r3558865  
    2929     data-height="<?php echo esc_attr( $atts['height'] ); ?>"
    3030     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>
    3154
    3255    <?php if ( $atts['show_rooms'] ) : ?>
    … …  
    101124                    placeholder="<?php echo esc_attr( $i18n['typeMessage'] ?? 'Type a message…' ); ?>"
    102125                    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;" />
    103129                <button id="akch-send-btn" class="akch-send-btn" disabled>
    104130                    <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
  • akukita-chat-with-ai-translation/trunk/templates/register-modal.php

    r3525122 r3558865  
    1111
    1212$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() );
     19if ( 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 );
    1432$register_url  = wp_registration_url();
    1533$lang_cookie   = sanitize_text_field( wp_unslash( $_COOKIE['akch_lang'] ?? '' ) );
  • akukita-chat-with-ai-translation/trunk/uninstall.php

    r3525122 r3558865  
    44 *
    55 * 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.
    77 *
    88 * IMPORTANT: WordPress executes this file directly without loading the plugin,
    … …  
    3838
    3939// ── 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.
    4042$akch_options = array(
     43        // Database / core settings.
    4144        'akch_db_version',
    4245        'akch_translation_provider',
    … …  
    4851        'akch_history_days',
    4952        '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',
    5061);
    5162
    5263foreach ( $akch_options as $akch_option ) {
    5364        delete_option( $akch_option );
     65        // Also remove from multisite network options just in case.
     66        delete_site_option( $akch_option );
    5467}
     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' ) );
    5584
    5685// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
Note: See TracChangeset for help on using the changeset viewer.