PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
← All changes | includes/class-mxchat-public.php +336 -27 3.2.15 → 3.2.25 View file →
@@ -5,8 +5,29 @@
5 5
6 6 class MxChat_Public {
7 7 private $options;
8 8
9 + // True once any FLOATING instance has rendered on this request via the
10 + // shortcode (or the block wrapping it) — append_chatbot_to_body() then
11 + // yields, so an explicit floating placement never doubles up with the
12 + // Auto-Display instance (plan 95dd1e).
13 + private static $floating_instance_rendered = false;
14 +
15 + // Bot ids already placed on this page (plan 977cad), floating and inline
16 + // alike. Two copies of one bot share every element id (chat-input-<bot>,
17 + // send-button-<bot>, ...) and chat-script.js looks elements up by id, so
18 + // the second copy's Send reads the FIRST copy's empty box and sends
19 + // nothing. One copy of a bot per page: append_chatbot_to_body() stands
20 + // down for a bot the content already shows, and a second explicit
21 + // placement of the same bot renders an administrator-only note instead.
22 + // Only page renders count: a render in wp-admin, admin-ajax, cron, REST,
23 + // WP-CLI, a feed, or inside wp_head (a plugin building meta tags from the
24 + // content) is not on the page and must not suppress the real one. Note
25 + // that a block theme renders the body BEFORE wp_head (template-canvas
26 + // builds the template first so blocks can enqueue), so "after wp_head"
27 + // is not the test.
28 + private static $rendered_bot_ids = array();
29 +
9 30 public function __construct() {
10 31 // Simply get the options without defining duplicated defaults
11 32 $this->options = get_option('mxchat_options', array());
12 33 add_shortcode('mxchat_chatbot', array($this, 'render_chatbot_shortcode'));
@@ -56,9 +77,18 @@
56 77 // Check if auto-append chatbot should be hidden on this page
57 78 if ($this->should_hide_chatbot('auto')) {
58 79 return; // Don't show auto-appended chatbot
59 80 }
60 -
81 +
82 + // An explicit floating placement (shortcode or the 95dd1e block) already
83 + // rendered during content — appending the auto instance would put two
84 + // floating widgets on the page with colliding element ids. The explicit
85 + // placement wins; content renders before wp_footer, so the flag is set
86 + // by the time this runs.
87 + if (self::$floating_instance_rendered) {
88 + return;
89 + }
90 +
61 91 // Get the bot that should be displayed using new logic
62 92 $bot_to_show = $this->get_display_bot();
63 93
64 94 // Don't show chatbot if determination is false
@@ -64,8 +94,17 @@
64 94 // Don't show chatbot if determination is false
65 95 if ($bot_to_show === false) {
66 96 return;
67 97 }
98 +
99 + // The bot is already on this page as an explicit placement (an inline
100 + // shortcode or block in the content). A second, floating copy would share
101 + // its element ids and could be opened but not send; the embedded one wins
102 + // (plan 977cad). A page whose embedded chatbot is a DIFFERENT bot keeps
103 + // both, their ids differ.
104 + if (self::is_bot_on_page($bot_to_show)) {
105 + return;
106 + }
68 107
69 108 // Handle consent for display
70 109 $consent_category = 'marketing';
71 110 $has_consent = true;
@@ -87,8 +126,56 @@
87 126 echo do_shortcode('[mxchat_chatbot floating="yes" has_consent="' . ($has_consent ? 'yes' : 'no') . '"]');
88 127 }
89 128 }
90 129
130 + /**
131 + * Is this render on the page a visitor sees? (plan 977cad) False in
132 + * wp-admin, admin-ajax, cron, REST, WP-CLI, a feed, and inside wp_head.
133 + * Not "after wp_head": block themes render the body content first.
134 + */
135 + private static function is_page_render() {
136 + if (is_admin() || wp_doing_ajax() || wp_doing_cron()
137 + || (defined('REST_REQUEST') && REST_REQUEST)
138 + || (defined('WP_CLI') && WP_CLI)) {
139 + return false;
140 + }
141 + return !doing_action('wp_head') && !is_feed();
142 + }
143 +
144 + private static function record_bot_on_page($bot_id) {
145 + if (!self::is_page_render()) {
146 + return;
147 + }
148 + $bot_id = sanitize_key($bot_id);
149 + if ($bot_id !== '' && !in_array($bot_id, self::$rendered_bot_ids, true)) {
150 + self::$rendered_bot_ids[] = $bot_id;
151 + }
152 + }
153 +
154 + /**
155 + * Has a placement of this bot already rendered in the page body on this
156 + * request? (plan 977cad)
157 + */
158 + public static function is_bot_on_page($bot_id) {
159 + if (!self::is_page_render()) {
160 + return false;
161 + }
162 + return in_array(sanitize_key($bot_id), self::$rendered_bot_ids, true);
163 + }
164 +
165 + /**
166 + * What a second placement of an already-shown bot renders: nothing for
167 + * visitors, one line for a logged-in administrator (plan 977cad).
168 + */
169 + private static function duplicate_placement_notice() {
170 + if (!current_user_can('manage_options')) {
171 + return '';
172 + }
173 + return '<p class="mxchat-placement-notice" data-nosnippet>'
174 + . esc_html__('MxChat: this page already shows this chatbot, so a second copy of the same bot is not displayed. Only administrators see this note.', 'mxchat')
175 + . '</p>';
176 + }
177 +
91 178 private function mxchat_get_user_identifier() {
92 179 return sanitize_text_field($_SERVER['REMOTE_ADDR']);
93 180 }
94 181
@@ -117,27 +204,31 @@
117 204 'bot_id' => '' // Support for multi-bot functionality - empty means auto-detect
118 205 ), $atts);
119 206
120 207 // Determine which bot to use
121 - $bot_id = $this->determine_bot_for_shortcode($attributes['bot_id']);
208 + $bot_id = sanitize_key($this->determine_bot_for_shortcode($attributes['bot_id']));
209 +
210 + // A second placement of a bot already on this page is not rendered
211 + // (plan 977cad, see $rendered_bot_ids): it could be opened but never
212 + // sent. Administrators get a one-line note in its place so the
213 + // duplicate is visible to the person who can remove it.
214 + if (self::is_bot_on_page($bot_id)) {
215 + return self::duplicate_placement_notice();
216 + }
217 + self::record_bot_on_page($bot_id);
122 218
123 - // UPDATED: Only check hiding for floating shortcodes that could conflict with auto-append
124 - // Non-floating shortcodes should always work
219 + // A floating shortcode always renders; what it does is suppress the LATER
220 + // auto-append instance (see append_chatbot_to_body), so a page never gets
221 + // two floating widgets. This replaced a dead empty branch here that
222 + // documented the intent but never suppressed anything (plan 95dd1e).
125 223 if ($attributes['floating'] === 'yes') {
126 - // For floating shortcodes, check if auto-append is hidden
127 - // This prevents duplicate floating chatbots
128 - if ($this->should_hide_chatbot('auto') && $this->is_auto_append_enabled()) {
129 - // If auto-append is enabled but hidden on this page,
130 - // allow the floating shortcode to work (user is overriding)
131 - // But if auto-append is disabled globally, also allow shortcode
132 - }
224 + self::$floating_instance_rendered = true;
133 225 }
134 -
226 +
135 227 // Non-floating shortcodes (floating="no") should NEVER be blocked by the hide setting
136 228 // This allows embedded chatbots even when floating is hidden
137 229
138 230 $is_floating = $attributes['floating'] === 'yes';
139 - $bot_id = sanitize_key($bot_id); // Sanitize bot ID
140 231
141 232 // Rest of your existing shortcode rendering logic continues unchanged...
142 233 // [All the existing HTML generation code remains the same]
143 234
@@ -215,14 +306,49 @@
215 306 $enable_email_block = isset($current_options['enable_email_block']) &&
216 307 ($current_options['enable_email_block'] === '1' || $current_options['enable_email_block'] === 'on');
217 308
218 309 // Add name field variables
219 - $enable_name_field = isset($current_options['enable_name_field']) &&
310 + $enable_name_field = isset($current_options['enable_name_field']) &&
220 311 ($current_options['enable_name_field'] === '1' || $current_options['enable_name_field'] === 'on');
221 - $name_field_placeholder = isset($current_options['name_field_placeholder']) ?
222 - esc_attr($current_options['name_field_placeholder']) :
312 + $name_field_placeholder = isset($current_options['name_field_placeholder']) ?
313 + esc_attr($current_options['name_field_placeholder']) :
223 314 esc_attr__('Enter your name', 'mxchat');
224 315
316 + // Require Email Address (c0cfaf). Default ON — absent key means required,
317 + // so existing installs render the same markup they always have.
318 + $require_email_field = ($current_options['lead_capture_require_email_toggle'] ?? 'on') === 'on';
319 +
320 + // Pre-fill the form from the profile when a signed-in visitor is being
321 + // shown it (c0cfaf). Emitted ONLY for a logged-in request, so the markup a
322 + // guest gets — the copy a page cache actually stores and re-serves — never
323 + // carries anyone's address. WordPress already marks logged-in responses
324 + // uncacheable, and such a page carries per-user data from other plugins
325 + // regardless.
326 + $prefill_email_attr = '';
327 + $prefill_name_attr = '';
328 + if (is_user_logged_in()) {
329 + if ($user_email !== '') {
330 + $prefill_email_attr = ' value="' . esc_attr($user_email) . '"';
331 + }
332 + if ($user_name !== '') {
333 + $prefill_name_attr = ' value="' . esc_attr($user_name) . '"';
334 + }
335 + }
336 +
337 + // Consent checkbox (b062c4). Default OFF — with the toggle off this block
338 + // renders nothing and the form markup is byte-identical to before.
339 + $enable_consent_checkbox = isset($current_options['enable_consent_checkbox']) &&
340 + ($current_options['enable_consent_checkbox'] === '1' || $current_options['enable_consent_checkbox'] === 'on');
341 + $consent_checkbox_required = isset($current_options['consent_checkbox_required']) &&
342 + ($current_options['consent_checkbox_required'] === '1' || $current_options['consent_checkbox_required'] === 'on');
343 + // Owner content, kses'd through the shared allowlist at render time as well
344 + // as save time — the render is the security boundary the spec names.
345 + $consent_checkbox_label = MxChat_Utils::sanitize_consent_label(
346 + isset($current_options['consent_checkbox_label']) && $current_options['consent_checkbox_label'] !== ''
347 + ? $current_options['consent_checkbox_label']
348 + : __('I agree to the Privacy Policy.', 'mxchat')
349 + );
350 +
225 351 ob_start();
226 352
227 353 // Check if floating attribute is set to 'yes' and wrap accordingly
228 354 if ($is_floating) {
@@ -285,13 +411,29 @@
285 411
286 412 // Add name field if enabled
287 413 if ($enable_name_field) {
288 414 echo ' <label for="user-name-' . esc_attr($bot_id) . '" class="sr-only mxchat-name-label">' . esc_html__('Name', 'mxchat') . '</label>';
289 - echo ' <input type="text" id="user-name-' . esc_attr($bot_id) . '" name="user_name" class="mxchat-name-input" required placeholder="' . $name_field_placeholder . '" />';
415 + echo ' <input type="text" id="user-name-' . esc_attr($bot_id) . '" name="user_name" class="mxchat-name-input" required' . $prefill_name_attr . ' placeholder="' . $name_field_placeholder . '" />';
290 416 }
291 417
418 + // The email field drops its required attribute when the owner
419 + // has made it optional (c0cfaf). Like the consent checkbox,
420 + // the attribute is UX only — the save endpoint re-enforces the
421 + // rule server-side, so a direct POST cannot bypass it.
292 422 echo ' <label for="user-email-' . esc_attr($bot_id) . '" class="sr-only">' . esc_html__('Email Address', 'mxchat') . '</label>';
293 - echo ' <input type="email" id="user-email-' . esc_attr($bot_id) . '" name="user_email" class="mxchat-email-input" required placeholder="' . esc_attr__('Enter your email address', 'mxchat') . '" />';
423 + echo ' <input type="email" id="user-email-' . esc_attr($bot_id) . '" name="user_email" class="mxchat-email-input"' . ($require_email_field ? ' required' : '') . $prefill_email_attr . ' placeholder="' . ($require_email_field ? esc_attr__('Enter your email address', 'mxchat') : esc_attr__('Enter your email address (optional)', 'mxchat')) . '" />';
424 +
425 + // Consent checkbox row (b062c4). The native required attribute
426 + // is UX only — the save endpoint re-enforces it server-side.
427 + // Clicking an anchor inside the label activates the link, not
428 + // the checkbox (interactive descendants skip label activation).
429 + if ($enable_consent_checkbox) {
430 + echo ' <div class="mxchat-consent-row">';
431 + echo ' <input type="checkbox" id="user-consent-' . esc_attr($bot_id) . '" name="user_consent" value="1" class="mxchat-consent-checkbox"' . ($consent_checkbox_required ? ' required' : '') . ' />';
432 + echo ' <label for="user-consent-' . esc_attr($bot_id) . '" class="mxchat-consent-label">' . $consent_checkbox_label . '</label>';
433 + echo ' </div>';
434 + }
435 +
294 436 echo '<button type="submit" id="email-submit-button-' . esc_attr($bot_id) . '" class="email-submit-button">';
295 437 $button_text = isset($current_options['email_blocker_button_text'])
296 438 ? $current_options['email_blocker_button_text']
297 439 : esc_html__('Start Chat', 'mxchat');
@@ -307,10 +449,22 @@
307 449 echo ' </div>';
308 450 echo ' </div>';
309 451
310 452 echo ' <div id="chat-container-' . esc_attr($bot_id) . '" class="chat-container" style="' . ($enable_email_block && $show_email_form ? 'display: none;' : '') . '">';
311 - echo ' <div id="chat-box-' . esc_attr($bot_id) . '" class="chat-box">';
312 - echo ' <div class="bot-message"' . ($skip_inline_colors ? '' : ' style="background: ' . esc_attr($bot_message_bg_color) . ';"') . '>';
453 + // role="log" + polite live region: new messages are announced to
454 + // screen readers explicitly, so the arrival signal no longer rides
455 + // the post-reply input autofocus (suppressed on touch — plan 03799f).
456 + // additions-only + non-atomic, with aria-busy held on the streaming
457 + // bubble until [DONE] (chat-script.js) so a streamed reply is
458 + // announced once, complete — not per token (WCAG 4.1.3, plan 67f126).
459 + echo ' <div id="chat-box-' . esc_attr($bot_id) . '" class="chat-box" role="log" aria-live="polite" aria-atomic="false" aria-relevant="additions text" aria-label="' . esc_attr__('Chat messages', 'mxchat') . '">';
460 + // mxchat-intro-message marks the greeting bubble so nothing has to
461 + // infer it from position (plan a1a79b). Only this element ever
462 + // carries the class — reset and the {visitor_name} substitution
463 + // both key off it, and both used to assume the greeting was still
464 + // the first .bot-message in the box, which stops being true the
465 + // moment chat persistence rehydrates the transcript.
466 + echo ' <div class="bot-message mxchat-intro-message"' . ($skip_inline_colors ? '' : ' style="background: ' . esc_attr($bot_message_bg_color) . ';"') . '>';
313 467 echo ' <div dir="auto"' . ($skip_inline_colors ? '' : ' style="color: ' . esc_attr($bot_message_font_color) . ';"') . '>';
314 468 echo wp_kses_post($intro_message);
315 469 echo ' </div>';
316 470 echo ' </div>';
@@ -360,8 +514,12 @@
360 514 // Max input length (plan a3fae2 part C) — global core setting, 0 = unlimited.
361 515 // Hard-caps typing/paste client-side; the chat handler enforces it server-side too.
362 516 $mxchat_max_input_length = isset($this->options['max_input_length']) ? intval($this->options['max_input_length']) : 0;
363 517 $mxchat_maxlength_attr = $mxchat_max_input_length > 0 ? ' maxlength="' . esc_attr($mxchat_max_input_length) . '"' : '';
518 + // Visually-hidden label = the input's accessible name (WCAG 3.3.2,
519 + // plan 67f126). The placeholder stays as visible prompt copy but
520 + // cannot be the name — it vanishes on the first keystroke.
521 + echo ' <label for="chat-input-' . esc_attr($bot_id) . '" class="sr-only">' . esc_html__('Type your message', 'mxchat') . '</label>';
364 522 echo ' <textarea id="chat-input-' . esc_attr($bot_id) . '" class="chat-input" dir="auto"' . $mxchat_maxlength_attr . ' placeholder="' . esc_attr($input_copy) . '"' . ($skip_inline_colors ? '' : ' style="color: ' . esc_attr($chat_input_font_color) . ';"') . '></textarea>';
365 523 // Language-neutral character counter (plan 7091a2). Numbers only — no
366 524 // translatable strings — so it reads correctly on every-language install.
367 525 // Only rendered when a cap is set; hidden until ~80% of the cap, then
@@ -447,14 +605,22 @@
447 605 echo ' <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" style="fill: none !important;"/>';
448 606 echo ' <circle cx="12" cy="13" r="3" style="fill: none !important;"/>';
449 607 echo ' </svg>';
450 608 echo ' </button>';
451 -
452 -
609 +
610 + // Toolbar items API (plan 65c9b6). Add-ons put their buttons here
611 + // through the mxchat_chat_toolbar_items filter instead of polling
612 + // the DOM after load. Only asked for when the toolbar is on: an item
613 + // nobody can see is work nobody should do (the Woo cart badge used
614 + // to fetch a count for a hidden toolbar on every page view).
615 + if (isset($this->options['chat_toolbar_toggle']) && $this->options['chat_toolbar_toggle'] === 'on') {
616 + echo self::render_toolbar_items($bot_id);
617 + }
618 +
453 619 echo ' </div>';
454 -
620 +
455 621 echo ' <div class="chatbot-footer">';
456 -
622 +
457 623 // Output the privacy notice if enabled
458 624 if ($privacy_toggle && !empty($privacy_text)) {
459 625 echo '<p class="privacy-notice">' . $privacy_text . '</p>';
460 626 }
@@ -500,10 +666,116 @@
500 666 return ob_get_clean();
501 667 }
502 668
503 669 /**
670 + * Render the add-on items of the chat toolbar (plan-mxchat-20260916-65c9b6).
671 + *
672 + * `mxchat_chat_toolbar_items` receives an empty array and the bot id and
673 + * returns items shaped ['id' => 'woo-cart', 'html' => '<button …>…</button>',
674 + * 'order' => 10]. Items are sorted by order (ties keep filter order), keyed
675 + * by id so a later callback can replace an earlier one, and the html is run
676 + * through wp_kses with the post allow-list plus the SVG shapes a toolbar
677 + * icon needs (filterable via mxchat_chat_toolbar_items_allowed_html).
678 + * Add-ons must not add the dropdowns / panels their button opens here —
679 + * the toolbar sits inside an overflow-hidden widget; append those to body
680 + * from JS on first use, the way the Woo cart does.
681 + *
682 + * @param string $bot_id
683 + * @return string HTML, empty when no add-on contributed anything
684 + */
685 + public static function render_toolbar_items($bot_id) {
686 + $items = apply_filters('mxchat_chat_toolbar_items', array(), $bot_id);
687 + if (!is_array($items) || empty($items)) {
688 + return '';
689 + }
690 +
691 + $clean = array();
692 + $seq = 0;
693 + foreach ($items as $key => $item) {
694 + if (!is_array($item) || empty($item['html']) || !is_string($item['html'])) {
695 + continue;
696 + }
697 + $id = isset($item['id']) ? sanitize_key($item['id']) : (is_string($key) ? sanitize_key($key) : '');
698 + if ($id === '') {
699 + continue;
700 + }
701 + $clean[$id] = array(
702 + 'html' => $item['html'],
703 + 'order' => isset($item['order']) ? (int) $item['order'] : 10,
704 + 'seq' => $seq++,
705 + );
706 + }
707 + if (empty($clean)) {
708 + return '';
709 + }
710 +
711 + uasort($clean, function ($a, $b) {
712 + if ($a['order'] === $b['order']) {
713 + return $a['seq'] <=> $b['seq'];
714 + }
715 + return $a['order'] <=> $b['order'];
716 + });
717 +
718 + $allowed = self::toolbar_items_allowed_html();
719 + $out = '';
720 + foreach ($clean as $id => $item) {
721 + $html = trim(wp_kses($item['html'], $allowed));
722 + if ($html === '') {
723 + continue;
724 + }
725 + $out .= ' <!-- toolbar item: ' . esc_html($id) . ' -->' . "\n";
726 + $out .= ' ' . $html . "\n";
727 + }
728 + return $out;
729 + }
730 +
731 + /**
732 + * Allow-list for toolbar item markup: everything wp_kses_post allows (button,
733 + * span, div, aria-* and data-* are in there) plus inline SVG icon shapes.
734 + */
735 + private static function toolbar_items_allowed_html() {
736 + $allowed = wp_kses_allowed_html('post');
737 + $shape = array(
738 + 'fill' => true,
739 + 'stroke' => true,
740 + 'stroke-width' => true,
741 + 'stroke-linecap' => true,
742 + 'stroke-linejoin' => true,
743 + 'transform' => true,
744 + 'style' => true,
745 + 'class' => true,
746 + );
747 + $allowed['svg'] = array_merge($shape, array(
748 + 'xmlns' => true,
749 + 'viewbox' => true,
750 + 'width' => true,
751 + 'height' => true,
752 + 'aria-hidden' => true,
753 + 'focusable' => true,
754 + 'role' => true,
755 + ));
756 + $allowed['path'] = array_merge($shape, array('d' => true));
757 + $allowed['circle'] = array_merge($shape, array('cx' => true, 'cy' => true, 'r' => true));
758 + $allowed['line'] = array_merge($shape, array('x1' => true, 'y1' => true, 'x2' => true, 'y2' => true));
759 + $allowed['polyline'] = array_merge($shape, array('points' => true));
760 + $allowed['polygon'] = array_merge($shape, array('points' => true));
761 + $allowed['rect'] = array_merge($shape, array('x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true));
762 + $allowed['g'] = $shape;
763 + if (!isset($allowed['button'])) {
764 + $allowed['button'] = array();
765 + }
766 + $allowed['button'] = array_merge($allowed['button'], array(
767 + 'type' => true,
768 + 'disabled' => true,
769 + 'aria-haspopup' => true,
770 + 'aria-pressed' => true,
771 + ));
772 + return apply_filters('mxchat_chat_toolbar_items_allowed_html', $allowed);
773 + }
774 +
775 + /**
504 776 * Get bot-specific options for multi-bot functionality
505 - * Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
777 +* Falls back to default options if bot_id is 'default' or multi-bot add-on is not active
506 778 */
507 779 private function get_bot_options($bot_id = 'default') {
508 780 // If default bot or multi-bot add-on not active, return empty (use default options)
509 781 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
@@ -539,13 +811,27 @@
539 811 }
540 812
541 813
542 814 private function determine_email_collection_state() {
543 - // Logged-in users skip the email form
815 + // Logged-in users skip the email form, UNLESS the owner has turned on
816 + // "Also Show for Logged-In Users" (c0cfaf). On a membership or LMS site
817 + // every visitor is signed in, so the short-circuit meant lead capture —
818 + // and the consent checkbox — never appeared at all.
819 + //
820 + // Deliberately no PII in this decision: the returned profile values are
821 + // NOT rendered into the markup, because this page HTML is cacheable and
822 + // a cached form carrying one user's address would serve it to the next
823 + // visitor. Pre-fill travels on the nocache'd AJAX check instead
824 + // (mxchat_check_email_provided). Both logged-in and guest requests render
825 + // the same empty form, so the cached copy is correct for either.
544 826 if (is_user_logged_in()) {
827 + $options = get_option('mxchat_options', array());
828 + $show_for_logged_in = isset($options['lead_capture_logged_in_toggle'])
829 + && $options['lead_capture_logged_in_toggle'] === 'on';
830 +
545 831 $current_user = wp_get_current_user();
546 832 return [
547 - 'show_email_form' => false,
833 + 'show_email_form' => $show_for_logged_in,
548 834 'user_email' => $current_user->user_email,
549 835 'user_name' => $current_user->display_name ?: $current_user->first_name ?: ''
550 836 ];
551 837 }
@@ -581,8 +867,23 @@
581 867 * constructor registers a wp_footer action, so constructing a throwaway
582 868 * instance would double-append the widget.
583 869 */
584 870 public static function compute_display_bot() {
871 + // The Elementor builder canvas is not a chat surface (plan 95dd1e part 2):
872 + // its preview iframe is a real front-end page load, so without this guard
873 + // the Auto-Display instance renders LIVE inside the canvas — front-end
874 + // chat scripts firing in the editor, able to start a session for the
875 + // person editing. Returning false here suppresses both the wp_footer
876 + // auto-append and the asset enqueue gate for preview requests only; the
877 + // placement widget's own canvas rendering is a static placeholder anyway.
878 + if (did_action('elementor/loaded')
879 + && class_exists('\Elementor\Plugin')
880 + && isset(\Elementor\Plugin::$instance->preview)
881 + && \Elementor\Plugin::$instance->preview
882 + && \Elementor\Plugin::$instance->preview->is_preview_mode()) {
883 + return false;
884 + }
885 +
585 886 $options = get_option('mxchat_options', array());
586 887 if (!is_array($options)) {
587 888 $options = array();
588 889 }
@@ -664,8 +965,16 @@
664 965
665 966 if ($display_bot === false && is_singular()) {
666 967 $post = get_post();
667 968 if ($post && has_shortcode((string) $post->post_content, 'mxchat_chatbot')) {
969 + $has_shortcode = true;
970 + }
971 + // The mxchat/chatbot block (plan-95dd1e) stores a block comment, not
972 + // shortcode text, so has_shortcode can't see it — first-chance detect
973 + // it here for the same no-FOUC reason. The render-time safety net in
974 + // render_chatbot_shortcode() still covers template/widget placements.
975 + if ($post && !$has_shortcode && function_exists('has_block')
976 + && has_block('mxchat/chatbot', $post)) {
668 977 $has_shortcode = true;
669 978 }
670 979 }
671 980