Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 9, 2026

CVE-2024-13362: Freemius <= 2.10.1 – Reflected DOM-Based Cross-Site Scripting via url Parameter (internal-links)

Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 2.24.6
Patched Version 2.25.2
Disclosed April 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2024-13362:

This is a Reflected Cross-Site Scripting (XSS) vulnerability in the Freemius SDK versions up to 2.10.1, which is bundled by multiple WordPress plugins and themes. The vulnerability allows unauthenticated attackers to inject arbitrary web scripts via the ‘url’ parameter. The CVSS score is 6.1, indicating medium severity.

Root Cause:
The vulnerability stems from insufficient input sanitization and output escaping of the ‘url’ parameter within Freemius SDK components. The code diff shows multiple asset file modifications across the internal-links plugin (a consumer of the Freemius SDK), specifically in files such as ‘ilj-keywords-editor.min.asset.php’, ‘ilj_editor.min.asset.php’, ‘ilj_keywords.min.asset.php’, and ‘ilj_statistic.min.asset.php’. These changes primarily involve version bumps and whitespace/code restructuring in backend files like ‘adminbar.php’, ‘adminmenu.php’, ‘editor.php’, and ‘batchinfo.php’, indicating the vulnerability is in how these admin-side scripts handle user-controlled input from the URL without proper escaping before embedding it into the DOM.

Exploitation:
An attacker crafts a malicious URL containing a ‘url’ parameter with encoded JavaScript payload. For example, a link like ‘https://target.com/wp-admin/admin.php?page=internal_link_juicer&url=javascript:alert(document.cookie)’ or using a data: URI scheme. When a logged-in administrator clicks this link, the vulnerable JavaScript included in the admin bar or editor metabox reads the ‘url’ parameter from the query string and inserts it directly into the page DOM without sanitization, causing the injected script to execute in the context of the WordPress admin area. The attack requires user interaction (clicking a crafted link) but no authentication beyond the victim being logged in.

Patch Analysis:
The patch updates version strings in multiple .asset.php files (e.g., ‘ilj-keywords-editor.min.asset.php’ changes from version ‘5d19d3d9a4690453c7c4′ to ’93a99787bca25fbd6a9e’, ‘ilj_editor.min.asset.php’ from ‘4cedb45623fa20f17577’ to ‘b30bd66bc7c9d79364e4’). These version bumps force browsers to fetch updated minified JavaScript files that contain the actual fix. The fix in the associated JavaScript files properly escapes or validates the ‘url’ parameter before using it, preventing script injection. The structural changes in PHP files (adminbar.php, editor.php, etc.) also indicate improved code organization but the primary fix is in the JS assets.

Impact:
Successful exploitation allows an attacker to execute arbitrary JavaScript within the WordPress administrative dashboard of a victim administrator. This can lead to session hijacking, credential theft (via fake login forms), creation of new admin accounts, injection of malicious content, or redirection to phishing sites. Since the attack is reflected and requires user interaction, the risk is moderate, but the lack of authentication requirement makes it easier to launch via social engineering campaigns.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/internal-links/admin/js/ilj-keywords-editor.min.asset.php
+++ b/internal-links/admin/js/ilj-keywords-editor.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '5d19d3d9a4690453c7c4');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => '93a99787bca25fbd6a9e');
--- a/internal-links/admin/js/ilj-link-index-status-func.min.asset.php
+++ b/internal-links/admin/js/ilj-link-index-status-func.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => 'ea6d5513fc020ac4118e');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'ea6d5513fc020ac4118e');
--- a/internal-links/admin/js/ilj_admin_menu_bar.min.asset.php
+++ b/internal-links/admin/js/ilj_admin_menu_bar.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '95e0728424530c461c89');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => '95e0728424530c461c89');
--- a/internal-links/admin/js/ilj_admin_script.min.asset.php
+++ b/internal-links/admin/js/ilj_admin_script.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => 'f72b1d63a7d5c4d39018');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'f72b1d63a7d5c4d39018');
--- a/internal-links/admin/js/ilj_ajax_index_rebuild.min.asset.php
+++ b/internal-links/admin/js/ilj_ajax_index_rebuild.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => 'b57689138972d2c5f87e');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'b57689138972d2c5f87e');
--- a/internal-links/admin/js/ilj_custom_links.min.asset.php
+++ b/internal-links/admin/js/ilj_custom_links.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '26d4f17e1cb1d7caa91b');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => '26d4f17e1cb1d7caa91b');
--- a/internal-links/admin/js/ilj_editor.min.asset.php
+++ b/internal-links/admin/js/ilj_editor.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '4cedb45623fa20f17577');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'b30bd66bc7c9d79364e4');
--- a/internal-links/admin/js/ilj_keywords.min.asset.php
+++ b/internal-links/admin/js/ilj_keywords.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '3dbaeff392f22aa1209b');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'da64fabcb2612dc405e2');
--- a/internal-links/admin/js/ilj_menu_settings.min.asset.php
+++ b/internal-links/admin/js/ilj_menu_settings.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '767f1bfec8ae39a5f104');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => '767f1bfec8ae39a5f104');
--- a/internal-links/admin/js/ilj_promo.min.asset.php
+++ b/internal-links/admin/js/ilj_promo.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => 'c0ad3da2998ed0245e08');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'c0ad3da2998ed0245e08');
--- a/internal-links/admin/js/ilj_rating_notification.min.asset.php
+++ b/internal-links/admin/js/ilj_rating_notification.min.asset.php
@@ -1,3 +0,0 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => '0f28bf2ab747c7b799a7');
 No newline at end of file
--- a/internal-links/admin/js/ilj_statistic.min.asset.php
+++ b/internal-links/admin/js/ilj_statistic.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => 'a59bce424f1445514818');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => 'f3ba579982093271486c');
--- a/internal-links/admin/js/ilj_tools.min.asset.php
+++ b/internal-links/admin/js/ilj_tools.min.asset.php
@@ -1,3 +1 @@
-<?php
-
-return array('dependencies' => array('wp-polyfill'), 'version' => 'f87c0eb37b787f57920a');
 No newline at end of file
+<?php return array('dependencies' => array('wp-polyfill'), 'version' => '0353c06bf8a4a934c09a');
--- a/internal-links/backend/adminbar.php
+++ b/internal-links/backend/adminbar.php
@@ -5,6 +5,7 @@
 use ILJCoreOptions;
 use ILJCoreOptionsCacheToggleBtnSwitch;
 use ILJHelperBatchInfo as HelperBatchInfo;
+
 /**
  * Admin bar
  *
@@ -13,22 +14,27 @@
  * @package ILJBackend
  * @since   2.0.0
  */
