“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-18400: The Slider, Gallery, and Carousel by MetaSlider plugin for WordPress versions up to and including 3.111.0 contains a Stored Cross-Site Scripting (XSS) vulnerability. The flaw resides in the handling of the ‘delay’ Post Meta Setting, which allows authenticated attackers with Author-level access to inject arbitrary web scripts. This vulnerability has a CVSS score of 6.4 and is categorized under CWE-79.nnThe root cause of the vulnerability is insufficient input sanitization and output escaping for the ‘delay’ post meta value. The plugin’s custom ‘ml-slider’ post type is registered without custom capability restrictions, and the ‘ml-slider_settings’ meta key is unprotected. This allows an Author-level user to create a malicious ‘ml-slider’ post and set the ‘delay’ meta value with a crafted payload via XML-RPC’s custom_fields feature. When the slideshow is rendered, the ‘delay’ value is output without proper sanitization, allowing the embedded script to execute in the browser of any user viewing the page.nnExploitation begins by an Author-level user authenticating to the WordPress site via XML-RPC. The attacker crafts a request to the /xmlrpc.php endpoint using the wp.newPost method, creating a new post of type ‘ml-slider’. Within this request, the attacker sets the custom_fields parameter, specifically targeting the ‘ml-slider_settings’ key, to inject a malicious JavaScript payload into the ‘delay’ field. After the post is published, any user, including administrators, who views a page containing the slideshow will trigger the XSS payload. This is a stored attack because the malicious script is persisted in the database and executes on every subsequent page load.nnThe diff provided shows changes to the admin Table.php file, but the patch details are truncated. The core fix likely adds sanitization for the ‘delay’ value when it is retrieved and output, and may also add capability checks to prevent lower-privileged users from creating or modifying ‘ml-slider’ posts with custom fields. The patched code probably escapes the output using functions like esc_attr() or sanitizes the input, which would neutralize the malicious script. The vulnerability is only partially addressed by the admin table changes, indicating the core fix is in the slideshow frontend rendering code.nnSuccessful exploitation allows an unprivileged Author user to execute arbitrary scripts in the context of any logged-in user who views the slideshow. This can lead to session hijacking, where the attacker steals the administrator’s cookies to gain administrative access. It can also be used for the defacement of the site, the injection of malicious content or links for phishing, and the complete compromise of the WordPress installation’s security and integrity.”,
“poc_php”: “n n keyml-slider_settingsn value{“type”:”flex”,”delay”:”>alert(/XSS/)”}n n n n nnXML;nn// Step 2: Send the request using cURLn$ch = curl_init($target_url);ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, $xml_request);ncurl_setopt($ch, CURLOPT_HTTPHEADER, array(‘Content-Type: text/xml’));nn$response = curl_exec($ch);nif (curl_errno($ch)) {n echo ‘cURL error: ‘ . curl_error($ch) . “\n”;n curl_close($ch);n exit;n}ncurl_close($ch);nn// Step 3: Check if the post was created (look for the post ID in the response)npreg_match(‘/(\d+)/’, $response, $matches);nif (isset($matches[1])) {n echo “[+] Vulnerable, malicous ml-slider post created with ID: ” . $matches[1] . “\n”;n echo “[+] XSS payload will trigger when any user views this slideshow.\n”;n} else {n echo “[-] Failed to create post. Response:\n” . $response . “\n”;n}n?>n”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-18400n# Block the XML-RPC wp.newPost request that attempts to set a malicious ‘delay’ value inn# ‘ml-slider_settings’ meta for the ‘ml-slider’ post type.nSecRule REQUEST_URI “@streq /xmlrpc.php” \n “id:202619400,phase:1,deny,status:403,chain,msg:’CVE-2026-18400 via XML-RPC custom_fields’,severity:’CRITICAL’,tag:’CVE-2026-18400′”n SecRule REQUEST_BODY “@rx wp.newPost” “chain”n SecRule REQUEST_BODY “@rx post_type.*ml-slider” “chain”n SecRule REQUEST_BODY “@rx ml-slider_settings.*delay” “chain”n SecRule REQUEST_BODY “@rx delay.*|javascript:|on[a-z]+=” “t:lowercase”n”
}

