| @@ -11,8 +11,23 @@ | ||
| 11 | 11 | // yields, so an explicit floating placement never doubles up with the |
| 12 | 12 | // Auto-Display instance (plan 95dd1e). |
| 13 | 13 | private static $floating_instance_rendered = false; |
| 14 | 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 | + | |
| 15 | 30 | public function __construct() { |
| 16 | 31 | // Simply get the options without defining duplicated defaults |
| 17 | 32 | $this->options = get_option('mxchat_options', array()); |
| 18 | 33 | add_shortcode('mxchat_chatbot', array($this, 'render_chatbot_shortcode')); |
| @@ -79,8 +94,17 @@ | ||
| 79 | 94 | // Don't show chatbot if determination is false |
| 80 | 95 | if ($bot_to_show === false) { |
| 81 | 96 | return; |
| 82 | 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 | + } | |
| 83 | 107 | |
| 84 | 108 | // Handle consent for display |
| 85 | 109 | $consent_category = 'marketing'; |
| 86 | 110 | $has_consent = true; |
| @@ -102,8 +126,56 @@ | ||
| 102 | 126 | echo do_shortcode('[mxchat_chatbot floating="yes" has_consent="' . ($has_consent ? 'yes' : 'no') . '"]'); |
| 103 | 127 | } |
| 104 | 128 | } |
| 105 | 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 | + | |
| 106 | 178 | private function mxchat_get_user_identifier() { |
| 107 | 179 | return sanitize_text_field($_SERVER['REMOTE_ADDR']); |
| 108 | 180 | } |
| 109 | 181 | |
| @@ -132,9 +204,18 @@ | ||
| 132 | 204 | 'bot_id' => '' // Support for multi-bot functionality - empty means auto-detect |
| 133 | 205 | ), $atts); |
| 134 | 206 | |
| 135 | 207 | // Determine which bot to use |
| 136 | - $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); | |
| 137 | 218 | |
| 138 | 219 | // A floating shortcode always renders; what it does is suppress the LATER |
| 139 | 220 | // auto-append instance (see append_chatbot_to_body), so a page never gets |
| 140 | 221 | // two floating widgets. This replaced a dead empty branch here that |
| @@ -146,9 +227,8 @@ | ||
| 146 | 227 | // Non-floating shortcodes (floating="no") should NEVER be blocked by the hide setting |
| 147 | 228 | // This allows embedded chatbots even when floating is hidden |
| 148 | 229 | |
| 149 | 230 | $is_floating = $attributes['floating'] === 'yes'; |
| 150 | - $bot_id = sanitize_key($bot_id); // Sanitize bot ID | |
| 151 | 231 | |
| 152 | 232 | // Rest of your existing shortcode rendering logic continues unchanged... |
| 153 | 233 | // [All the existing HTML generation code remains the same] |
| 154 | 234 | |