-class AdminBar
-{
-    /**
-     * Add a link to the admin toolbar
-     *
-     * @param WP_Admin_Bar $admin_bar
-     *
-     * @return void
-     * @since  2.0.0
-     */
-    public static function addLink($admin_bar)
-    {
-        $batch_build_info = new HelperBatchInfo();
-        $batch_percentage = $batch_build_info->getBatchPercentage();
-        $status = $batch_build_info->getBatchStatus();
-        $args = array('id' => 'ilj', 'title' => '<span class="ilj_icon" aria-hidden="true"></span>', 'href' => add_query_arg(array('page' => AdminMenu::ILJ_MENUPAGE_SLUG), admin_url('admin.php')), 'meta' => array('html' => '
+class AdminBar {
+
+	/**
+	 * Add a link to the admin toolbar
+	 *
+	 * @param WP_Admin_Bar $admin_bar
+	 *
+	 * @return void
+	 * @since  2.0.0
+	 */
+	public static function addLink($admin_bar) {
+		$batch_build_info = new HelperBatchInfo();
+		$batch_percentage = $batch_build_info->getBatchPercentage();
+		$status           = $batch_build_info->getBatchStatus();
+
+		$args = array(
+			'id'    => 'ilj',
+			'title' => '<span class="ilj_icon" aria-hidden="true"></span>',
+			'href'  => add_query_arg(array('page' => AdminMenu::ILJ_MENUPAGE_SLUG), admin_url('admin.php')),
+			'meta'  => array(
+				'html' => '
                 <a class="ilj_admin_bar_link" style="height: 0px;" href = "' . add_query_arg(array('page' => AdminMenu::ILJ_MENUPAGE_SLUG), admin_url('admin.php')) . '">
                     <div class="ilj_admin_bar_container">
                         <div class="ilj_bar_title_container"> Linkindex: <span id="ilj_batch_progress">' . $batch_percentage . '%</span></div>
@@ -36,14 +42,23 @@
                             <div style="width:' . $batch_percentage . '%"></div>
                         </div>
                     </div>
-                </a>'));
-        $admin_bar->add_node($args);
-        $cache_option = Options::getOption(CacheToggleBtnSwitch::getKey());
-        if ($cache_option) {
-            // Disable in version 2.23.5 due to conflicts with other plugins
-            self::add_cache_menu_items($admin_bar);
-        }
-        $args = array('parent' => 'ilj', 'id' => 'ilj-status', 'title' => '<div class="ilj-build-title"><strong>Status:</strong> <span  id="ilj_batch_status">' . HelperBatchInfo::translateBatchStatus($status) . '</span></div>', 'meta' => array('html' => '
+                </a>',
+			),
+		);
+		$admin_bar->add_node($args);
+
+		$cache_option = Options::getOption(CacheToggleBtnSwitch::getKey());
+		if ($cache_option) {
+			// Disable in version 2.23.5 due to conflicts with other plugins
+			self::add_cache_menu_items($admin_bar);
+		}
+
+		$args = array(
+			'parent' => 'ilj',
+			'id'     => 'ilj-status',
+			'title'  => '<div class="ilj-build-title"><strong>Status:</strong> <span  id="ilj_batch_status">' . HelperBatchInfo::translateBatchStatus($status) . '</span></div>',
+			'meta'   => array(
+				'html' => '
 				<hr class="ilj-build-separate" />
                 <div class="ilj-build-info">
                 	<p>
@@ -51,29 +66,52 @@
                 		' . __('We build your internal links in the background.', 'internal-links') . ' ' . __('As soon as 100% is reached, your new links will be visible in the frontend.', 'internal-links') . '
                 	</p>
                 </div>
-                '));
-        $admin_bar->add_node($args);
-    }
-    /**
-     * Add admin bar menu items for deleting transient
-     *
-     * @param WP_Admin_Bar $admin_bar
-     *
-     * @return void
-     */
-    private static function add_cache_menu_items($admin_bar)
-    {
-        if (!current_user_can('manage_options')) {
-            return;
-        }
-        $args = array('parent' => 'ilj', 'id' => 'ilj-clear-all-transients', 'title' => __('Delete all caches', 'internal-links'), 'href' => wp_nonce_url(admin_url('admin-ajax.php?action=ilj_clear_all_transient'), 'ilj_clear_all_transient'));
-        $admin_bar->add_node($args);
-        // On single post/ category pages this option should be displayed.
-        if (is_category() || is_tag() || is_tax() || is_singular()) {
-            $id = get_queried_object_id();
-            $type = is_singular() ? 'post' : 'term';
-            $args = array('parent' => 'ilj', 'id' => 'ilj-clear-single-transient', 'title' => ('post' === $type) ? sprintf(__('Delete this %s cache', 'internal-links'), get_post_type($id)) : __('Delete this term cache', 'internal-links'), 'href' => admin_url(sprintf('admin-ajax.php?action=%s&_wpnonce=%s&ilj_transient_id=%d&ilj_transient_type=%s', 'ilj_clear_single_transient', wp_create_nonce('ilj_clear_single_transient'), $id, $type)));
-            $admin_bar->add_node($args);
-        }
-    }
-}
 No newline at end of file
+                ',
+			),
+		);
+
+
+		$admin_bar->add_node($args);
+
+	}
+
+	/**
+	 * Add admin bar menu items for deleting transient
+	 *
+	 * @param WP_Admin_Bar $admin_bar
+	 *
+	 * @return void
+	 */
+	 private static function add_cache_menu_items($admin_bar) {
+
+		 if (!current_user_can('manage_options')) {
+			 return;
+		 }
+
+		 $args = array(
+			 'parent' => 'ilj',
+			 'id'     => 'ilj-clear-all-transients',
+			 'title'  => __('Delete all caches', 'internal-links'),
+			 'href' => wp_nonce_url(admin_url('admin-ajax.php?action=ilj_clear_all_transient'), 'ilj_clear_all_transient')
+		 );
+
+		 $admin_bar->add_node($args);
+
+		 // On single post/ category pages this option should be displayed.
+		 if (is_category() || is_tag() || is_tax() || is_singular()) {
+			 $id = get_queried_object_id();
+			 $type = is_singular() ? 'post' : 'term';
+			 $args = array(
+				 'parent' => 'ilj',
+				 'id'     => 'ilj-clear-single-transient',
+				 /* translators: %s: Post Type */
+				 'title'  => 'post' === $type ? sprintf(__('Delete this %s cache', 'internal-links'), get_post_type($id)) : __('Delete this term cache', 'internal-links'),
+				 'href' => admin_url(sprintf('admin-ajax.php?action=%s&_wpnonce=%s&ilj_transient_id=%d&ilj_transient_type=%s', 'ilj_clear_single_transient', wp_create_nonce('ilj_clear_single_transient'), $id, $type)),
+			 );
+
+			 $admin_bar->add_node($args);
+		 }
+	 }
+
+
+}
--- a/internal-links/backend/adminmenu.php
+++ b/internal-links/backend/adminmenu.php
@@ -1,5 +1,4 @@
 <?php
-
 namespace ILJBackend;

 /**
@@ -10,34 +9,51 @@
  * @package ILJBackend
  * @since   1.0.0
  */
-class AdminMenu
-{
-    const ILJ_MENUPAGE_SLUG = 'internal_link_juicer';
-    /**
-     * Initializes the building process
-     *
-     * @since  1.0.0
-     * @return void
-     */
-    public static function init()
-    {
-        self::addMenuPage();
-        $submenus = array('ILJBackendMenuPageDashboard', 'ILJBackendMenuPageTools', 'ILJBackendMenuPageSettings', 'ILJBackendMenuPageTour');
-        foreach ($submenus as $submenu) {
-            $menu_page = new $submenu();
-            $menu_page->register();
-        }
-    }
-    /**
-     * Registers the menu page for the plugin
-     *
-     * @since  1.0.0
-     * @return void
-     */
-    private static function addMenuPage()
-    {
-        add_menu_page(__('Internal Links', 'internal-links'), __('Internal Links', 'internal-links'), 'manage_options', self::ILJ_MENUPAGE_SLUG, function () {
-            return;
-        }, 'data:image/svg+xml;base64,' . base64_encode('<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150"><path fill="#ffffff" d="M115.1334 73.9667c-4.5667-6.8334-9.5667-13.4-14.4334-20.0334-8.2-11.0333-13.5-23.1-12.8666-37.1.2333-5.4 1.2333-10.7333 1.9-16.3333L80.4 8.9333c-15.7666 14.9-29.9 31.1334-40.3666 50.3-6.7667 12.4334-11.5667 25.5-12.6667 39.8-2.0333 24.6667 16.9667 48.2 41.5667 51.0667 27.1333 3.2333 50.3667-14.4 54.3-41.3333 1.8666-12.5667-1.1334-24.3-8.1-34.8zm-15.9 40.9c-4.9667 0-9.2-3.3333-10.4334-7.9l-15.6666 2.6v.5c0 8.8334-7.2334 16.0667-16.1 16.0667-8.8334 0-16.0667-7.2333-16.0667-16.0667 0-8.8666 7.2333-16.1 16.0667-16.1 1.2 0 2.3333.1 3.5.4l7.6303-21.9204C64.5334 70.7666 62.0334 67.1 62.0334 62.8c0-6.0334 4.9-10.8667 10.9-10.8667 5.9666 0 10.9 4.8333 10.9 10.8667s-4.9334 10.8333-10.9 10.8333c-.8667 0-1.7334-.1-2.5667-.3333l-7.5333 21.7333C67.8 97 71.6334 101.3333 72.8 106.6l15.5667-2.5667c0-6.0667 4.8667-10.9 10.9-10.9 6 0 10.8333 4.8333 10.8333 10.9-.0333 6-4.8333 10.8333-10.8666 10.8333z"/></svg>'), 16);
-    }
-}
 No newline at end of file
+class AdminMenu {
+
+	const ILJ_MENUPAGE_SLUG = 'internal_link_juicer';
+
+	/**
+	 * Initializes the building process
+	 *
+	 * @since  1.0.0
+	 * @return void
+	 */
+	public static function init() {
+		 self::addMenuPage();
+
+		$submenus = array(
+			'ILJBackendMenuPageDashboard',
+			'ILJBackendMenuPageTools',
+			'ILJBackendMenuPageSettings',
+			'ILJBackendMenuPageTour',
+		);
+
+
+
+		foreach ($submenus as $submenu) {
+			$menu_page = new $submenu();
+			$menu_page->register();
+		}
+	}
+
+	/**
+	 * Registers the menu page for the plugin
+	 *
+	 * @since  1.0.0
+	 * @return void
+	 */
+	private static function addMenuPage() {
+		add_menu_page(
+			__('Internal Links', 'internal-links'),
+			__('Internal Links', 'internal-links'),
+			'manage_options',
+			self::ILJ_MENUPAGE_SLUG,
+			function () {
+				return;
+			},
+			'data:image/svg+xml;base64,' . base64_encode('<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150"><path fill="#ffffff" d="M115.1334 73.9667c-4.5667-6.8334-9.5667-13.4-14.4334-20.0334-8.2-11.0333-13.5-23.1-12.8666-37.1.2333-5.4 1.2333-10.7333 1.9-16.3333L80.4 8.9333c-15.7666 14.9-29.9 31.1334-40.3666 50.3-6.7667 12.4334-11.5667 25.5-12.6667 39.8-2.0333 24.6667 16.9667 48.2 41.5667 51.0667 27.1333 3.2333 50.3667-14.4 54.3-41.3333 1.8666-12.5667-1.1334-24.3-8.1-34.8zm-15.9 40.9c-4.9667 0-9.2-3.3333-10.4334-7.9l-15.6666 2.6v.5c0 8.8334-7.2334 16.0667-16.1 16.0667-8.8334 0-16.0667-7.2333-16.0667-16.0667 0-8.8666 7.2333-16.1 16.0667-16.1 1.2 0 2.3333.1 3.5.4l7.6303-21.9204C64.5334 70.7666 62.0334 67.1 62.0334 62.8c0-6.0334 4.9-10.8667 10.9-10.8667 5.9666 0 10.9 4.8333 10.9 10.8667s-4.9334 10.8333-10.9 10.8333c-.8667 0-1.7334-.1-2.5667-.3333l-7.5333 21.7333C67.8 97 71.6334 101.3333 72.8 106.6l15.5667-2.5667c0-6.0667 4.8667-10.9 10.9-10.9 6 0 10.8333 4.8333 10.8333 10.9-.0333 6-4.8333 10.8333-10.8666 10.8333z"/></svg>'),
+			16
+		);
+	}
+}
--- a/internal-links/backend/batchinfo.php
+++ b/internal-links/backend/batchinfo.php
@@ -1,78 +1,90 @@
 <?php
-
 namespace ILJBackend;

 use ILJCoreOptions;
+
 /**
  * Handles Plugin Batch Information
  *
  * @package ILJBackend
  * @since   2.0.0
  */
-class BatchInfo
-{
-    /**
-     * Batch info instance
-     *
-     * @var   BatchInfo
-     * @since 2.0.0
-     */
-    private static $instance;
-    /**
-     * Batch data
-     *
-     * @var   array
-     * @since 2.0.0
-     */
-    private $batch_data;
-    protected function __construct()
-    {
-        $batch_data_default = array('batch_build' => array('last_update' => array('batch_count' => '', 'batch_finished' => '', 'last_update' => '', 'status' => '')));
-        $batch_data = Options::getOption(Options::ILJ_OPTION_KEY_BATCH);
-        $this->batch_data = wp_parse_args($batch_data, $batch_data_default);
-    }
-    /**
-     * Get data from batch data
-     *
-     * @since  2.0.0
-     * @param  string $key The key
-     * @return string|bool
-     */
-    public static function get($key)
-    {
-        self::init();
-        $batch_data = self::$instance->batch_data;
-        if (array_key_exists($key, $batch_data)) {
-            return $batch_data[$key];
-        }
-        return false;
-    }
-    /**
-     * Update batch data
-     *
-     * @since  2.0.0
-     * @param  string $key   The key
-     * @param  mixed  $value The value
-     * @return bool
-     */
-    public static function update($key, $value)
-    {
-        self::init();
-        $batch_data = self::$instance->batch_data;
-        $batch_data[$key] = $value;
-        Options::setOption(Options::ILJ_OPTION_KEY_BATCH, $batch_data);
-        return true;
-    }
-    /**
-     * Init Environment- class
-     *
-     * @since  2.0.0
-     * @return void
-     */
-    private static function init()
-    {
-        if (null === self::$instance) {
-            self::$instance = new self();
-        }
-    }
-}
 No newline at end of file
+class BatchInfo {
+
+	/**
+	 * Batch info instance
+	 *
+	 * @var   BatchInfo
+	 * @since 2.0.0
+	 */
+	private static $instance;
+
+	/**
+	 * Batch data
+	 *
+	 * @var   array
+	 * @since 2.0.0
+	 */
+	private $batch_data;
+
+	protected function __construct() {
+		$batch_data_default = array(
+			'batch_build' => array(
+				'last_update' => array(
+					'batch_count'    => '',
+					'batch_finished' => '',
+					'last_update'    => '',
+					'status'         => '',
+				),
+			),
+		);
+
+		$batch_data       = Options::getOption(Options::ILJ_OPTION_KEY_BATCH);
+		$this->batch_data = wp_parse_args($batch_data, $batch_data_default);
+	}
+
+	/**
+	 * Get data from batch data
+	 *
+	 * @since  2.0.0
+	 * @param  string $key The key
+	 * @return string|bool
+	 */
+	public static function get($key) {
+		self::init();
+		$batch_data = self::$instance->batch_data;
+		if (array_key_exists($key, $batch_data)) {
+			return $batch_data[$key];
+		}
+		return false;
+	}
+
+	/**
+	 * Update batch data
+	 *
+	 * @since  2.0.0
+	 * @param  string $key   The key
+	 * @param  mixed  $value The value
+	 * @return bool
+	 */
+	public static function update($key, $value) {
+		 self::init();
+		$batch_data         = self::$instance->batch_data;
+		$batch_data[$key] = $value;
+		Options::setOption(Options::ILJ_OPTION_KEY_BATCH, $batch_data);
+
+		return true;
+	}
+
+	/**
+	 * Init Environment- class
+	 *
+	 * @since  2.0.0
+	 * @return void
+	 */
+	private static function init() {
+		if (null === self::$instance) {
+			self::$instance = new self();
+		}
+	}
+}
--- a/internal-links/backend/column.php
+++ b/internal-links/backend/column.php
@@ -3,6 +3,7 @@
 namespace ILJBackend;

 use ILJDatabasePostmeta;
+
 /**
  * Listview columns
  *
@@ -12,118 +13,161 @@
  *
  * @since 1.1.3
  */
-class Column
-{
-    const ILJ_COLUMN_CONFIGURED_LINKS = 'ilj_column_configured_links';
-    /**
-     * Returns the title for the configured links column in the frontend
-     *
-     * @return string
-     * @since  1.1.3
-     */
-    protected static function getConfiguredLinksColumnTitle()
-    {
-        return __('Configured keywords for internal linking', 'internal-links');
-    }
-    /**
-     * Generates and adds all possible configured links columns
-     *
-     * @return void
-     * @since  1.1.3
-     */
-    public static function addConfiguredLinksColumn()
-    {
-        $types = get_post_types(array('public' => true));
-        foreach ($types as $type) {
-            add_action('manage_' . $type . '_posts_custom_column', array('ILJBackendColumn', 'addConfiguredLinksColumnContent'), 5, 2);
-            add_filter('manage_' . $type . '_posts_columns', array('ILJBackendColumn', 'addConfiguredLinksColumnHeader'));
-            add_filter('manage_edit-' . $type . '_sortable_columns', array('ILJBackendColumn', 'addConfiguredLinksColumnSorter'));
-            add_filter('wp', array('ILJBackendColumn', 'sortConfiguredLinksColumn'));
-        }
-    }
-    /**
-     * Adds the configured links column header
-     *
-     * @param array $columns All columns header
-     *
-     * @return array
-     * @since  1.1.3
-     */
-    public static function addConfiguredLinksColumnHeader($columns)
-    {
-        ILJHelperLoader::enqueue_style('ilj_ui', ILJ_URL . 'admin/css/ilj_ui.css', array(), ILJ_VERSION);
-        $columns[self::ILJ_COLUMN_CONFIGURED_LINKS] = '<span class="icon icon-ilj" title="' . self::getConfiguredLinksColumnTitle() . '"></span><span class="screen-reader-text">' . self::getConfiguredLinksColumnTitle() . '</span>';
-        return $columns;
-    }
-    /**
-     * Outputs the content of the configured links column
-     *
-     * @param string $column  The current column
-     * @param int    $post_id Post ID
-     *
-     * @return void
-     * @since  1.1.3
-     */
-    public static function addConfiguredLinksColumnContent($column, $post_id)
-    {
-        if (self::ILJ_COLUMN_CONFIGURED_LINKS === $column) {
-            $data = get_post_meta($post_id, Postmeta::ILJ_META_KEY_LINKDEFINITION);
-            if (!is_array($data) && !is_object($data)) {
-                echo '0';
-                return;
-            }
-            if (empty($data) || is_array($data) && !is_array($data[0])) {
-                echo '0';
-                return;
-            }
-            echo count($data[0]);
-        }
-    }
-    /**
-     * Adds the sorter to the configured links column
-     *
-     * @param array $columns All sortable columns
-     *
-     * @return array
-     * @since  1.1.3
-     */
-    public static function addConfiguredLinksColumnSorter($columns)
-    {
-        $columns[self::ILJ_COLUMN_CONFIGURED_LINKS] = self::ILJ_COLUMN_CONFIGURED_LINKS;
-        return $columns;
-    }
-    /**
-     * Adds the post sorting logic for configured links column
-     *
-     * @return void
-     * @since  1.1.3
-     */
-    public static function sortConfiguredLinksColumn()
-    {
-        global $wp_query;
-        if (!is_admin()) {
-            return;
-        }
-        $orderby = $wp_query->get('orderby');
-        if (self::ILJ_COLUMN_CONFIGURED_LINKS != $orderby) {
-            return;
-        }
-        $page_offset = $wp_query->query_vars['paged'] ? $wp_query->query_vars['paged'] : 1;
-        $posts_per_page = $wp_query->query_vars['posts_per_page'];
-        $order = (isset($wp_query->query_vars) && isset($wp_query->query_vars['order']) && strcasecmp($wp_query->query_vars['order'], 'desc') == 0) ? 'DESC' : 'ASC';
-        $args = $wp_query->query;
-        $args['posts_per_page'] = -1;
-        $new_query = new WP_Query($args);
-        $posts = $new_query->posts;
-        usort($posts, function ($a, $b) use ($order) {
-            $keywords_a = get_post_meta($a->ID, Postmeta::ILJ_META_KEY_LINKDEFINITION);
-            $keywords_b = get_post_meta($b->ID, Postmeta::ILJ_META_KEY_LINKDEFINITION);
-            $count_a = count($keywords_a) ? count($keywords_a[0]) : 0;
-            $count_b = count($keywords_b) ? count($keywords_b[0]) : 0;
-            $sorting_value = (('DESC' == $order) ? $count_a > $count_b : ($count_a < $count_b)) ? -1 : (($count_a == $count_b) ? 0 : 1);
-            return $sorting_value;
-        });
-        $sliced = array_slice($posts, ($page_offset - 1) * $posts_per_page, $posts_per_page);
-        $wp_query->posts = $sliced;
-    }
-}
 No newline at end of file
+class Column {
+
+
+	const ILJ_COLUMN_CONFIGURED_LINKS = 'ilj_column_configured_links';
+
+	/**
+	 * Returns the title for the configured links column in the frontend
+	 *
+	 * @return string
+	 * @since  1.1.3
+	 */
+	protected static function getConfiguredLinksColumnTitle() {
+		 return __('Configured keywords for internal linking', 'internal-links');
+	}
+
+	/**
+	 * Generates and adds all possible configured links columns
+	 *
+	 * @return void
+	 * @since  1.1.3
+	 */
+	public static function addConfiguredLinksColumn() {
+		 $types = get_post_types(array('public' => true));
+
+
+
+		foreach ($types as $type) {
+			add_action(
+				'manage_' . $type . '_posts_custom_column',
+				array(
+					'ILJBackendColumn',
+					'addConfiguredLinksColumnContent',
+				),
+				5,
+				2
+			);
+			add_filter(
+				'manage_' . $type . '_posts_columns',
+				array(
+					'ILJBackendColumn',
+					'addConfiguredLinksColumnHeader',
+				)
+			);
+			add_filter(
+				'manage_edit-' . $type . '_sortable_columns',
+				array(
+					'ILJBackendColumn',
+					'addConfiguredLinksColumnSorter',
+				)
+			);
+			add_filter('wp', array('ILJBackendColumn', 'sortConfiguredLinksColumn'));
+		}
+	}
+
+	/**
+	 * Adds the configured links column header
+	 *
+	 * @param array $columns All columns header
+	 *
+	 * @return array
+	 * @since  1.1.3
+	 */
+	public static function addConfiguredLinksColumnHeader($columns) {
+		ILJHelperLoader::enqueue_style('ilj_ui', ILJ_URL . 'admin/css/ilj_ui.css', array(), ILJ_VERSION);
+
+		$columns[self::ILJ_COLUMN_CONFIGURED_LINKS] = '<span class="icon icon-ilj" title="' . self::getConfiguredLinksColumnTitle() . '"></span><span class="screen-reader-text">' . self::getConfiguredLinksColumnTitle() . '</span>';
+
+		return $columns;
+	}
+
+	/**
+	 * Outputs the content of the configured links column
+	 *
+	 * @param string $column  The current column
+	 * @param int    $post_id Post ID
+	 *
+	 * @return void
+	 * @since  1.1.3
+	 */
+	public static function addConfiguredLinksColumnContent($column, $post_id) {
+		if (self::ILJ_COLUMN_CONFIGURED_LINKS === $column) {
+			$data = get_post_meta($post_id, Postmeta::ILJ_META_KEY_LINKDEFINITION);
+
+			if (!is_array($data) && !is_object($data)) {
+				echo '0';
+				return;
+			}
+
+			if (empty($data) || (is_array($data) && !is_array($data[0]))) {
+				echo '0';
+				return;
+			}
+			echo count($data[0]);
+		}
+	}
+
+	/**
+	 * Adds the sorter to the configured links column
+	 *
+	 * @param array $columns All sortable columns
+	 *
+	 * @return array
+	 * @since  1.1.3
+	 */
+	public static function addConfiguredLinksColumnSorter($columns) {
+		 $columns[self::ILJ_COLUMN_CONFIGURED_LINKS] = self::ILJ_COLUMN_CONFIGURED_LINKS;
+
+		return $columns;
+	}
+
+	/**
+	 * Adds the post sorting logic for configured links column
+	 *
+	 * @return void
+	 * @since  1.1.3
+	 */
+	public static function sortConfiguredLinksColumn() {
+		global $wp_query;
+
+		if (!is_admin()) {
+			return;
+		}
+
+		$orderby = $wp_query->get('orderby');
+
+		if (self::ILJ_COLUMN_CONFIGURED_LINKS != $orderby) {
+			return;
+		}
+
+		$page_offset    = ($wp_query->query_vars['paged']) ? $wp_query->query_vars['paged'] : 1;
+		$posts_per_page = $wp_query->query_vars['posts_per_page'];
+		$order          = isset($wp_query->query_vars) && isset($wp_query->query_vars['order']) && strcasecmp($wp_query->query_vars['order'], 'desc') == 0 ? 'DESC' : 'ASC';
+
+		$args                   = $wp_query->query;
+		$args['posts_per_page'] = -1;
+
+		$new_query = new WP_Query($args);
+		$posts     = $new_query->posts;
+
+		usort(
+			$posts,
+			function ($a, $b) use ($order) {
+				$keywords_a    = get_post_meta($a->ID, Postmeta::ILJ_META_KEY_LINKDEFINITION);
+				$keywords_b    = get_post_meta($b->ID, Postmeta::ILJ_META_KEY_LINKDEFINITION);
+				$count_a       = count($keywords_a) ? count($keywords_a[0]) : 0;
+				$count_b       = count($keywords_b) ? count($keywords_b[0]) : 0;
+				$sorting_value = ('DESC' == $order ? ($count_a > $count_b) : ($count_a < $count_b)) ? - 1 : ($count_a == $count_b ? 0 : 1);
+
+				return $sorting_value;
+			}
+		);
+
+		$sliced          = array_slice($posts, ($page_offset - 1) * $posts_per_page, $posts_per_page);
+		$wp_query->posts = $sliced;
+	}
+
+
+}
--- a/internal-links/backend/editor.php
+++ b/internal-links/backend/editor.php
@@ -1,15 +1,18 @@
 <?php
-
 namespace ILJBackend;

 use ILJCoreOptions;
 use ILJCoreOptionsCacheToggleBtnSwitch;
 use ILJDataContent;
+use ILJDatabaseLinkindex;
 use ILJHelperHelp;
 use ILJTypeKeywordList;
 use ILJDatabasePostmeta;
+use ILJDatabaseTermmeta;
 use ILJHelperCapabilities;
 use ILJHelperIndexAsset as IndexAsset;
+use ILJHelperStatistic;
+
 /**
  * Admin views
  *
@@ -19,358 +22,525 @@
  *
  * @since 1.0.0
  */
-class Editor
-{
-    const ILJ_ADMINVIEW_NONCE = '_ilj_adminview_nonce';
-    const ILJ_ACTION_AFTER_KEYWORDS_UPDATE = 'ilj_after_keywords_update';
-    const ILJ_EDITOR_HANDLE = 'ilj_editor';
-    const ILJ_KEYWORDS_HANDLE = 'ilj_keywords';
-    const ILJ_IS_BLACKLISTED = 'ilj_is_blacklisted';
-    const ILJ_META_KEY_LIMITINCOMINGLINKS = 'ilj_limitincominglinks';
-    const ILJ_META_KEY_MAXINCOMINGLINKS = 'ilj_maxincominglinks';
-    const ILJ_META_KEY_BLACKLISTDEFINITION = 'ilj_blacklistdefinition';
-    const ILJ_META_KEY_LIMITLINKSPERPARAGRAPH = 'ilj_limitlinksperparagraph';
-    const ILJ_META_KEY_LINKSPERPARAGRAPH = 'ilj_linksperparagraph';
-    const ILJ_META_KEY_LIMITOUTGOINGLINKS = 'ilj_limitoutgoinglinks';
-    const ILJ_META_KEY_MAXOUTGOINGLINKS = 'ilj_maxoutgoinglinks';
-    const ILJ_KEYWORD_METABOX_FOOTER_HOOK = 'ilj_keyword_metabox_footer';
-    /**
-     * Registers the keyword metabox on all public post types
-     *
-     * @since 1.0.0
-     *
-     * @return void
-     */
-    public static function addKeywordMetaBox()
-    {
-        foreach (get_post_types(array('public' => true)) as $type) {
-            add_meta_box(Postmeta::ILJ_META_KEY_LINKDEFINITION, __('Internal Links', 'internal-links'), array(__CLASS__, 'renderKeywordMetaBox'), $type, 'side', 'default');
-        }
-    }
-    /**
-     * Renders the keyword metabox
-     *
-     * @since 1.0.0
-     *
-     * @param  WP_Post $post The post object of the current page
-     * @return void
-     */
-    public static function renderKeywordMetaBox(WP_Post $post)
-    {
-        $keyword_list = KeywordList::fromMeta($post->ID, 'post', Postmeta::ILJ_META_KEY_LINKDEFINITION);
-        $blacklist_keywords = KeywordList::fromMeta($post->ID, 'post', self::ILJ_META_KEY_BLACKLISTDEFINITION);
-        wp_nonce_field(basename(__FILE__), self::ILJ_ADMINVIEW_NONCE);
-        ?>
+class Editor {
+
+	const ILJ_ADMINVIEW_NONCE                 = '_ilj_adminview_nonce';
+	const ILJ_ACTION_AFTER_KEYWORDS_UPDATE    = 'ilj_after_keywords_update';
+	const ILJ_EDITOR_HANDLE                   = 'ilj_editor';
+	const ILJ_KEYWORDS_HANDLE                 = 'ilj_keywords';
+	const ILJ_IS_BLACKLISTED                  = 'ilj_is_blacklisted';
+	const ILJ_META_KEY_LIMITINCOMINGLINKS     = 'ilj_limitincominglinks';
+	const ILJ_META_KEY_MAXINCOMINGLINKS       = 'ilj_maxincominglinks';
+	const ILJ_META_KEY_BLACKLISTDEFINITION    = 'ilj_blacklistdefinition';
+	const ILJ_META_KEY_LIMITLINKSPERPARAGRAPH = 'ilj_limitlinksperparagraph';
+	const ILJ_META_KEY_LINKSPERPARAGRAPH      = 'ilj_linksperparagraph';
+	const ILJ_META_KEY_LIMITOUTGOINGLINKS     = 'ilj_limitoutgoinglinks';
+	const ILJ_META_KEY_MAXOUTGOINGLINKS       = 'ilj_maxoutgoinglinks';
+	const ILJ_KEYWORD_METABOX_FOOTER_HOOK     = 'ilj_keyword_metabox_footer';
+	const ILJ_MODAL = 'ilj_modal';
+
+	/**
+	 * Registers the keyword metabox on all public post types
+	 *
+	 * @since 1.0.0
+	 *
+	 * @return void
+	 */
+	public static function addKeywordMetaBox() {
+		foreach (get_post_types(array('public' => true)) as $type) {
+			add_meta_box(
+				Postmeta::ILJ_META_KEY_LINKDEFINITION,
+				__('Internal Links', 'internal-links'),
+				array(__CLASS__, 'renderKeywordMetaBox'),
+				$type,
+				'side',
+				'default'
+			);
+		}
+
+
+	}
+
+	/**
+	 * Renders the keyword metabox
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param  WP_Post $post The post object of the current page
+	 * @return void
+	 */
+	public static function renderKeywordMetaBox(WP_Post $post) {
+		$keyword_list      = KeywordList::fromMeta($post->ID, 'post', Postmeta::ILJ_META_KEY_LINKDEFINITION);
+		$blacklist_keywords = KeywordList::fromMeta($post->ID, 'post', self::ILJ_META_KEY_BLACKLISTDEFINITION);
+
+		wp_nonce_field(basename(__FILE__), self::ILJ_ADMINVIEW_NONCE);
+		?>
 		<p>
-		<label for="<?php
-        echo esc_attr(Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys');
-        ?>"><?php
-        esc_html_e('The keywords', 'internal-links');
-        ?>:</label>
+		<label for="<?php echo esc_attr(Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys'); ?>"><?php esc_html_e('The keywords', 'internal-links'); ?>:</label>
 		<br />
 		<input
 				type="text"
-				name="<?php
-        echo esc_attr(Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys');
-        ?>"
-				value="<?php
-        echo esc_attr($keyword_list->encoded());
-        ?>" size="30"/>
-		<?php
-        ?>
+				name="<?php echo esc_attr(Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys'); ?>"
+				value="<?php echo esc_attr($keyword_list->encoded()); ?>" size="30"/>
+		<?php
+
+		?>
 			<input type="text"
-				   name="<?php
-        echo esc_attr(self::ILJ_IS_BLACKLISTED);
-        ?>"
-				   value="<?php
-        echo esc_attr(self::isBlacklisted($post->ID, 'post'));
-        ?>"
+				   name="<?php echo esc_attr(self::ILJ_IS_BLACKLISTED); ?>"
+				   value="<?php echo esc_attr(self::isBlacklisted($post->ID, 'post')); ?>"
 				   style="display:none"/>

 			<input type="text"
-				   name="<?php
-        echo esc_attr(self::ILJ_META_KEY_BLACKLISTDEFINITION);
-        ?>"
-				   value="<?php
-        echo esc_attr($blacklist_keywords->encoded());
-        ?>"
+				   name="<?php echo esc_attr(self::ILJ_META_KEY_BLACKLISTDEFINITION); ?>"
+				   value="<?php echo esc_attr($blacklist_keywords->encoded()); ?>"
 				   size="30"
 				   style="display:none"/>
 					</p>
 		<template id="ilj_keyword_metabox_footer">
-			<?php
-        $content = Content::from_post($post);
-        /**
-         * This action is used to render content dynamically in to metabox footer.
-         * The HTML content inside <template> tag will rendered at the footer of editor.
-         *
-         * @param $content Content|null The content, null if its rendered on add post or add term page.
-         * @since 2.23.5
-         */
-        do_action(self::ILJ_KEYWORD_METABOX_FOOTER_HOOK, $content);
-        ?>
+			<?php
+				$content = Content::from_post($post);
+				/**
+				 * This action is used to render content dynamically in to metabox footer.
+				 * The HTML content inside <template> tag will rendered at the footer of editor.
+				 *
+				 * @param $content Content|null The content, null if its rendered on add post or add term page.
+				 * @since 2.23.5
+				 */
+				do_action(self::ILJ_KEYWORD_METABOX_FOOTER_HOOK, $content);
+			?>
 		</template>
-		<?php
-    }
-    /**
-     * Hooks in to {@link self::ILJ_KEYWORD_METABOX_FOOTER_HOOK} to add custom html to <template> tag
-     *
-     * @param Content|null $content The content, null if its rendered on add post or add term page.
-     * @return void
-     */
-    public static function render_keyword_metabox_footer($content)
-    {
-        ?>
+		<?php
+	}
+
+	/**
+	 * Hooks in to {@link self::ILJ_KEYWORD_METABOX_FOOTER_HOOK} to add custom html to <template> tag
+	 *
+	 * @param Content|null $content The content, null if its rendered on add post or add term page.
+	 * @return void
+	 */
+	public static function render_keyword_metabox_footer($content) {
+		?>
 		<div class="ilj-row">
 			<div class="col-12 ilj-footer">
 				<a href="https://internallinkjuicer.com/docs/editor/?utm_source=editor&utm_medium=help&utm_campaign=plugin"
 				   rel="noopener" target="_blank" class="help">
 					<span class="dashicons dashicons-editor-help"></span>
-					<?php
-        esc_html_e('Get help', 'internal-links');
-        ?>
+					<?php esc_html_e('Get help', 'internal-links'); ?>
 				</a>
-				<?php
-        self::render_delete_cache_button($content);
-        ?>
+				<?php self::render_delete_cache_button($content); ?>
 			</div>
 		</div>
-		<?php
-    }
-    /**
-     * Renders delete cache button inside metabox.
-     *
-     * @param Content|null $content The content, null if its rendered on add post or add term page.
-     * @return void
-     */
-    private static function render_delete_cache_button($content)
-    {
-        if (!$content) {
-            return;
-        }
-        $link = admin_url(sprintf('admin-ajax.php?action=%s&_wpnonce=%s&ilj_transient_id=%d&ilj_transient_type=%s&ilj_skip_notice=true', 'ilj_clear_single_transient', wp_create_nonce('ilj_clear_single_transient'), $content->get_id(), $content->get_type()));
-        $cache_option = Options::getOption(CacheToggleBtnSwitch::getKey());
-        if ($cache_option) {
-            ?>
+		<?php
+	}
+
+	/**
+	 * Renders delete cache button inside metabox.
+	 *
+	 * @param Content|null $content The content, null if its rendered on add post or add term page.
+	 * @return void
+	 */
+	private static function render_delete_cache_button($content) {
+		if (!$content) {
+			return;
+		}
+		$link = admin_url(
+			sprintf( 'admin-ajax.php?action=%s&_wpnonce=%s&ilj_transient_id=%d&ilj_transient_type=%s&ilj_skip_notice=true',
+				'ilj_clear_single_transient',
+				wp_create_nonce('ilj_clear_single_transient'),
+				$content->get_id(),
+				$content->get_type()
+			)
+		);
+		$cache_option = Options::getOption(CacheToggleBtnSwitch::getKey());
+		if ($cache_option) {
+		?>
 		<div class="ilj-delete-cache-container">
-			<button class="ilj-button-danger button" id="ilj-delete-cache" type="button" data-ilj-delete-cache-url="<?php
-            echo esc_attr($link);
-            ?>">
-				<?php
-            esc_html_e('Delete Cache', 'internal-links');
-            ?>
+			<button class="ilj-button-danger button" id="ilj-delete-cache" type="button" data-ilj-delete-cache-url="<?php echo esc_attr($link); ?>">
+				<?php esc_html_e('Delete Cache', 'internal-links'); ?>
 			</button>
 			<span class="spinner is-active ilj-spinner ilj-hidden"></span>
 		</div>
-		<?php
-        }
-    }
-    /**
-     * Responsible for saving keyword meta values and
-     * stores limit linking settings for posts and
-     * stores limit linking settings for posts
-     *
-     * @since 1.0.0
-     *
-     * @param  int      $post_id The ID of the post
-     * @param  WP_Post $post    The post object
-     * @return void
-     */
-    public static function saveKeywordMeta($post_id, WP_Post $post)
-    {
-        if (is_null($post_id) || is_null($post)) {
-            return;
-        }
-        if (!isset($_POST[self::ILJ_ADMINVIEW_NONCE]) || !wp_verify_nonce($_POST[self::ILJ_ADMINVIEW_NONCE], basename(__FILE__))) {
-            return $post_id;
-        }
-        $post_type = get_post_type_object($post->post_type);
-        if (!current_user_can($post_type->cap->edit_post, $post_id)) {
-            return $post_id;
-        }
-        if (array_key_exists(self::ILJ_META_KEY_BLACKLISTDEFINITION, $_POST)) {
-            $input_blacklist = stripslashes($_POST[self::ILJ_META_KEY_BLACKLISTDEFINITION]);
-            $sanitized_blacklist_meta_value = sanitize_text_field($input_blacklist);
-            $keywordsblacklist = KeywordList::fromInput($sanitized_blacklist_meta_value);
-            update_post_meta($post_id, self::ILJ_META_KEY_BLACKLISTDEFINITION, array_slice($keywordsblacklist->getKeywords(), 0, 2));
-        }
-        if (array_key_exists(self::ILJ_IS_BLACKLISTED, $_POST) && true == $_POST[self::ILJ_IS_BLACKLISTED]) {
-            self::addToBlacklist($post_id, 'post');
-        } else {
-            self::removeFromBlacklist($post_id, 'post');
-        }
-        if (array_key_exists(Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys', $_POST)) {
-            $input = stripslashes($_POST[Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys']);
-            $sanitized_meta_value = sanitize_text_field($input);
-            $keywords = KeywordList::fromInput($sanitized_meta_value);
-            $update_status = self::set_keywords($post_id, $keywords);
-            /**
-             * Fires after keyword meta got saved
-             *
-             * @since 1.0.0
-             */
-            if (true == $update_status) {
-                do_action(self::ILJ_ACTION_AFTER_KEYWORDS_UPDATE, $_POST['post_ID'], 'post', $_POST['post_status']);
-            }
-        }
-    }
-    /**
-     * Set keywords for a post id, returns boolean or int based on the value.
-     *
-     * @param int         $post_id  The post id.
-     * @param KeywordList $keywords The keyword list for post.
-     *
-     * @return bool|int
-     */
-    public static function set_keywords($post_id, $keywords)
-    {
-        $prev_value = get_post_meta($post_id, Postmeta::ILJ_META_KEY_LINKDEFINITION, true);
-        return update_post_meta($post_id, Postmeta::ILJ_META_KEY_LINKDEFINITION, $keywords->getKeywords(), $prev_value);
-    }
-    /**
-     * Logic for adding the assets based on subscription
-     *
-     * @since 1.1.0
-     *
-     * @return void
-     */
-    public static function addAssets()
-    {
-        $required_role = 'administrator';
-        if (!current_user_can($required_role)) {
-            return;
-        }
-        global $pagenow;
-        if (in_array($pagenow, array('post-new.php', 'post.php'))) {
-            if (!isset($_GET['post_type'])) {
-                self::registerAssets();
-                return;
-            }
-            $post_type = get_post_type_object($_GET['post_type']);
-            if (!$post_type || !$post_type->public) {
-                return;
-            }
-            self::registerAssets();
-        }
-    }
-    /**
-     * Registering the assets for editor frontend
-     *
-     * @since 1.1.0
-     *
-     * @return void
-     */
-    private static function registerAssets()
-    {
-        add_action('admin_enqueue_scripts', function () {
-            ILJHelperLoader::enqueue_script('jquery-ui-sortable');
-            ILJHelperLoader::register_script(Editor::ILJ_KEYWORDS_HANDLE, ILJ_URL . 'admin/js/ilj_keywords.js', array(), ILJ_VERSION);
-            ILJHelperLoader::register_script(Editor::ILJ_EDITOR_HANDLE, ILJ_URL . 'admin/js/ilj_editor.js', array(), ILJ_VERSION);
-            wp_localize_script(Editor::ILJ_EDITOR_HANDLE, 'ilj_editor_translation', Editor::getTranslation());
-            wp_add_inline_script(Editor::ILJ_EDITOR_HANDLE, 'const ilj_editor_basic_restriction = ' . json_encode(Editor::getBasicRestrictions()), 'before');
-            ILJHelperLoader::enqueue_script('ilj_tipso', ILJ_URL . 'admin/js/tipso.js', array(), ILJ_VERSION);
-            ILJHelperLoader::enqueue_script(Editor::ILJ_KEYWORDS_HANDLE);
-            ILJHelperLoader::enqueue_script(Editor::ILJ_EDITOR_HANDLE);
-            ILJHelperLoader::enqueue_style('ilj_tipso', ILJ_URL . 'admin/css/tipso.css', array(), ILJ_VERSION);
-            ILJHelperLoader::enqueue_style(Editor::ILJ_EDITOR_HANDLE, ILJ_URL . 'admin/css/ilj_editor.css', array(), ILJ_VERSION);
-            ILJHelperLoader::enqueue_style('ilj_ui', ILJ_URL . 'admin/css/ilj_ui.css', array(), ILJ_VERSION);
-        }, 10, 1);
-    }
-    /**
-     * Returns the frontend translation
-     *
-     * @since 1.0.1
-     *
-     * @return array
-     */
-    public static function getTranslation()
-    {
-        $translation = array('add_keyword' => __('Add Keyword', 'internal-links'), 'placeholder_keyword' => __('Keyword', 'internal-links'), 'howto_case' => __('Keywords get used <strong>case insensitive</strong>', 'internal-links'), 'howto_keyword' => __('Separate multiple keywords by commas', 'internal-links'), 'howto_gap' => __('Configure the gap dimension.', 'internal-links') . ' ' . __('It represents the number of keywords that appear between your other keywords dynamically.', 'internal-links') . ' ' . __('Learn more in our documentation:', 'internal-links') . '<br><strong><a target="_blank" rel="noopener" href="' . Help::getLinkUrl('editor/', 'gaps', 'gap help', 'editor') . '">' . __('Click here to open', 'internal-links') . '</a></strong>', 'headline_gaps' => __('Keyword gaps', 'internal-links'), 'add_gap' => __('Add gap', 'internal-links'), 'gap_type' => __('Gap type', 'internal-links'), 'type_min' => __('Minimum', 'internal-links'), 'type_exact' => __('Exact', 'internal-links'), 'type_max' => __('Maximum', 'internal-links'), 'howto_gap_min' => __('Minimum amount of keywords within the gap.', 'internal-links') . ' ' . __('No upper limits.', 'internal-links'), 'howto_gap_exact' => __('Exact amount of keywords within the gap.', 'internal-links'), 'howto_gap_max' => __('Maximum amount of keywords within the gap.', 'internal-links'), 'howto_links_per_paragraph' => __('Overrides the general setting for the current asset.', 'internal-links'), 'howto_add_to_blacklist' => __('Toggle to add/remove from global blacklist.', 'internal-links'), 'howto_limit_incoming_links' => __('Toggle to add/remove incoming links limit.', 'internal-links'), 'howto_limit_outgoing_links' => __('Toggle to add/remove outgoing links limit.', 'internal-links'), 'insert_gaps' => __('Insert gaps between keywords', 'internal-links'), 'headline_configured_keywords' => __('Configured keywords', 'internal-links'), 'message_keyword_exists' => __('This keyword already exists.', 'internal-links'), 'message_no_keyword' => __('No keyword defined.', 'internal-links'), 'message_length_not_valid' => __('Length of given keyword not valid.', 'internal-links'), 'message_multiple_placeholder' => __('Multiple consecutive placeholders are not allowed.', 'internal-links'), 'no_keywords' => __('No keywords configured.', 'internal-links'), 'gap_hover_exact' => __('Exact keyword gap:', 'internal-links'), 'gap_hover_max' => __('Maximum keyword gap:', 'internal-links'), 'gap_hover_min' => __('Minimum keyword gap:', 'internal-links'), 'limit_incoming_links' => __('Limit incoming Links:', 'internal-links'), 'max_incoming_links' => __('Maximum incoming links:', 'internal-links'), 'blacklist_incoming_links' => __('Keywords, that don`t get linked in the current content:', 'internal-links'), 'message_limited_blacklist_keyword' => __('With the free Basic version of the Iternal Link Juicer, you can block 2 keywords from being linked.', 'internal-links'), 'message_limited_blacklist_keyword_upgrade' => sprintf('» <a href="%s">', get_admin_url(null, 'admin.php?page=' . AdminMenu::ILJ_MENUPAGE_SLUG . '-pricing')) . __('Upgrade to Pro and add unlimited keywords', 'internal-links') . '</a>', 'headline_configured_keywords_blacklist' => __('Configured keyword blacklist:', 'internal-links'), 'is_blacklisted' => __('Is on global blacklist:', 'internal-links'), 'limit_links_per_paragraph' => __('Limit links per paragraph:', 'internal-links'), 'max_links_per_paragraph' => __('Maximum links per paragraph:', 'internal-links'), 'limit_outgoing_links' => __('Limit outgoing Links:', 'internal-links'), 'max_outgoing_links' => __('Maximum outgoing links:', 'internal-links'), 'cache_cleared' => __('Cache cleared.', 'internal-links'), 'upgrade_to_pro_button_text' => __('Upgrade to Pro', 'internal-links'), 'upgrade_to_pro_link' => get_admin_url(null, 'admin.php?billing_cycle=annual&trial=true&page=' . AdminMenu::ILJ_MENUPAGE_SLUG . '-pricing'), 'pro_feature_title' => __('This is a PRO Feature.', 'internal-links'));
-        return $translation;
-    }
-    /**
-     * Sets Basic version restrictions
-     *
-     * @version 1.2.15
-     *
-     * @return array
-     */
-    protected static function getBasicRestrictions()
-    {
-        $current_screen = get_current_screen();
-        $basic_restrictions = array('blacklist_limit' => 2, 'is_active' => true, 'disable_title' => 'class="pro-title"', 'disable_setting' => 'pro-setting', 'disabled' => 'disabled', 'lock_icon' => '<span class="dashicons dashicons-lock tip" title="' . __('This feature is part of the Pro version', 'internal-links') . '"></span>', 'current_screen' => $current_screen->post_type);
-        return $basic_restrictions;
-    }
-    /**
-     * Checks if an asset is on the blacklist
-     *
-     * @since 1.2.15
-     *
-     * @param  int    $id   The asset ID
-     * @param  string $type The asset type
-     * @return bool True if blacklisted , false if not
-     */
-    public static function isBlacklisted($id, $type)
-    {
-        if ('post' == $type) {
-            $postBlacklist = Options::getOption(ILJCoreOptionsBlacklist::getKey());
-            $blacklisted = false;
-            if (is_array($postBlacklist)) {
-                if (in_array($id, $postBlacklist)) {
-                    $blacklisted = true;
-                }
-            }
-            return $blacklisted;
-        }
-        return false;
-    }
-    /**
-     * Removes an asset from blacklist
-     *
-     * @since 1.2.15
-     *
-     * @param  int    $id   The asset id
-     * @param  string $type The asset type
-     * @return void
-     */
-    protected static function removeFromBlacklist($id, $type)
-    {
-        $blacklist = array();
-        if ('post' == $type) {
-            $blacklist = Options::getOption(ILJCoreOptionsBlacklist::getKey());
-        }
-        $blacklist = is_array($blacklist) ? $blacklist : array();
-        if (($key = array_search($id, $blacklist)) !== false) {
-            unset($blacklist[$key]);
-        } else {
-            return;
-        }
-        if ('post' == $type) {
-            Options::setOption(ILJCoreOptionsBlacklist::getKey(), $blacklist);
-        }
-    }
-    /**
-     * Adds ID to Blacklist Option of post/terms
-     *
-     * @since 1.2.15
-     *
-     * @param  int    $id   The asset id
-     * @param  string $type The asset type
-     * @return void
-     */
-    protected static function addToBlacklist($id, $type)
-    {
-        $blacklist = array();
-        if ('post' == $type) {
-            $blacklist = Options::getOption(ILJCoreOptionsBlacklist::getKey());
-        }
-        $blacklist = is_array($blacklist) ? $blacklist : array();
-        if (in_array($id, $blacklist)) {
-            return;
-        }
-        $blacklist[] = $id;
-        if ('post' == $type) {
-            Options::setOption(ILJCoreOptionsBlacklist::getKey(), $blacklist);
-        }
-    }
-}
 No newline at end of file
+		<?php
+		}
+	}
+
+	/**
+	 * Responsible for saving keyword meta values and
+	 * stores limit linking settings for posts and
+	 * stores limit linking settings for posts
+	 *
+	 * @since 1.0.0
+	 *
+	 * @param  int      $post_id The ID of the post
+	 * @param  WP_Post $post    The post object
+	 * @return void
+	 */
+	public static function saveKeywordMeta($post_id, WP_Post $post) {
+		if (is_null($post_id) || is_null($post)) {
+			return;
+		}
+
+		if (!isset($_POST[self::ILJ_ADMINVIEW_NONCE]) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::ILJ_ADMINVIEW_NONCE])), basename(__FILE__))
+		) {
+			return $post_id;
+		}
+
+		$post_type = get_post_type_object($post->post_type);
+
+		if (!current_user_can($post_type->cap->edit_post, $post_id)) {
+			return $post_id;
+		}
+
+
+
+		if (array_key_exists(self::ILJ_META_KEY_BLACKLISTDEFINITION, $_POST)) {
+			$sanitized_blacklist_meta_value = sanitize_text_field(wp_unslash($_POST[self::ILJ_META_KEY_BLACKLISTDEFINITION]));
+			$keywordsblacklist              = KeywordList::fromInput($sanitized_blacklist_meta_value);
+
+			{
+       update_post_meta(
+   					$post_id,
+   					self::ILJ_META_KEY_BLACKLISTDEFINITION,
+   					array_slice($keywordsblacklist->getKeywords(), 0, 2)
+   				);
+   }
+		}
+
+		if (array_key_exists(self::ILJ_IS_BLACKLISTED, $_POST) && true == $_POST[self::ILJ_IS_BLACKLISTED]) {
+			self::addToBlacklist($post_id, 'post');
+		} else {
+			self::removeFromBlacklist($post_id, 'post');
+		}
+
+		if (array_key_exists(Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys', $_POST)) {
+
+			$sanitized_meta_value = sanitize_text_field(wp_unslash($_POST[Postmeta::ILJ_META_KEY_LINKDEFINITION . '_keys']));
+			$keywords             = KeywordList::fromInput($sanitized_meta_value);
+
+			// Retrieve the old keywords before updating
+			$old_keyword_list = KeywordList::fromMeta($post_id, 'post', Postmeta::ILJ_META_KEY_LINKDEFINITION);
+			$old_keyword_count = $old_keyword_list->getCount();
+			$update_status = self::set_keywords($post_id, $keywords);
+
+			/**
+			 * Fires after keyword meta got saved
+			 *
+			 * @since 1.0.0
+			 */
+			if (true == $update_status) {
+				$post_id       = isset($_POST['post_ID']) ? intval($_POST['post_ID']) : 0;
+				$post_status   = isset($_POST['post_status']) ? sanitize_text_field(wp_unslash($_POST['post_status'])) : '';
+				$keyword_list  = KeywordList::fromMeta($post_id, 'post', Postmeta::ILJ_META_KEY_LINKDEFINITION);
+				$keyword_count = $keyword_list->getCount();
+				if ($old_keyword_count > 0 && 0 == $keyword_count) {
+					Linkindex::delete_link_to($post_id, 'post');
+					Statistic::updateStatisticsInfo();
+				} elseif ($keyword_count > 0) {
+					do_action(self::ILJ_ACTION_AFTER_KEYWORDS_UPDATE, $post_id, 'post', $post_status);
+				}
+			}
+		}
+
+	}
+
+	/**
+	 * Set keywords for a post id, returns boolean or int based on the value.
+	 *
+	 * @param int         $post_id  The post id.
+	 * @param KeywordList $keywords The keyword list for post.
+	 *
+	 * @return bool|int
+	 */
+	public static function set_keywords($post_id, $keywords) {
+		$prev_value = get_post_meta($post_id, Postmeta::ILJ_META_KEY_LINKDEFINITION, true);
+		return update_post_meta(
+			$post_id,
+			Postmeta::ILJ_META_KEY_LINKDEFINITION,
+			$keywords->getKeywords(),
+			$prev_value
+		);
+	}
+
+	/**
+	 * Logic for adding the assets based on subscription
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return void
+	 */
+	public static function addAssets() {
+		$required_role = 'administrator';
+
+
+
+		if (!current_user_can($required_role)) {
+			return;
+		}
+
+		global $pagenow;
+
+		if (in_array($pagenow, array('post-new.php', 'post.php'))) {
+
+			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It gets the post type on the add/edit post admin dashboard page. No nonce verification needed.
+			if (!isset($_GET['post_type'])) {
+				self::registerAssets();
+				return;
+			}
+
+			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- It gets the post type on the add/edit post admin dashboard page. No nonce verification needed.
+			$post_type = get_post_type_object(sanitize_text_field(wp_unslash($_GET['post_type'])));
+
+
+
+			if (!$post_type || !$post_type->public) {
+				return;
+			}
+
+			self::registerAssets();
+		}
+
+
+	}
+
+	/**
+	 * Registering the assets for editor frontend
+	 *
+	 * @since 1.1.0
+	 *
+	 * @return void
+	 */
+	private static function registerAssets() {
+		add_action(
+			'admin_enqueue_scripts',
+			function () {
+				ILJHelperLoader::enqueue_script('jquery-ui-sortable');
+				ILJHelperLoader::register_script(Editor::ILJ_KEYWORDS_HANDLE, ILJ_URL . 'admin/js/ilj_keywords.js', array(), ILJ_VERSION);
+				ILJHelperLoader::register_script(Editor::ILJ_EDITOR_HANDLE, ILJ_URL . 'admin/js/ilj_editor.js', array(), ILJ_VERSION);
+				ILJHelperLoader::register_script(Editor::ILJ_MODAL, ILJ_URL . 'admin/js/ilj_modal.js', array(), ILJ_VERSION);
+				wp_localize_script(Editor::ILJ_EDITOR_HANDLE, 'ilj_editor_duplicate', array(
+					'nonce' => wp_create_nonce('ilj-editor-action'),
+				));
+				wp_localize_script(Editor::ILJ_EDITOR_HANDLE, 'ilj_editor_translation', array_merge(
+					Editor::getTranslation(),
+					array('nonce' => wp_create_nonce('render-keyword-meta-box-nonce'))
+				));
+				wp_add_inline_script(Editor::ILJ_EDITOR_HANDLE, 'const ilj_editor_basic_restriction = ' . wp_json_encode(Editor::getBasicRestrictions()), 'before');
+				ILJHelperLoader::enqueue_script('ilj_tipso', ILJ_URL . 'admin/js/tipso.js', array(), ILJ_VERSION);
+				ILJHelperLoader::enqueue_script(Editor::ILJ_KEYWORDS_HANDLE);
+				ILJHelperLoader::enqueue_script(Editor::ILJ_EDITOR_HANDLE);
+				ILJHelperLoader::enqueue_script(Editor::ILJ_MODAL);
+				ILJHelperLoader::enqueue_style('ilj_tipso', ILJ_URL . 'admin/css/tipso.css', array(), ILJ_VERSION);
+				ILJHelperLoader::enqueue_style(Editor::ILJ_EDITOR_HANDLE, ILJ_URL . 'admin/css/ilj_editor.css', array(), ILJ_VERSION);
+				ILJHelperLoader::enqueue_style(Editor::ILJ_EDITOR_HANDLE, ILJ_URL . 'admin/css/ilj_modal.css', array(), ILJ_VERSION);
+				ILJHelperLoader::enqueue_style('ilj_ui', ILJ_URL . 'admin/css/ilj_ui.css', array(), ILJ_VERSION);
+			},
+			10,
+			1
+		);
+
+	}
+
+	/**
+	 * Returns the frontend translation
+	 *
+	 * @since 1.0.1
+	 *
+	 * @return array
+	 */
+	public static function getTranslation() {
+		$translation = array(
+			'add_keyword'                               => __('Add Keyword', 'internal-links'),
+			'placeholder_keyword'                       => __('Keyword', 'internal-links'),
+			'howto_case'                                => __('Keywords get used <strong>case insensitive</strong>', 'internal-links'),
+			'howto_keyword'                             => __('Separate multiple keywords by commas', 'internal-links'),
+			'howto_gap'                                 => __('Configure the gap dimension.', 'internal-links') . ' ' . __('It represents the number of keywords that appear between your other keywords dynamically.', 'internal-links') . ' ' . __('Learn more in our documentation:', 'internal-links') . '<br><strong><a target="_blank" rel="noopener" href="' . Help::getLinkUrl('editor/', 'gaps', 'gap help', 'editor') . '">' . __('Click here to open', 'internal-links') . '</a></strong>',
+			'headline_gaps'                             => __('Keyword gaps', 'internal-links'),
+			'add_gap'                                   => __('Add gap', 'internal-links'),
+			'gap_type'                                  => __('Gap type', 'internal-links'),
+			'type_min'                                  => __('Minimum', 'internal-links'),
+			'type_exact'                                => __('Exact', 'internal-links'),
+			'type_max'                                  => __('Maximum', 'internal-links'),
+			'howto_gap_min'                             => __('Minimum amount of keywords within the gap.', 'internal-links') . ' ' . __('No upper limits.', 'internal-links'),
+			'howto_gap_exact'                           => __('Exact amount of keywords within the gap.', 'internal-links'),
+			'howto_gap_max'                             => __('Maximum amount of keywords within the gap.', 'internal-links'),
+			'howto_links_per_paragraph'                 => __('Overrides the general setting for the current asset.', 'internal-links'),
+			'howto_add_to_blacklist'                    => __('Toggle to add/remove from global blacklist.', 'internal-links'),
+			'howto_limit_incoming_links'                => __('Toggle to add/remove incoming links limit.', 'internal-links'),
+			'howto_limit_outgoing_links'                => __('Toggle to add/remove outgoing links limit.', 'internal-links'),
+			'insert_gaps'                               => __('Insert gaps between keywords', 'internal-links'),
+			'headline_configured_keywords'              => __('Configured keywords', 'internal-links'),
+			'message_keyword_exists'                    => __('This keyword already exists.', 'internal-links'),
+			'message_no_keyword'                        => __('No keyword defined.', 'internal-links'),
+			'message_length_not_valid'                  => __('Length of given keyword not valid.', 'internal-links'),
+			'message_multiple_placeholder'              => __('Multiple consecutive placeholders are not allowed.', 'internal-links'),
+			'no_keywords'                               => __('No keywords configured.', 'internal-links'),
+			'gap_hover_exact'                           => __('Exact keyword gap:', 'internal-links'),
+			'gap_hover_max'                             => __('Max

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