CVE-2026-18400: Slider, Gallery, and Carousel by MetaSlider <= 3.111.0 Authenticated (Author+) Stored Cross-Site Scripting via 'delay' Post Meta Setting PoC, Patch Analysis & Rule
CVE-2026-18400
ml-slider
3.111.0
3.111.1
Analysis Overview
Differential between vulnerable and patched code
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/ml-slider/admin/Table.php
+++ b/ml-slider/admin/Table.php
@@ -1,626 +1,628 @@
-<?php
-if (!defined('ABSPATH')) {
- die('No direct access.');
-}
-if (!class_exists('WP_List_table')) {
- require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
-}
-class MetaSlider_Admin_Table extends WP_List_table
-{
- public function prepare_items()
- {
- $this->process_action();
-
- // Check if we filter by theme
- $nonce_valid = isset($_REQUEST['search_wpnonce']) && wp_verify_nonce(sanitize_key($_REQUEST['search_wpnonce']), 'metaslider_search_slideshows');
- if ($nonce_valid && isset($_POST['metaslider_theme'])) {
- $theme_param = sanitize_text_field($_POST['metaslider_theme']);
- $current_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
- if ($theme_param !== '') {
- $_GET['metaslider_theme'] = $theme_param;
- $_SERVER['REQUEST_URI'] = add_query_arg('metaslider_theme', $theme_param, $current_uri);
- } else {
- unset($_GET['metaslider_theme']);
- $_SERVER['REQUEST_URI'] = remove_query_arg('metaslider_theme', $current_uri);
- }
- }
- $columns = $this->get_columns();
- $hidden = array();
- $sortable = $this->get_sortable_columns();
- $this->_column_headers = array($columns, $hidden, $sortable);
-
- if (isset($_REQUEST['s'])) {
- $table_data = $this->table_data(sanitize_text_field($_REQUEST['s']));
- } else {
- $table_data = $this->table_data();
- }
-
- //pagination
- $global_settings = get_option( 'metaslider_global_settings' );
- $slideshows_per_page = isset($global_settings['dashboardItems']) ? $global_settings['dashboardItems'] : 10;
- $table_page = $this->get_pagenum();
- $this->items = array_slice($table_data, (($table_page - 1) * $slideshows_per_page), $slideshows_per_page);
- $total_slideshows = count($table_data);
- $this->set_pagination_args(array(
- 'total_items' => $total_slideshows,
- 'per_page' => $slideshows_per_page,
- 'total_pages' => ceil($total_slideshows/$slideshows_per_page)
- ));
- }
-
- private function table_data($search='', $status='publish')
- {
- global $wpdb;
- $wpdbTable = $wpdb->prefix . 'posts';
- $columns = ['slides', 'post_title', 'post_date', 'slide_count', 'slideshow_type', 'slideshow_theme'];
- $global_settings = get_option( 'metaslider_global_settings' );
-
- $orderBy = $_GET['orderby'] ?? $global_settings['dashboardSort'] ?? 'ID';
- $orderBy = in_array($orderBy, $columns, true) ? $orderBy : 'ID';
-
- $order = $_GET['order'] ?? $global_settings['dashboardOrder'] ?? 'asc';
-
- $status = isset($_GET['post_status']) && 'trash' === $_GET['post_status'] ? 'trash' : 'publish';
-
- $query_results = $this->get_slideshow_query($status, $search);
-
- foreach ($query_results as &$each_slide) {
- $theme = get_post_meta($each_slide['ID'], 'metaslider_slideshow_theme', true);
-
- if ($theme && isset($theme['title'])) {
- $each_slide['slideshow_theme'] = $theme['title'];
- $each_slide['slideshow_theme_folder'] = $theme['folder'] ?? '';
- } else {
- $each_slide['slideshow_theme'] = '';
- $each_slide['slideshow_theme_folder'] = '';
- }
-
- $slideshow_slides = $this->get_slides($each_slide['ID'], $status);
- $each_slide['slideshow_thumb'] = $slideshow_slides;
- $each_slide['slideshow_type'] = $this->get_slide_types($slideshow_slides);
- $each_slide['slide_count'] = count($slideshow_slides);
- }
-
- $theme_filter = isset($_REQUEST['metaslider_theme']) ? sanitize_text_field($_REQUEST['metaslider_theme']) : '';
- if ($theme_filter !== '') {
- $query_results = array_values(array_filter($query_results, function ($slide) use ($theme_filter) {
- return $theme_filter === '__none__'
- ? $slide['slideshow_theme_folder'] === ''
- : $slide['slideshow_theme_folder'] === $theme_filter;
- }));
- }
-
- if ($orderBy === 'slide_count') {
- if ($order === 'asc') {
- usort($query_results, function($a, $b) {
- return $a['slide_count'] - $b['slide_count'];
- });
- } else {
- usort($query_results, function($a, $b) {
- return $b['slide_count'] - $a['slide_count'];
- });
- }
- } elseif ($orderBy === 'post_date') {
- usort($query_results, function($a, $b) use ($orderBy, $order) {
- $aDate = !empty($a[$orderBy]) ? strtotime($a[$orderBy]) : 0;
- $bDate = !empty($b[$orderBy]) ? strtotime($b[$orderBy]) : 0;
- return ($order === 'asc')
- ? $aDate - $bDate
- : $bDate - $aDate;
- });
- } else {
- usort($query_results, function($a, $b) use ($orderBy, $order) {
- return ($order === 'asc') ? strcmp($a[$orderBy], $b[$orderBy]) : strcmp($b[$orderBy], $a[$orderBy]);
- });
- }
-
- return $query_results;
- }
-
- public function no_items()
- {
- if (!empty($_GET['post_status']) && $_GET['post_status'] === 'trash') {
- esc_html_e(
- 'You don't have any trashed slideshows.',
- 'ml-slider'
- );
- } else {
- printf(
- esc_html__(
- 'You don't have any slideshows yet. Click %shere%s to create a new slideshow.',
- 'ml-slider'
- ),
- '<a href="' . esc_url(wp_nonce_url(admin_url("admin-post.php?action=metaslider_create_slider"), "metaslider_create_slider")) . '">','</a>'
- );
- }
-
- }
-
- protected function get_views() {
- global $wpdb;
-
- $views = [];
- $parameters = ['action', 'slideshows', 'post_status', '_wpnonce', 'paged'];
- $current = $_REQUEST['post_status'] ?? 'all';
- $base_url = remove_query_arg($parameters);
-
- // Helper function to generate view links
- $generate_view_link = function ($status, $label) use ($base_url, $current) {
- $count = $this->slideshow_count($status);
- if ($count == 0 && $status === 'trash') {
- return null;
- }
- $url = ($status === 'all') ? $base_url : add_query_arg('post_status', $status, $base_url);
- $class = ($current === $status) ? ' class="current"' : '';
- return "<a href='" . esc_url($url) . "' {$class}>" . esc_html__($label, 'ml-slider') . " ({$count})</a>";
- };
-
- $views['all'] = $generate_view_link('all', 'Published');
-
- if ($trash_link = $generate_view_link('trash', 'Trash')) {
- $views['trash'] = $trash_link;
- }
-
- return $views;
- }
-
- private function slideshow_count($status = 'all')
- {
- if ($status === 'all') {
- $status = 'publish';
- }
- $results = $this->get_slideshow_query($status);
- return count($results);
- }
-
- public function get_bulk_actions()
- {
- if (isset($_REQUEST['post_status']) && $_REQUEST['post_status'] == "trash") {
- $actions = array(
- 'restore' => __('Restore', 'ml-slider'),
- 'permanent' => __('Delete Permanently', 'ml-slider')
- );
- } else {
- $actions = array(
- 'delete' => __('Trash', 'ml-slider')
- );
- }
-
- return $actions;
- }
-
- public function get_columns()
- {
- $columns = array(
- 'cb' => '<input type="checkbox" />',
- 'slideshow_thumb' => esc_html__('Preview', 'ml-slider'),
- 'post_title' => esc_html__('Title', 'ml-slider'),
- 'slideshow_type' => esc_html__('Type of Slides', 'ml-slider'),
- 'slide_count' => esc_html__('Number of Slides', 'ml-slider'),
- 'slideshow_theme' => esc_html__('Theme', 'ml-slider'),
- 'post_date' => esc_html__('Created', 'ml-slider'),
- 'ID' => esc_html__('Shortcode', 'ml-slider')
- );
-
- $status = $_REQUEST['post_status'] ?? 'publish';
- if ($status !== 'trash') {
- $columns['used_on'] = __('Usage', 'ml-slider');
- }
-
- return $columns;
- }
-
- public function get_sortable_columns()
- {
- $sortable_columns = array(
- 'post_title' => array('post_title', false),
- 'post_date' => array('post_date', false),
- 'slideshow_type' => array('slideshow_type',false),
- 'slide_count' => array('slide_count',false),
- 'slideshow_theme' => array('slideshow_theme',false)
- );
- return $sortable_columns;
- }
-
- public function getslide_thumb($slideId)
- {
- $logo = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(dirname(__FILE__) . '/assets/metaslider.svg'));
- if (get_post_type($slideId) == 'attachment') {
- $image = wp_get_attachment_image_src($slideId, 'thumbnail');
- } else {
- $image = wp_get_attachment_image_src(get_post_thumbnail_id($slideId), 'thumbnail');
- }
-
- if (isset($image[0])) {
- $slidethumb = "<img src='". esc_url($image[0]) ."'>";
- } else {
- $slidethumb = "<img src='". $logo ."' class='thumb-logo'>";
- }
- return $slidethumb;
- }
-
- private function get_slideshow_query($status = 'publish', $search = '') {
- global $wpdb;
- $wpdbTable = $wpdb->prefix . 'posts';
-
- $query = "SELECT ID, post_title, post_date FROM $wpdbTable WHERE post_type = %s AND post_status = %s";
- $params = ['ml-slider', $status];
-
- if (!empty($search)) {
- $query .= " AND post_title LIKE %s";
- $params[] = '%' . $wpdb->esc_like($search) . '%';
- }
-
- $prepared_query = $wpdb->prepare($query, $params); // WPCS: unprepared SQL OK.
- return $wpdb->get_results($prepared_query, ARRAY_A); // WPCS: unprepared SQL OK.
- }
-
- public function get_slides($slideshowId, $status)
- {
- $post_status = $status === 'trash' ? array('trash', 'publish') : array($status);
- $slides = get_posts(array(
- 'post_type' => array('ml-slide'),
- 'post_status' => $post_status,
- 'orderby' => 'menu_order',
- 'order' => 'ASC',
- 'lang' => '',
- 'suppress_filters' => 1,
- 'posts_per_page' => -1,
- 'tax_query' => array(
- array(
- 'taxonomy' => 'ml-slider',
- 'field' => 'slug',
- 'terms' => (int) $slideshowId
- )
- )
- ));
-
- return $slides;
- }
-
- public function get_slide_types($slides)
- {
- $slide_list = array();
- foreach ($slides as $slide) {
- $post_title = explode(' - ', $slide->post_title);
- $add_space = str_replace('_', ' ', $post_title[1]);
- if($add_space == 'html overlay') {
- $slide_list[] = 'Layer';
- } else {
- $slide_list[] = ucwords($add_space);
- }
- }
- $final_list = array_values(array_unique($slide_list));
- $type_list = '';
- foreach ($final_list as $list) {
- $type_list .= $list . '<br>';
- }
- return $type_list;
- }
-
- public function column_cb($item)
- {
- return sprintf(
- '<input type="checkbox" name="slideshows[]" value="%d" />', (int)$item['ID']
- );
- }
-
- public function column_slideshow_thumb($item)
- {
- $logo = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(dirname(__FILE__) . '/assets/metaslider.svg'));
- $placeholder = "<img src='". $logo ."' class='thumb-logo'>";
- $numberOfSlides = count($item['slideshow_thumb']);
-
- $thumbHtml = "<div class='w-16 h-16 bg-gray-light slidethumb'>";
- if ($numberOfSlides === 0) {
- $thumbHtml .= $placeholder;
- } else {
- if ($numberOfSlides === 1){
- $thumbHtml .= isset($item['slideshow_thumb']->ID)
- ? $this->getslide_thumb($item['slideshow_thumb']->ID) : $placeholder;
- } else {
- foreach ($item['slideshow_thumb'] as $thumb) {
- $thumbHtml .= isset($thumb->ID) ? $this->getslide_thumb($thumb->ID) : $placeholder;
- }
- }
- }
- $thumbHtml .= "</div>";
- return $thumbHtml;
- }
-
- public function column_post_title($item)
- {
- $page = empty($_REQUEST['page']) ? 'metaslider' : sanitize_key($_REQUEST['page']);
- if(isset($_GET['post_status']) && $_GET['post_status'] == 'trash') {
- $restoreUrl = wp_nonce_url('?page=' . $page . '&post_status=trash&action=restore&slideshows=' . absint($item['ID']), 'bulk-' . $this->_args['plural'] );
- $deleteUrl = wp_nonce_url('?page=' . $page . '&post_status=trash&action=permanent&slideshows=' . absint($item['ID']), 'bulk-' . $this->_args['plural'] );
-
- $actions = [
- 'restore' => '<a href="' . esc_url($restoreUrl) . '">' . esc_html__('Restore', 'ml-slider') . '</a>',
- 'permanent' => '<a class="submitdelete" href="' . esc_url($deleteUrl) . '">' . esc_html__('Delete Permanently', 'ml-slider') . '</a>',
- ];
-
- return sprintf(
- '%1$s %2$s',
- '<a class="row-title">' . esc_html($item['post_title']) . '</a>',
- $this->row_actions($actions)
- );
- } else {
- $editUrl = '?page=' . $page . '&id=' . absint($item['ID']);
- $deleteUrl = wp_nonce_url('?page=' . $page . '&action=delete&slideshows=' . absint($item['ID']), 'bulk-' . $this->_args['plural'] );
-
- $actions = [
- 'edit' => '<a href="' . esc_url($editUrl) . '">' . esc_html__('Edit', 'ml-slider') . '</a>',
- 'trash' => '<a class="submitdelete" href="' . esc_url($deleteUrl) . '">' . esc_html__('Trash', 'ml-slider') . '</a>',
- ];
-
- return sprintf(
- '%1$s %2$s',
- '<a class="row-title" href="' . esc_url($editUrl) . '">' . esc_html($item['post_title']) . '</a>',
- $this->row_actions($actions)
- );
- }
- }
-
- public function column_slideshow_type($item)
- {
- return $item['slideshow_type'];
- }
-
- public function column_slide_count($item)
- {
- return $item['slide_count'];
- }
-
- public function column_slideshow_theme($item)
- {
- return $item['slideshow_theme'];
- }
-
- public function column_post_date($item)
- {
- $date = strtotime($item['post_date']);
- $dateFormat = get_option('date_format');
- $timeFormat = get_option( 'time_format' );
- return ucfirst( wp_date( $dateFormat.' at '.$timeFormat, $date ) );
- }
-
- public function column_ID($item)
- {
- return ('<pre class="copy-shortcode tipsy-tooltip" original-title="' . __('Click to copy shortcode.', 'ml-slider') . '"><div class="text-orange cursor-pointer whitespace-normal inline">[metaslider id="'. esc_attr($item['ID']) .'"]</div></pre><span class="copy-message" style="display:none;"><div class="dashicons dashicons-yes"></div></span>');
- }
-
- public function column_used_on($item)
- {
- $slideshow_id = $item['ID'];
- $pages = $this->get_posts_using_slideshow($slideshow_id);
-
- if ($pages !== esc_html__('Not found.', 'ml-slider')) {
- return '<button class="open-modal button" data-id="' . esc_attr($slideshow_id) . '">'
- . esc_html__('View Usage', 'ml-slider') . '</button>
- <div class="modal-overlay" id="overlay-' . esc_attr($slideshow_id) . '" style="display: none;"></div>
- <div class="shortcode-modal bg-white shadow" id="modal-' . esc_attr($slideshow_id) . '" style="display: none;">
- <div class="modal-content">
- <span class="close-modal" data-id="' . esc_attr($slideshow_id) . '">×</span>
- <h3 class="text-lg font-medium m-0 leading-6 text-gray-darkest">'
- . esc_html__('Content Using This Slideshow', 'ml-slider') . '</h3>'
- . $pages . '
- </div>
- </div>';
- } else {
- return esc_html__('Not found.', 'ml-slider');
- }
- }
-
-
- private function get_posts_using_slideshow($slideshow_id)
- {
- global $wpdb;
- $results = [];
-
- $posts = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT ID, post_title, post_type FROM {$wpdb->posts} WHERE post_status = %s AND (post_content LIKE %s OR post_content REGEXP %s)",
- 'publish',
- '%' . $wpdb->esc_like("[metaslider id="$slideshow_id"") . '%',
- '\[metaslider[^]]*id=["\']?' . $slideshow_id . '["\']?[^]]*\]'
- )
- );
-
- // Organize by post type
- $grouped_results = [];
-
- foreach ($posts as $post) {
- $post_type_obj = get_post_type_object($post->post_type);
- $post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst($post->post_type);
-
- $grouped_results[$post_type_label][] = sprintf(
- '<li><a href="%s" target="_blank">%s</a></li>',
- esc_url(get_permalink($post->ID)),
- esc_html($post->post_title)
- );
- }
-
- $theme_mods = get_option('theme_mods_' . get_option('stylesheet'), []);
- if (is_array($theme_mods)) {
- foreach ($theme_mods as $key => $value) {
- if (is_string($value) && preg_match('/[metaslider[^]]*id=["']?' . $slideshow_id . '["']?[^]]*]/', $value)) {
- $grouped_results[esc_html__('Theme Setting', 'ml-slider')][] = "<li>" . esc_html($key) . "</li>";
- }
- }
- }
-
- $all_options = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_value LIKE %s",
- '%' . $wpdb->esc_like("[metaslider id="$slideshow_id"") . '%'
- )
- );
-
- foreach ($all_options as $option) {
- if (is_string($option->option_value) && preg_match('/[metaslider[^]]*id=["']?' . $slideshow_id . '["']?[^]]*]/', $option->option_value)) {
- $grouped_results[esc_html__('Option', 'ml-slider')][] = "<li>" . esc_html($option->option_name) . "</li>";
- }
- }
-
- $output = '';
- foreach ($grouped_results as $category => $items) {
- $output .= "<h5>{$category}:</h5><ul>" . implode('', $items) . "</ul>";
- }
-
- return empty($output) ? esc_html__('Not found.', 'ml-slider') : $output;
- }
-
- public function extra_tablenav( $which )
- {
- if ( $which == "top" ) {
- echo '<input type="hidden" name="page" value="' . esc_attr($_REQUEST['page'] ?? 'metaslider') . '">';
- $this->render_filters();
- if (isset($_REQUEST['post_status']) && $_REQUEST['post_status'] == "trash") {
- if ( ! empty($this->table_data('', 'trash'))) {
- submit_button( __( 'Empty Trash' ), 'apply', 'delete_all', false );
- }
- }
- }
- }
-
- /**
- * Renders table nav filter dropdowns; currently includes theme, extendable for future filters.
- *
- * @since 3.110.0
- *
- * @return void
- */
- private function render_filters()
- {
- $status = (isset($_GET['post_status']) && 'trash' === $_GET['post_status']) ? 'trash' : 'publish';
- $slideshows = $this->get_slideshow_query($status);
-
- $themes = [];
- $has_no_theme = false;
- foreach ($slideshows as $slideshow) {
- $theme = get_post_meta($slideshow['ID'], 'metaslider_slideshow_theme', true);
- if ($theme && isset($theme['folder'], $theme['title'])) {
- $themes[$theme['folder']] = $theme['title'];
- } else {
- $has_no_theme = true;
- }
- }
-
- if (empty($themes) && !$has_no_theme) return;
-
- asort($themes);
-
- $current = isset($_REQUEST['metaslider_theme']) ? sanitize_text_field($_REQUEST['metaslider_theme']) : '';
-
- echo '<div class="alignleft actions">';
- echo '<select name="metaslider_theme" id="metaslider-theme-filter">';
- echo '<option value="">' . esc_html__('All Themes', 'ml-slider') . '</option>';
- foreach ($themes as $folder => $title) {
- printf(
- '<option value="%s"%s>%s</option>',
- esc_attr($folder),
- selected($current, $folder, false),
- esc_html($title)
- );
- }
- if ($has_no_theme) {
- printf(
- '<option value="__none__"%s>%s</option>',
- selected($current, '__none__', false),
- esc_html__('(No Theme)', 'ml-slider')
- );
- }
- echo '</select> ';
- submit_button(__('Filter', 'ml-slider'), 'button', 'filter_action', false);
- echo '</div>';
- }
-
- public function check_num_rows()
- {
- $table_data = $this->table_data();
- return $table_data;
- }
-
- protected function process_action()
- {
- $action = $this->current_action();
-
- if (isset($_POST['delete_all'])
- || ($action && in_array($action, array('delete', 'restore', 'permanent')))
- ) {
- // Check nonce
- if (! isset($_REQUEST['_wpnonce'])
- || empty($_REQUEST['_wpnonce'])
- || ! wp_verify_nonce(
- sanitize_key($_REQUEST['_wpnonce']),
- 'bulk-' . $this->_args['plural']
- )
- ) {
- wp_die('Cannot process action', 'ml-slider');
- }
-
- if (isset($_POST['delete_all'])) {
- $slideshows = $this->table_data('', 'trash');
- foreach($slideshows as $slideshow_id) {
- wp_delete_post($slideshow_id['ID'], true);
- }
-
- return;
- }
-
- if(isset($_REQUEST['slideshows'])) {
-
- if(is_array($_REQUEST['slideshows'])) {
- $slideshows = array_map('intval', $_REQUEST['slideshows']);
- } else {
- $toArray = array($_REQUEST['slideshows']);
- $slideshows = array_map('intval', $toArray);
- }
- } else {
- //single slider
- if(isset($_REQUEST['id'])) {
- $toArray = array($_REQUEST['id']);
- $slideshows = array_map('intval', $toArray);
- }
- }
-
- switch ( $action ) {
- case 'delete':
- foreach($slideshows as $slideshow_id) {
- wp_update_post(array(
- 'ID' => $slideshow_id,
- 'post_status' => 'trash'
- ));
- }
- break;
- case 'permanent':
- foreach($slideshows as $slideshow_id) {
- wp_delete_post( $slideshow_id, true);
- }
- break;
- case 'restore':
- foreach($slideshows as $slideshow_id) {
- wp_update_post(array(
- 'ID' => $slideshow_id,
- 'post_status' => 'publish'
- ));
-
- $slides = $this->get_slides($slideshow_id, 'trash');
- foreach ($slides as $key => $slide) {
- wp_update_post(array(
- 'ID' => $slide->ID,
- 'post_status' => 'publish'
- ));
- }
- }
- break;
- default:
- return;
- break;
- }
- }
-
- return;
- }
-}
+<?php
+if (!defined('ABSPATH')) {
+ die('No direct access.');
+}
+if (!class_exists('WP_List_table')) {
+ require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
+}
+class MetaSlider_Admin_Table extends WP_List_table
+{
+ public function prepare_items()
+ {
+ $this->process_action();
+
+ // Check if we filter by theme
+ $nonce_valid = isset($_REQUEST['search_wpnonce']) && wp_verify_nonce(sanitize_key($_REQUEST['search_wpnonce']), 'metaslider_search_slideshows');
+ if ($nonce_valid && isset($_POST['metaslider_theme'])) {
+ $theme_param = sanitize_text_field($_POST['metaslider_theme']);
+ $current_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
+ if ($theme_param !== '') {
+ $_GET['metaslider_theme'] = $theme_param;
+ $_SERVER['REQUEST_URI'] = add_query_arg('metaslider_theme', $theme_param, $current_uri);
+ } else {
+ unset($_GET['metaslider_theme']);
+ $_SERVER['REQUEST_URI'] = remove_query_arg('metaslider_theme', $current_uri);
+ }
+ }
+ $columns = $this->get_columns();
+ $hidden = array();
+ $sortable = $this->get_sortable_columns();
+ $this->_column_headers = array($columns, $hidden, $sortable);
+
+ if (isset($_REQUEST['s'])) {
+ $table_data = $this->table_data(sanitize_text_field($_REQUEST['s']));
+ } else {
+ $table_data = $this->table_data();
+ }
+
+ //pagination
+ $global_settings = get_option( 'metaslider_global_settings' );
+ $slideshows_per_page = isset($global_settings['dashboardItems']) ? $global_settings['dashboardItems'] : 10;
+ $table_page = $this->get_pagenum();
+ $this->items = array_slice($table_data, (($table_page - 1) * $slideshows_per_page), $slideshows_per_page);
+ $total_slideshows = count($table_data);
+ $this->set_pagination_args(array(
+ 'total_items' => $total_slideshows,
+ 'per_page' => $slideshows_per_page,
+ 'total_pages' => ceil($total_slideshows/$slideshows_per_page)
+ ));
+ }
+
+ private function table_data($search='', $status='publish')
+ {
+ global $wpdb;
+ $wpdbTable = $wpdb->prefix . 'posts';
+ $columns = ['slides', 'post_title', 'post_date', 'slide_count', 'slideshow_type', 'slideshow_theme'];
+ $global_settings = get_option( 'metaslider_global_settings' );
+
+ $orderBy = $_GET['orderby'] ?? $global_settings['dashboardSort'] ?? 'ID';
+ $orderBy = in_array($orderBy, $columns, true) ? $orderBy : 'ID';
+
+ $order = $_GET['order'] ?? $global_settings['dashboardOrder'] ?? 'asc';
+
+ $status = isset($_GET['post_status']) && 'trash' === $_GET['post_status'] ? 'trash' : 'publish';
+
+ $query_results = $this->get_slideshow_query($status, $search);
+
+ foreach ($query_results as &$each_slide) {
+ $theme = get_post_meta($each_slide['ID'], 'metaslider_slideshow_theme', true);
+
+ if ($theme && isset($theme['title'])) {
+ $each_slide['slideshow_theme'] = $theme['title'];
+ $each_slide['slideshow_theme_folder'] = $theme['folder'] ?? '';
+ } else {
+ $each_slide['slideshow_theme'] = '';
+ $each_slide['slideshow_theme_folder'] = '';
+ }
+
+ $slideshow_slides = $this->get_slides($each_slide['ID'], $status);
+ $each_slide['slideshow_thumb'] = $slideshow_slides;
+ $each_slide['slideshow_type'] = $this->get_slide_types($slideshow_slides);
+ $each_slide['slide_count'] = count($slideshow_slides);
+ }
+
+ $theme_filter = isset($_REQUEST['metaslider_theme']) ? sanitize_text_field($_REQUEST['metaslider_theme']) : '';
+ if ($theme_filter !== '') {
+ $query_results = array_values(array_filter($query_results, function ($slide) use ($theme_filter) {
+ return $theme_filter === '__none__'
+ ? $slide['slideshow_theme_folder'] === ''
+ : $slide['slideshow_theme_folder'] === $theme_filter;
+ }));
+ }
+
+ if ($orderBy === 'slide_count') {
+ if ($order === 'asc') {
+ usort($query_results, function($a, $b) {
+ return $a['slide_count'] - $b['slide_count'];
+ });
+ } else {
+ usort($query_results, function($a, $b) {
+ return $b['slide_count'] - $a['slide_count'];
+ });
+ }
+ } elseif ($orderBy === 'post_date') {
+ usort($query_results, function($a, $b) use ($orderBy, $order) {
+ $aDate = !empty($a[$orderBy]) ? strtotime($a[$orderBy]) : 0;
+ $bDate = !empty($b[$orderBy]) ? strtotime($b[$orderBy]) : 0;
+ return ($order === 'asc')
+ ? $aDate - $bDate
+ : $bDate - $aDate;
+ });
+ } else {
+ usort($query_results, function($a, $b) use ($orderBy, $order) {
+ return ($order === 'asc') ? strcmp($a[$orderBy], $b[$orderBy]) : strcmp($b[$orderBy], $a[$orderBy]);
+ });
+ }
+
+ return $query_results;
+ }
+
+ public function no_items()
+ {
+ if (!empty($_GET['post_status']) && $_GET['post_status'] === 'trash') {
+ esc_html_e(
+ 'You don't have any trashed slideshows.',
+ 'ml-slider'
+ );
+ } else {
+ printf(
+ esc_html__(
+ 'You don't have any slideshows yet. Click %shere%s to create a new slideshow.',
+ 'ml-slider'
+ ),
+ '<a href="' . esc_url(wp_nonce_url(admin_url("admin-post.php?action=metaslider_create_slider"), "metaslider_create_slider")) . '">','</a>'
+ );
+ }
+
+ }
+
+ protected function get_views() {
+ global $wpdb;
+
+ $views = [];
+ $parameters = ['action', 'slideshows', 'post_status', '_wpnonce', 'paged'];
+ $current = $_REQUEST['post_status'] ?? 'all';
+ $base_url = remove_query_arg($parameters);
+
+ // Helper function to generate view links
+ $generate_view_link = function ($status, $label) use ($base_url, $current) {
+ $count = $this->slideshow_count($status);
+ if ($count == 0 && $status === 'trash') {
+ return null;
+ }
+ $url = ($status === 'all') ? $base_url : add_query_arg('post_status', $status, $base_url);
+ $class = ($current === $status) ? ' class="current"' : '';
+ return "<a href='" . esc_url($url) . "' {$class}>" . esc_html__($label, 'ml-slider') . " ({$count})</a>";
+ };
+
+ $views['all'] = $generate_view_link('all', 'Published');
+
+ if ($trash_link = $generate_view_link('trash', 'Trash')) {
+ $views['trash'] = $trash_link;
+ }
+
+ return $views;
+ }
+
+ private function slideshow_count($status = 'all')
+ {
+ if ($status === 'all') {
+ $status = 'publish';
+ }
+ $results = $this->get_slideshow_query($status);
+ return count($results);
+ }
+
+ public function get_bulk_actions()
+ {
+ if (isset($_REQUEST['post_status']) && $_REQUEST['post_status'] == "trash") {
+ $actions = array(
+ 'restore' => __('Restore', 'ml-slider'),
+ 'permanent' => __('Delete Permanently', 'ml-slider')
+ );
+ } else {
+ $actions = array(
+ 'delete' => __('Trash', 'ml-slider')
+ );
+ }
+
+ return $actions;
+ }
+
+ public function get_columns()
+ {
+ $columns = array(
+ 'cb' => '<input type="checkbox" />',
+ 'slideshow_thumb' => esc_html__('Preview', 'ml-slider'),
+ 'post_title' => esc_html__('Title', 'ml-slider'),
+ 'slideshow_type' => esc_html__('Type of Slides', 'ml-slider'),
+ 'slide_count' => esc_html__('Number of Slides', 'ml-slider'),
+ 'slideshow_theme' => esc_html__('Theme', 'ml-slider'),
+ 'post_date' => esc_html__('Created', 'ml-slider'),
+ 'ID' => esc_html__('Shortcode', 'ml-slider')
+ );
+
+ $status = $_REQUEST['post_status'] ?? 'publish';
+ if ($status !== 'trash') {
+ $columns['used_on'] = __('Usage', 'ml-slider');
+ }
+
+ return $columns;
+ }
+
+ public function get_sortable_columns()
+ {
+ $sortable_columns = array(
+ 'post_title' => array('post_title', false),
+ 'post_date' => array('post_date', false),
+ 'slideshow_type' => array('slideshow_type',false),
+ 'slide_count' => array('slide_count',false),
+ 'slideshow_theme' => array('slideshow_theme',false)
+ );
+ return $sortable_columns;
+ }
+
+ public function getslide_thumb($slideId)
+ {
+ $logo = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(dirname(__FILE__) . '/assets/metaslider.svg'));
+ if (get_post_type($slideId) == 'attachment') {
+ $image = wp_get_attachment_image_src($slideId, 'thumbnail');
+ } else {
+ $image = wp_get_attachment_image_src(get_post_thumbnail_id($slideId), 'thumbnail');
+ }
+
+ if (isset($image[0])) {
+ $slidethumb = "<img src='". esc_url($image[0]) ."'>";
+ } else {
+ $slidethumb = "<img src='". $logo ."' class='thumb-logo'>";
+ }
+ return $slidethumb;
+ }
+
+ private function get_slideshow_query($status = 'publish', $search = '') {
+ global $wpdb;
+ $wpdbTable = $wpdb->prefix . 'posts';
+
+ $query = "SELECT ID, post_title, post_date FROM $wpdbTable WHERE post_type = %s AND post_status = %s";
+ $params = ['ml-slider', $status];
+
+ if (!empty($search)) {
+ $query .= " AND post_title LIKE %s";
+ $params[] = '%' . $wpdb->esc_like($search) . '%';
+ }
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query fragments are fixed SQL above, with all dynamic values passed through $wpdb->prepare().
+ $prepared_query = $wpdb->prepare($query, $params);
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $prepared_query is prepared immediately above.
+ return $wpdb->get_results($prepared_query, ARRAY_A);
+ }
+
+ public function get_slides($slideshowId, $status)
+ {
+ $post_status = $status === 'trash' ? array('trash', 'publish') : array($status);
+ $slides = get_posts(array(
+ 'post_type' => array('ml-slide'),
+ 'post_status' => $post_status,
+ 'orderby' => 'menu_order',
+ 'order' => 'ASC',
+ 'lang' => '',
+ 'suppress_filters' => 1,
+ 'posts_per_page' => -1,
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'ml-slider',
+ 'field' => 'slug',
+ 'terms' => (int) $slideshowId
+ )
+ )
+ ));
+
+ return $slides;
+ }
+
+ public function get_slide_types($slides)
+ {
+ $slide_list = array();
+ foreach ($slides as $slide) {
+ $post_title = explode(' - ', $slide->post_title);
+ $add_space = str_replace('_', ' ', $post_title[1]);
+ if($add_space == 'html overlay') {
+ $slide_list[] = 'Layer';
+ } else {
+ $slide_list[] = ucwords($add_space);
+ }
+ }
+ $final_list = array_values(array_unique($slide_list));
+ $type_list = '';
+ foreach ($final_list as $list) {
+ $type_list .= $list . '<br>';
+ }
+ return $type_list;
+ }
+
+ public function column_cb($item)
+ {
+ return sprintf(
+ '<input type="checkbox" name="slideshows[]" value="%d" />', (int)$item['ID']
+ );
+ }
+
+ public function column_slideshow_thumb($item)
+ {
+ $logo = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(dirname(__FILE__) . '/assets/metaslider.svg'));
+ $placeholder = "<img src='". $logo ."' class='thumb-logo'>";
+ $numberOfSlides = count($item['slideshow_thumb']);
+
+ $thumbHtml = "<div class='w-16 h-16 bg-gray-light slidethumb'>";
+ if ($numberOfSlides === 0) {
+ $thumbHtml .= $placeholder;
+ } else {
+ if ($numberOfSlides === 1){
+ $thumbHtml .= isset($item['slideshow_thumb']->ID)
+ ? $this->getslide_thumb($item['slideshow_thumb']->ID) : $placeholder;
+ } else {
+ foreach ($item['slideshow_thumb'] as $thumb) {
+ $thumbHtml .= isset($thumb->ID) ? $this->getslide_thumb($thumb->ID) : $placeholder;
+ }
+ }
+ }
+ $thumbHtml .= "</div>";
+ return $thumbHtml;
+ }
+
+ public function column_post_title($item)
+ {
+ $page = empty($_REQUEST['page']) ? 'metaslider' : sanitize_key($_REQUEST['page']);
+ if(isset($_GET['post_status']) && $_GET['post_status'] == 'trash') {
+ $restoreUrl = wp_nonce_url('?page=' . $page . '&post_status=trash&action=restore&slideshows=' . absint($item['ID']), 'bulk-' . $this->_args['plural'] );
+ $deleteUrl = wp_nonce_url('?page=' . $page . '&post_status=trash&action=permanent&slideshows=' . absint($item['ID']), 'bulk-' . $this->_args['plural'] );
+
+ $actions = [
+ 'restore' => '<a href="' . esc_url($restoreUrl) . '">' . esc_html__('Restore', 'ml-slider') . '</a>',
+ 'permanent' => '<a class="submitdelete" href="' . esc_url($deleteUrl) . '">' . esc_html__('Delete Permanently', 'ml-slider') . '</a>',
+ ];
+
+ return sprintf(
+ '%1$s %2$s',
+ '<a class="row-title">' . esc_html($item['post_title']) . '</a>',
+ $this->row_actions($actions)
+ );
+ } else {
+ $editUrl = '?page=' . $page . '&id=' . absint($item['ID']);
+ $deleteUrl = wp_nonce_url('?page=' . $page . '&action=delete&slideshows=' . absint($item['ID']), 'bulk-' . $this->_args['plural'] );
+
+ $actions = [
+ 'edit' => '<a href="' . esc_url($editUrl) . '">' . esc_html__('Edit', 'ml-slider') . '</a>',
+ 'trash' => '<a class="submitdelete" href="' . esc_url($deleteUrl) . '">' . esc_html__('Trash', 'ml-slider') . '</a>',
+ ];
+
+ return sprintf(
+ '%1$s %2$s',
+ '<a class="row-title" href="' . esc_url($editUrl) . '">' . esc_html($item['post_title']) . '</a>',
+ $this->row_actions($actions)
+ );
+ }
+ }
+
+ public function column_slideshow_type($item)
+ {
+ return $item['slideshow_type'];
+ }
+
+ public function column_slide_count($item)
+ {
+ return $item['slide_count'];
+ }
+
+ public function column_slideshow_theme($item)
+ {
+ return $item['slideshow_theme'];
+ }
+
+ public function column_post_date($item)
+ {
+ $date = strtotime($item['post_date']);
+ $dateFormat = get_option('date_format');
+ $timeFormat = get_option( 'time_format' );
+ return ucfirst( wp_date( $dateFormat.' at '.$timeFormat, $date ) );
+ }
+
+ public function column_ID($item)
+ {
+ return ('<pre class="copy-shortcode tipsy-tooltip" original-title="' . __('Click to copy shortcode.', 'ml-slider') . '"><div class="text-orange cursor-pointer whitespace-normal inline">[metaslider id="'. esc_attr($item['ID']) .'"]</div></pre><span class="copy-message" style="display:none;"><div class="dashicons dashicons-yes"></div></span>');
+ }
+
+ public function column_used_on($item)
+ {
+ $slideshow_id = $item['ID'];
+ $pages = $this->get_posts_using_slideshow($slideshow_id);
+
+ if ($pages !== esc_html__('Not found.', 'ml-slider')) {
+ return '<button class="open-modal button" data-id="' . esc_attr($slideshow_id) . '">'
+ . esc_html__('View Usage', 'ml-slider') . '</button>
+ <div class="modal-overlay" id="overlay-' . esc_attr($slideshow_id) . '" style="display: none;"></div>
+ <div class="shortcode-modal bg-white shadow" id="modal-' . esc_attr($slideshow_id) . '" style="display: none;">
+ <div class="modal-content">
+ <span class="close-modal" data-id="' . esc_attr($slideshow_id) . '">×</span>
+ <h3 class="text-lg font-medium m-0 leading-6 text-gray-darkest">'
+ . esc_html__('Content Using This Slideshow', 'ml-slider') . '</h3>'
+ . $pages . '
+ </div>
+ </div>';
+ } else {
+ return esc_html__('Not found.', 'ml-slider');
+ }
+ }
+
+
+ private function get_posts_using_slideshow($slideshow_id)
+ {
+ global $wpdb;
+ $results = [];
+
+ $posts = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT ID, post_title, post_type FROM {$wpdb->posts} WHERE post_status = %s AND (post_content LIKE %s OR post_content REGEXP %s)",
+ 'publish',
+ '%' . $wpdb->esc_like("[metaslider id="$slideshow_id"") . '%',
+ '\[metaslider[^]]*id=["\']?' . $slideshow_id . '["\']?[^]]*\]'
+ )
+ );
+
+ // Organize by post type
+ $grouped_results = [];
+
+ foreach ($posts as $post) {
+ $post_type_obj = get_post_type_object($post->post_type);
+ $post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst($post->post_type);
+
+ $grouped_results[$post_type_label][] = sprintf(
+ '<li><a href="%s" target="_blank">%s</a></li>',
+ esc_url(get_permalink($post->ID)),
+ esc_html($post->post_title)
+ );
+ }
+
+ $theme_mods = get_option('theme_mods_' . get_option('stylesheet'), []);
+ if (is_array($theme_mods)) {
+ foreach ($theme_mods as $key => $value) {
+ if (is_string($value) && preg_match('/[metaslider[^]]*id=["']?' . $slideshow_id . '["']?[^]]*]/', $value)) {
+ $grouped_results[esc_html__('Theme Setting', 'ml-slider')][] = "<li>" . esc_html($key) . "</li>";
+ }
+ }
+ }
+
+ $all_options = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_value LIKE %s",
+ '%' . $wpdb->esc_like("[metaslider id="$slideshow_id"") . '%'
+ )
+ );
+
+ foreach ($all_options as $option) {
+ if (is_string($option->option_value) && preg_match('/[metaslider[^]]*id=["']?' . $slideshow_id . '["']?[^]]*]/', $option->option_value)) {
+ $grouped_results[esc_html__('Option', 'ml-slider')][] = "<li>" . esc_html($option->option_name) . "</li>";
+ }
+ }
+
+ $output = '';
+ foreach ($grouped_results as $category => $items) {
+ $output .= "<h5>{$category}:</h5><ul>" . implode('', $items) . "</ul>";
+ }
+
+ return empty($output) ? esc_html__('Not found.', 'ml-slider') : $output;
+ }
+
+ public function extra_tablenav( $which )
+ {
+ if ( $which == "top" ) {
+ echo '<input type="hidden" name="page" value="' . esc_attr($_REQUEST['page'] ?? 'metaslider') . '">';
+ $this->render_filters();
+ if (isset($_REQUEST['post_status']) && $_REQUEST['post_status'] == "trash") {
+ if ( ! empty($this->table_data('', 'trash'))) {
+ submit_button( __( 'Empty Trash' ), 'apply', 'delete_all', false );
+ }
+ }
+ }
+ }
+
+ /**
+ * Renders table nav filter dropdowns; currently includes theme, extendable for future filters.
+ *
+ * @since 3.110.0
+ *
+ * @return void
+ */
+ private function render_filters()
+ {
+ $status = (isset($_GET['post_status']) && 'trash' === $_GET['post_status']) ? 'trash' : 'publish';
+ $slideshows = $this->get_slideshow_query($status);
+
+ $themes = [];
+ $has_no_theme = false;
+ foreach ($slideshows as $slideshow) {
+ $theme = get_post_meta($slideshow['ID'], 'metaslider_slideshow_theme', true);
+ if ($theme && isset($theme['folder'], $theme['title'])) {
+ $themes[$theme['folder']] = $theme['title'];
+ } else {
+ $has_no_theme = true;
+ }
+ }
+
+ if (empty($themes) && !$has_no_theme) return;
+
+ asort($themes);
+
+ $current = isset($_REQUEST['metaslider_theme']) ? sanitize_text_field($_REQUEST['metaslider_theme']) : '';
+
+ echo '<div class="alignleft actions">';
+ echo '<select name="metaslider_theme" id="metaslider-theme-filter">';
+ echo '<option value="">' . esc_html__('All Themes', 'ml-slider') . '</option>';
+ foreach ($themes as $folder => $title) {
+ printf(
+ '<option value="%s"%s>%s</option>',
+ esc_attr($folder),
+ selected($current, $folder, false),
+ esc_html($title)
+ );
+ }
+ if ($has_no_theme) {
+ printf(
+ '<option value="__none__"%s>%s</option>',
+ selected($current, '__none__', false),
+ esc_html__('(No Theme)', 'ml-slider')
+ );
+ }
+ echo '</select> ';
+ submit_button(__('Filter', 'ml-slider'), 'button', 'filter_action', false);
+ echo '</div>';
+ }
+
+ public function check_num_rows()
+ {
+ $table_data = $this->table_data();
+ return $table_data;
+ }
+
+ protected function process_action()
+ {
+ $action = $this->current_action();
+
+ if (isset($_POST['delete_all'])
+ || ($action && in_array($action, array('delete', 'restore', 'permanent')))
+ ) {
+ // Check nonce
+ if (! isset($_REQUEST['_wpnonce'])
+ || empty($_REQUEST['_wpnonce'])
+ || ! wp_verify_nonce(
+ sanitize_key($_REQUEST['_wpnonce']),
+ 'bulk-' . $this->_args['plural']
+ )
+ ) {
+ wp_die('Cannot process action', 'ml-slider');
+ }
+
+ if (isset($_POST['delete_all'])) {
+ $slideshows = $this->table_data('', 'trash');
+ foreach($slideshows as $slideshow_id) {
+ wp_delete_post($slideshow_id['ID'], true);
+ }
+
+ return;
+ }
+
+ if(isset($_REQUEST['slideshows'])) {
+
+ if(is_array($_REQUEST['slideshows'])) {
+ $slideshows = array_map('intval', $_REQUEST['slideshows']);
+ } else {
+ $toArray = array($_REQUEST['slideshows']);
+ $slideshows = array_map('intval', $toArray);
+ }
+ } else {
+ //single slider
+ if(isset($_REQUEST['id'])) {
+ $toArray = array($_REQUEST['id']);
+ $slideshows = array_map('intval', $toArray);
+ }
+ }
+
+ switch ( $action ) {
+ case 'delete':
+ foreach($slideshows as $slideshow_id) {
+ wp_update_post(array(
+ 'ID' => $slideshow_id,
+ 'post_status' => 'trash'
+ ));
+ }
+ break;
+ case 'permanent':
+ foreach($slideshows as $slideshow_id) {
+ wp_delete_post( $slideshow_id, true);
+ }
+ break;
+ case 'restore':
+ foreach($slideshows as $slideshow_id) {
+ wp_update_post(array(
+ 'ID' => $slideshow_id,
+ 'post_status' => 'publish'
+ ));
+
+ $slides = $this->get_slides($slideshow_id, 'trash');
+ foreach ($slides as $key => $slide) {
+ wp_update_post(array(
+ 'ID' => $slide->ID,
+ 'post_status' => 'publish'
+ ));
+ }
+ }
+ break;
+ default:
+ return;
+ break;
+ }
+ }
+
+ return;
+ }
+}
--- a/ml-slider/admin/lib/helpers.php
+++ b/ml-slider/admin/lib/helpers.php
@@ -509,8 +509,12 @@
function metaslider_filter_unsafe_html( $content, $slide, $slider_id, $settings )
{
try {
- if ( ! class_exists( 'HTMLPurifier' ) ) {
- require_once( METASLIDER_PATH . 'lib/htmlpurifier/library/HTMLPurifier.auto.php' );
+ if ( ! class_exists( 'HTMLPurifier_Config' ) || ! class_exists( 'HTMLPurifier' ) ) {
+ $autoloader = METASLIDER_PATH . 'lib/htmlpurifier/library/HTMLPurifier.auto.php';
+ if ( ! file_exists( $autoloader ) ) {
+ throw new RuntimeException( 'HTMLPurifier autoloader not found: ' . $autoloader );
+ }
+ require_once( $autoloader );
}
$config = HTMLPurifier_Config::createDefault();
// How to filter:
@@ -522,8 +526,7 @@
$config = apply_filters('metaslider_html_purifier_config', $config, $slide, $slider_id, $settings);
$purifier = new HTMLPurifier( $config );
$content = $purifier->purify( $content );
- } catch ( Exception $e ) {
- // If something goes wrong then escape
+ } catch ( Throwable $e ) {
$content = htmlspecialchars( do_shortcode( $content ), ENT_NOQUOTES, 'UTF-8' );
}
--- a/ml-slider/admin/routes/api.php
+++ b/ml-slider/admin/routes/api.php
@@ -1168,7 +1168,7 @@
// If there are files here, then we need to prepare them
// Dont use get_file_params() as it's WP4.4
- // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REST route access is checked by can_access(), and uploaded files are validated by process_uploads().
$images = isset($_FILES['files']) ? $this->process_uploads($_FILES['files'], $data['image_data']) : array();
// $images should be an array of image data at this point
--- a/ml-slider/inc/slider/metaslider.class.php
+++ b/ml-slider/inc/slider/metaslider.class.php
@@ -56,9 +56,10 @@
if (
is_array($settings) &&
isset($settings['type']) &&
- in_array($settings['type'], array( 'flex', 'coin', 'nivo', 'responsive' ))
+ in_array($settings['type'], array( 'flex', 'coin', 'nivo', 'responsive' ))
) {
- return $settings;
+ // Only trust keys we actually recognise
+ return array_intersect_key($settings, $this->get_default_parameters());
} else {
return $this->get_default_parameters();
}
@@ -76,6 +77,11 @@
$defaults = $this->get_default_parameters();
if (isset($defaults[$name])) {
+ // Make sure to return a string, even if the default is boolean
+ if (gettype($defaults[$name]) == 'boolean') {
+ return $defaults[$name] ? 'true' : 'false';
+ }
+
return $defaults[$name] ? $defaults[$name] : 'false';
}
} else {
@@ -644,6 +650,26 @@
}
/**
+ * Coerce a setting into a JS-safe integer token, without turning an empty/disabled
+ * setting (e.g. an unset width/height, normalised by get_setting() to the string
+ * 'false') into 0. Anything that's neither numeric nor the 'true'/'false' sentinel
+ * is untrusted and is coerced to 0 rather than ever being output raw.
+ *
+ * @since 3.111.1
+ *
+ * @param mixed $val
+ * @return int|string
+ */
+ protected function to_js_int($val)
+ {
+ if (is_numeric($val)) {
+ return (int) $val;
+ }
+
+ return ($val === 'true' || $val === 'false') ? $val : 0;
+ }
+
+ /**
* Build the javascript parameter arguments for the slider.
*
* @return string parameters
@@ -657,8 +683,10 @@
if ($param = $this->get_param($name)) {
$val = $this->get_setting($name);
- if (gettype($default) == 'integer' || $val == 'true' || $val == 'false') {
- $options[$param] = $val;
+ if (is_int($default)) {
+ $options[$param] = $this->to_js_int($val);
+ } elseif (is_bool($default) || $val === 'true' || $val === 'false') {
+ $options[$param] = $val === 'true' ? 'true' : 'false';
} else {
$options[$param] = '"' . esc_js($val) . '"';
}
--- a/ml-slider/inc/slider/metaslider.flex.class.php
+++ b/ml-slider/inc/slider/metaslider.flex.class.php
@@ -80,13 +80,13 @@
{
if (isset($options["carouselMode"])) {
if ($options["carouselMode"] == "true") {
- $options["itemWidth"] = $this->get_setting('width');
+ $options["itemWidth"] = $this->to_js_int($this->get_setting('width'));
$options["animation"] = "'slide'";
$options["direction"] = "'horizontal'";
- $options["minItems"] = $this->get_setting('minItems');
- $options["maxItems"] = $this->get_setting('maxItems');
+ $options["minItems"] = $this->to_js_int($this->get_setting('minItems'));
+ $options["maxItems"] = $this->to_js_int($this->get_setting('maxItems'));
$options["move"] = 1;
- $options["itemMargin"] = apply_filters('metaslider_carousel_margin', $this->get_setting('carouselMargin'), $slider_id);
+ $options["itemMargin"] = $this->to_js_int(apply_filters('metaslider_carousel_margin', $this->get_setting('carouselMargin'), $slider_id));
//activate infinite loop when carousel is set to 'continously' and 'autoplay'
if($this->get_setting('infiniteLoop') == 'true'){
$options["controlNav"] = "false";
@@ -221,7 +221,7 @@
public function get_carousel_css($css, $settings, $slider_id)
{
if (isset($settings['carouselMode']) && $settings['carouselMode'] == 'true') {
- $margin = apply_filters('metaslider_carousel_margin', $this->get_setting('carouselMargin'), $slider_id);
+ $margin = (int) apply_filters('metaslider_carousel_margin', $this->get_setting('carouselMargin'), $slider_id);
$css .= "n #metaslider_{$slider_id}.flexslider .slides li {margin-right: {$margin}px !important;}";
if(isset($settings['infiniteLoop']) && $settings['infiniteLoop'] == 'true'){
@@ -240,8 +240,8 @@
$slides = count($this->slides);
}
$double = $slides * 2;
- $animationtime = ($settings['animationSpeed'] * $slides) + ($settings['delay'] * $slides);
- $transform_width = $margin + $settings["width"];
+ $animationtime = ((int) $settings['animationSpeed'] * $slides) + ((int) $settings['delay'] * $slides);
+ $transform_width = $margin + (int) $settings["width"];
$reverse = ( isset( $settings['reverse'] ) && $settings['reverse'] == 'true' ) ? true : false;
$start_position = $reverse ? "calc(var(--ms-slide-width) * -" . $slides . ")" : "0";
$end_position = $reverse ? "0" : "calc(var(--ms-slide-width) * -" . $slides . ")";
@@ -856,6 +856,13 @@
*/
public function manage_pausePlay_button($options, $slider_id, $settings)
{
+ // Don't show the pause/play button when there's only one slide
+ if (count($this->slides) <= 1) {
+ $options['pausePlay'] = 'false';
+ remove_filter('metaslider_flex_slider_parameters', array($this, 'manage_pausePlay_button'));
+ return $options;
+ }
+
if (isset($settings['pausePlay']) && $settings['pausePlay'] === 'true') {
/* @since 3.97 - disable hover on pause when play button is enabled */
unset($options['pauseOnHover']);
@@ -874,7 +881,7 @@
$script = "$('.flex-pauseplay a').removeClass('flex-pause').addClass('flex-play');";
if ($showPlayText && !empty($settings['playText'])) {
- $script .= "$('.flex-pauseplay a').text('" . addslashes($settings['playText']) . "');";
+ $script .= "$('.flex-pauseplay a').text('" . esc_js($settings['playText']) . "');";
}
$options['start'] = array_merge($options['start'], [$script]);
@@ -890,10 +897,10 @@
"var ms_pause_play_sync = function() {
var pausePlayBtn = $('#metaslider_" . $slider_id . " .flex-pauseplay a');
if (pausePlayBtn.hasClass('flex-pause')) {
- pausePlayBtn.attr('aria-label', '" . addslashes($pauseText) . "');
+ pausePlayBtn.attr('aria-label', '" . esc_js($pauseText) . "');
{$ariaLiveOff}
} else {
- pausePlayBtn.attr('aria-label', '" . addslashes($playText) . "');
+ pausePlayBtn.attr('aria-label', '" . esc_js($playText) . "');
{$ariaLiveOn}
}
};
--- a/ml-slider/ml-slider.php
+++ b/ml-slider/ml-slider.php
@@ -5,7 +5,7 @@
* Plugin Name: MetaSlider Slideshow
* Plugin URI: https://www.metaslider.com
* Description: MetaSlider gives you the power to create a beautiful slideshow, carousel, or gallery on your WordPre
Frequently Asked Questions
What is CVE-2026-18400?
Vulnerability overviewCVE-2026-18400 is a Stored Cross-Site Scripting (XSS) vulnerability in the Slider, Gallery, and Carousel by MetaSlider plugin for WordPress, affecting versions up to and including 3.111.0. It allows authenticated users with Author-level access to inject arbitrary web scripts via the ‘delay’ post meta setting, which execute when any user views the slideshow.
How does the vulnerability work?
Technical explanationThe vulnerability stems from insufficient input sanitization and output escaping of the ‘delay’ value in the ‘ml-slider_settings’ meta key. An attacker with Author-level access can create an ‘ml-slider’ post via XML-RPC, setting the ‘delay’ field to a malicious JavaScript payload. When the slideshow is rendered, the payload is output without proper escaping, leading to script execution in the browser of any visitor.
Who is affected by this vulnerability?
Affected users and rolesWordPress sites running MetaSlider version 3.111.0 or earlier are affected. The attack requires an authenticated user with at least Author-level access, meaning users with Contributor or Subscriber roles cannot exploit it directly. However, any Author-level user can compromise the site by injecting scripts that may affect administrators and other users.
How can I check if my site is vulnerable?
Detection stepsCheck the MetaSlider plugin version in your WordPress admin dashboard under Plugins. If it is 3.111.0 or lower, your site is vulnerable. Additionally, you can review your site’s XML-RPC settings and user roles to assess exposure, but the most reliable method is to update to the patched version 3.111.1.
What is the CVSS score and what does it mean?
Severity assessmentThe CVSS score is 6.4, which is rated as Medium severity. This indicates a moderate risk: exploitation requires authenticated access, but the impact is significant because it can lead to session hijacking, site defacement, or full compromise of the WordPress installation if an administrator views the malicious slideshow.
How is the vulnerability exploited?
Attack vectorAn attacker authenticates as an Author-level user and sends a crafted XML-RPC request to the /xmlrpc.php endpoint using the wp.newPost method. The request creates a new ‘ml-slider’ post with a custom field ‘ml-slider_settings’ containing a malicious ‘delay’ value. Once published, the stored XSS payload triggers when any user views the slideshow.
What does the proof of concept demonstrate?
PoC explanationThe proof of concept (PoC) shows how an Author-level user can create a malicious slider post via XML-RPC. It sends a wp.newPost request with a custom field that sets the ‘delay’ value to a JavaScript payload. The PoC then checks the response for a post ID, confirming the vulnerability if the post is created successfully.
How can I fix the vulnerability?
Mitigation stepsThe primary fix is to update the MetaSlider plugin to version 3.111.1 or later, which includes proper sanitization and output escaping for the ‘delay’ value. As an interim measure, you can disable XML-RPC if not needed, or restrict Author-level users from creating ‘ml-slider’ posts, but updating is the recommended solution.
Is there a patch available?
Patch detailsYes, the patched version is 3.111.1. The patch addresses the vulnerability by adding sanitization for the ‘delay’ value when it is retrieved and output, and likely includes capability checks to prevent lower-privileged users from setting malicious custom fields. The provided code diff shows changes to the admin Table.php file, but the core fix is in the frontend rendering code.
What are the practical risks of this vulnerability?
Impact assessmentIf exploited, an Author-level attacker can inject scripts that execute in the context of any user viewing the slideshow, including administrators. This can lead to session hijacking, where the attacker steals admin cookies and gains full control, or to site defacement and phishing attacks. The integrity and security of the WordPress installation are at risk.
How does the WAF rule help mitigate this?
Security rule explanationThe provided ModSecurity rule blocks XML-RPC requests that attempt to create an ‘ml-slider’ post with a malicious ‘delay’ value. It checks for the presence of ‘wp.newPost’, ‘ml-slider’ post type, and suspicious patterns in the ‘delay’ field, such as JavaScript events. This can prevent exploitation at the web application firewall level.
What should I do if I suspect my site is compromised?
Incident responseIf you suspect exploitation, immediately update the plugin to the patched version, change all user passwords, especially for Author and Admin roles, and review your site for unauthorized posts or injected scripts. You should also check server logs for suspicious XML-RPC requests and consider using a security plugin to scan for malware.
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.
Trusted by Developers & Organizations






