# contact-forms/2.3.5/admin/submissions-list-page.php

Contact Forms by Cimatti, version 2.3.5. 1,157 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/2.3.5/code/admin/submissions-list-page.php
- Raw: https://pluginprobe.com/plugins/contact-forms/2.3.5/raw/admin/submissions-list-page.php
- Modified: 2026-08-21T08:40:42+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/contact-forms/2.3.5/code/admin/submissions-list-page.php#L10-L20`.

```php
<?php
if ( ! defined( 'ABSPATH' ) ) exit;

if(!class_exists('WP_List_Table')){
    require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}

class Accua_Forms_Submissions_List_Table extends WP_List_Table {

    var $message = NULL;
    var $active_items = 0;
    var $del_items = 0;

    var $items_per_lead_status = array();

    public $export_xls = false;
    public $removed_columns = array();

    function __construct(){
        global $status, $page;
        $this->message = '';
        parent::__construct( array(
            'singular'  => 'submission',
            'plural'    => 'submissions',
            'ajax'      => false
        ) );
    }

    function get_num_of_active_items () {
      return $this->active_items;
    }

    function get_num_of_del_items () {
      return $this->del_items;
    }

    function get_items_per_lead_status() {
      return $this->items_per_lead_status;
    }

    function column_default($item, $column_name){
      if (isset($item[$column_name])) {
        return $item[$column_name];
      } else {
        return '';
      }
    }

    function column_cb($item){
        return sprintf(
            '<input type="checkbox" name="%1$s[]" value="%2$s" />',
            /*$1%s*/ $this->_args['singular'],
            /*$2%s*/ $item['ID']
        );
    }

    function column_lead_status($item) {
      return accua_forms_select_lead_status($item['ID'], $item['lead_status']);
    }

    function column_ID($item) {
        $view_url = admin_url('admin.php?page=accua_forms_submissions_list&sid=' . $item['ID']);
        return sprintf('<a href="%s"><strong>%s</strong></a>', esc_url($view_url), esc_html($item['ID']));
    }

    function column_singlesub($item){
        $view_url = admin_url('admin.php?page=accua_forms_submissions_list&sid=' . $item['ID']);

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
        $del = isset($_GET['del']) && $_GET['del'] == 1;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
        $spam_view = !$del && isset($_GET['lead_status']) && (int) $_GET['lead_status'] === -1;

        $links = array(
            sprintf('<a href="%s">%s</a>', esc_url($view_url), esc_html__('Open', 'contact-forms')),
        );
        if ($del) {
            $restore_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&del=1&action=restore&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s">%s</a>', esc_url($restore_url), esc_html__('Restore', 'contact-forms'));
            $shred_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&del=1&action=shred&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s" class="submitdelete" onclick="return confirm(\'%s\');">%s</a>', esc_url($shred_url), esc_js(__('Are you sure you want to permanently delete this submission? This action cannot be undone.', 'contact-forms')), esc_html__('Permanently delete', 'contact-forms'));
        } elseif ($spam_view) {
            $unspam_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&lead_status=-1&action=unspam&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s">%s</a>', esc_url($unspam_url), esc_html__('Not spam', 'contact-forms'));
            $trash_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&lead_status=-1&action=delete&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($trash_url), esc_html_x('Trash', 'row action', 'contact-forms'));
            $shred_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&lead_status=-1&action=shred&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s" class="submitdelete" onclick="return confirm(\'%s\');">%s</a>', esc_url($shred_url), esc_js(__('Are you sure you want to permanently delete this submission? This action cannot be undone.', 'contact-forms')), esc_html__('Permanently delete', 'contact-forms'));
        } else {
            $trash_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&action=delete&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($trash_url), esc_html_x('Trash', 'row action', 'contact-forms'));
            $spam_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&action=spam&submission[]=' . $item['ID']), 'bulk-submissions');
            $links[] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($spam_url), esc_html_x('Spam', 'row action', 'contact-forms'));
        }
        return implode(' | ', $links);
    }

    /**
     * Drop the core 'fixed' class: with dynamic per-field columns, content-based
     * (auto) column sizing reads better than evenly divided fixed widths.
     */
    protected function get_table_classes() {
        return array_diff(parent::get_table_classes(), array('fixed'));
    }

    /**
     * Wrap the table - and only the table - in a horizontal scroll container.
     *
     * With 'fixed' dropped above the table sizes to its content, so once
     * enough columns are on it is wider than the screen. Unwrapped, that made
     * the *document* scroll sideways: the filters, the pagination and the
     * heading slid out of view, the fixed admin bar stopped short of the
     * content, and where WordPress does not add body.sticky-menu (it only does
     * when the admin menu is shorter than the page, so tall menus miss out)
     * #adminmenuwrap sat in normal flow and slid away too, while the fixed
     * #adminmenuback stayed behind as an empty coloured strip.
     *
     * submissions-list.js gives this box a sticky scrollbar pinned to the
     * bottom of the viewport - its own one sits at the foot of a table that
     * can be a hundred rows tall, far below the fold.
     *
     * Buffering parent::display() rather than copying the core table markup
     * keeps the override safe across WordPress versions: print_table_description()
     * only exists since 6.4 and the plugin still supports 5.9.
     */
    public function display() {
        ob_start();
        parent::display();
        $html = ob_get_clean();

        $start = strpos($html, '<table');
        $end   = strrpos($html, '</table>');

        if ($start === false || $end === false || $end < $start) {
            // Unexpected markup: emit it untouched rather than mangle it.
            echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WP_List_Table display() outputs safe HTML
            return;
        }

        $end += strlen('</table>');

        // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- WP_List_Table display() outputs safe HTML, already escaped by core; re-escaping it here would emit the markup as text. disable/enable rather than ignore: the statement spans several lines and a single-line ignore does not reach the substr() calls on the continuations.
        echo substr($html, 0, $start)
            . '<div class="accua-table-scroll">'
            . substr($html, $start, $end - $start)
            . '</div>'
            . substr($html, $end);
        // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
    }

    /**
     * Column keys kept visible by the "Essential Columns" toolbar button.
     *
     * Actions, ID, Form and Submitted are always essential; a field column is
     * essential when its definition has the "Show in essential columns" flag
     * (Fields page checkbox, since 2.2.47 - the upgrade migration flags the
     * email field, the only field column of the previously hardcoded list).
     *
     * Single source of truth: used for the button tooltip here and mirrored
     * to setEssentialColumns() in submissions-list.js via wp_localize_script.
     */
    public static function essential_columns() {
        $essential = array('singlesub', 'ID', 'form_title', 'submitted');
        $avail_fields = get_option('accua_forms_avail_fields', array());
        if (is_array($avail_fields)) {
            foreach ($avail_fields as $slug => $field_data) {
                if (!empty($field_data['essential_column'])) {
                    $essential[] = '_field_' . $slug;
                }
            }
        }
        return $essential;
    }

    function column_uri( $item ) {
        $value = $item['uri'] ?? '';
        $url = $value !== '' ? home_url( $value ) : '';
        return $this->truncate_long_value( $value, 80, $url );
    }

    function column_referrer( $item ) {
        $value = $item['referrer'] ?? '';
        return $this->truncate_long_value( $value, 80, $value );
    }

    /**
     * Render a URL column value as a clickable link. The cell is CSS-truncated
     * (max-width + ellipsis) and expandable-cells.js turns cells that overflow
     * into the [+] expandable widget client-side. Both callers always pass a
     * non-empty $url for a non-empty $value, so no server-side truncation runs
     * (the old plain-text <details> branch was unreachable and duplicated the
     * text, which made browser find-in-page count matches twice).
     */
    private function truncate_long_value( $value, $max = 80, $url = '' ) {
        if ( $value === '' ) {
            return '';
        }
        return '<a href="' . esc_url( $url ) . '" target="_blank" title="' . esc_attr( $value ) . '">' . esc_html( $value ) . '</a>';
    }

    protected function get_primary_column_name() {
        return 'ID';
    }

    function no_items() {
        esc_html_e('No submissions found.', 'contact-forms');
    }

    function set_message($single_message) {
      $this->message=$single_message;
    }

    function get_message() {
      if($this->message!=NULL)
        return $this->message;
      else
        return NULL;
    }

    function get_columns(){
        global $wpdb;
        $columns = array(
            'cb' => '<input type="checkbox" />', //Render a checkbox instead of text
            'singlesub' => __('Actions', 'contact-forms'),
            'ID' => 'ID',
            'form_title' => 'Form',
            'form_id' => __("Form ID", 'contact-forms'),
            'pid' => __("Page ID", 'contact-forms'),
            'ip' => 'IP',
            'uri'  => __("Page", 'contact-forms'),
            'referrer' => __("Referrer", 'contact-forms'),
            'lang' => __("Language", 'contact-forms'),
            'created' => __("Opened", 'contact-forms'),
            'submitted' => __("Submitted", 'contact-forms'),
        );
        $query = "SELECT DISTINCT afsv_field_id FROM `{$wpdb->prefix}accua_forms_submissions_values`";
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
        $res = $wpdb->get_col($query);

        $avail_fields = get_option('accua_forms_avail_fields', array());
        foreach ($res as $col) {
          // Internal values the plugin stores alongside the submitted ones are
          // not fields and get no column: the "__" prefix is reserved (the
          // Fields page refuses it as a slug) and "_accua_" carries plumbing
          // such as the file download token, whose value has no business being
          // listed - or exported. The single submission page skips the same two
          // prefixes.
          if (str_starts_with($col, '__') || str_starts_with($col, '_accua_')) {
            continue;
          }
          if (empty($avail_fields[$col]) || !isset($avail_fields[$col]['name'])) {
            /* translators: %s: field slug/identifier */
            $columns['_field_'.$col] = sprintf( __( '%s (removed)', 'contact-forms' ), $col );
            $this->removed_columns['_field_'.$col] = $col;
          } else {
            $columns['_field_'.$col] = $avail_fields[$col]['name'];
          }
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
        if(isset($_GET['del']) && $_GET['del']==1){

        } else{
            $columns['lead_status'] = __('Lead Status', 'contact-forms');
        }
        return $columns;
    }

    function get_bulk_actions() {
      // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
      $del = isset($_GET['del']) && $_GET['del'] == 1;
      // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
      $spam_view = !$del && isset($_GET['lead_status']) && (int) $_GET['lead_status'] === -1;
      if ($del) {
        $actions = array(
            'restore' => __('Restore', 'contact-forms'),
            'shred' => __('Permanently delete', 'contact-forms'),
        );
      } elseif ($spam_view) {
        $actions = array(
            'unspam' => __('Not spam', 'contact-forms'),
            'delete' => __('Move to trash', 'contact-forms'),
            'shred' => __('Permanently delete', 'contact-forms'),
        );
      } else {
        $actions = array(
            'delete' => __('Move to trash', 'contact-forms'),
            'spam' => __('Mark as spam', 'contact-forms'),
            'anonymize' => __('Anonymize', 'contact-forms'),
        );
      }
      // The two buttons above the table export the whole filtered view; these
      // are the same two exports narrowed to the ticked rows, so they carry
      // the same labels. Offered in every view, since the buttons are too and
      // the trash and spam views are exactly where a partial export is wanted.
      $actions['export_visible'] = __('Export visible columns to Excel', 'contact-forms');
      $actions['export_all'] = __('Export all columns to Excel', 'contact-forms');
      return $actions;
    }

    /**
     * The column keys an export writes, in table order.
     *
     * The buttons above the table answer this in the browser, from the Screen
     * Options checkboxes; a bulk action arrives as a plain form submit, so the
     * answer has to come from the server instead. The two agree: core saves a
     * column toggle to the user option as soon as it is clicked, and the
     * checkbox list is the column list minus the columns core refuses to hide,
     * which here is only the checkbox column.
     *
     * 'singlesub' carries the row actions, links back to this same screen with
     * nothing to export; the JS drops it too.
     *
     * @param bool $only_visible Leave out the columns hidden in Screen Options.
     * @return array
     */
    public function export_column_keys($only_visible) {
      $keys = array_diff(array_keys($this->get_columns()), array('cb', 'singlesub'));
      if ($only_visible) {
        $keys = array_diff($keys, get_hidden_columns($this->screen));
      }
      return array_values($keys);
    }

    protected function get_views() {
        $views = array();
        $base_url = admin_url('admin.php?page=accua_forms_submissions_list');

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
        $del = isset($_GET['del']) && $_GET['del'] == 1;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
        $active_lead_status = isset($_GET['lead_status']) ? (int) $_GET['lead_status'] : null;

        $class = (!$del && $active_lead_status === null) ? 'current' : '';
        $views['active'] = sprintf(
            '<a href="%s" class="%s">%s</a> (%s)',
            esc_url($base_url),
            $class,
            esc_html__('Active', 'contact-forms'),
            number_format_i18n($this->active_items)
        );

        $lead_statuses = accua_forms_get_lead_statuses();
        foreach ($lead_statuses as $lead_status_id => $lead_status_label) {
            $id = (int) $lead_status_id;
            // Spam gets its own always-visible view below (comments-like).
            if ($id === -1) {
                continue;
            }
            if (!empty($this->items_per_lead_status[$lead_status_id]->n)) {
                $class = ($active_lead_status === $id) ? 'current' : '';
                $views['lead_' . $id] = sprintf(
                    '<a href="%s" class="%s" title="%s">%s</a> (%s)',
                    esc_url(add_query_arg('lead_status', $id, $base_url)),
                    $class,
                    esc_attr__('Lead status', 'contact-forms'),
                    esc_html($lead_status_label),
                    number_format_i18n(absint($this->items_per_lead_status[$lead_status_id]->n))
                );
            }
        }

        $spam_count = !empty($this->items_per_lead_status[-1]->n) ? absint($this->items_per_lead_status[-1]->n) : 0;
        $class = (!$del && $active_lead_status === -1) ? 'current' : '';
        $views['spam'] = sprintf(
            '<a href="%s" class="%s">%s</a> (%s)',
            esc_url(add_query_arg('lead_status', '-1', $base_url)),
            $class,
            esc_html__('Spam', 'contact-forms'),
            number_format_i18n($spam_count)
        );

        $class = $del ? 'current' : '';
        $views['trash'] = sprintf(
            '<a href="%s" class="%s">%s</a> (%s)',
            esc_url(add_query_arg('del', '1', $base_url)),
            $class,
            esc_html__('Trash', 'contact-forms'),
            number_format_i18n($this->del_items)
        );

        return $views;
    }

    function prepare_items($all=false, $get = array()) {
        global $wpdb, $hook_suffix;

        if (!$get) {
          // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin list table uses WordPress nonce verification via check_admin_referer
          $get = stripslashes_deep($_GET);
        }

        $per_page = $this->get_items_per_page('accua_forms_submissions_per_page', 100);
        $del = isset($get['del']) && $get['del']==1;

        // Active excludes spam (afs_lead_status = -1), like the WordPress comments screen.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
        $this->active_items = $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_status >= 0 AND afs_lead_status <> -1");
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
        $this->del_items = $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_status < 0");

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
        $this->items_per_lead_status = $wpdb->get_results("SELECT afs_lead_status AS status, COUNT(*) AS n
                   FROM `{$wpdb->prefix}accua_forms_submissions`
                   WHERE afs_status >= 0
                   GROUP BY afs_lead_status", OBJECT_K);

        $filter = $filter_query_custom_field = "";
        $search = '';

        if(isset($get['fid']) && ($get['fid'])!=-1) {
          $filter .= $wpdb->prepare(" AND afs_form_id = %s ", $get['fid']);
        }
        if(isset($get['pid']) && ($get['pid']!=-1)) {
          $filter .= $wpdb->prepare(" AND afs_post_id = %d ", $get['pid']);
        }

        if(isset($get['year']) && ($get['year']>0)) {
          $filter .= $wpdb->prepare(" AND year(afs_submitted) = %d ", $get['year']);
        }
        if(isset($get['month']) && ($get['month']>0)) {
          $filter .= $wpdb->prepare(" AND month(afs_submitted) = %d ", $get['month']);
        }

        if(isset($get['s'])) {
            $search = trim($get['s']);
        }

        if (isset($get['date_from']) && $get['date_from'] !== '') {
          $filter .= $wpdb->prepare(" AND afs_submitted >= %s ", $get['date_from']);
        }
        if (isset($get['date_to']) && $get['date_to'] !== '') {
          $filter .= $wpdb->prepare(" AND afs_submitted < %s ", $get['date_to']);
        }

        // Bulk export of the ticked rows. Read only in export mode ($all):
        // the normal render reads $_GET, where a row action leaves its
        // submission[] behind, and honoring that there would silently narrow
        // the table to the single row the action was aimed at.
        if ($all && !empty($get['accua_export_ids']) && is_array($get['accua_export_ids'])) {
          $export_ids = array_values(array_unique(array_map('intval', $get['accua_export_ids'])));
          $placeholders = implode(',', array_fill(0, count($export_ids), '%d'));
          // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
          $filter .= $wpdb->prepare(" AND afs_id IN ($placeholders) ", ...$export_ids);
          // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
        }

        $columns = $this->get_columns();
        $hidden = get_hidden_columns($hook_suffix);

        // Build sortable columns: all main DB columns + all field columns
        $sortable = array(
            'ID'          => array('ID', true),
            'form_title'  => array('form_title', false),
            'form_id'     => array('form_id', false),
            'pid'         => array('pid', false),
            'ip'          => array('ip', false),
            'uri'         => array('uri', false),
            'referrer'    => array('referrer', false),
            'lang'        => array('lang', false),
            'created'     => array('created', true),
            'submitted'   => array('submitted', true),
            'lead_status' => array('lead_status', false),
        );
        foreach ($columns as $key => $label) {
            if (strpos($key, '_field_') === 0) {
                $sortable[$key] = array($key, false);
            }
        }

        $this->_column_headers = array($columns, $hidden, $sortable);
        $current_page = $this->get_pagenum();

        if ($all) {
          $limit = '';
        } else {
          $limit = ($current_page - 1) * $per_page;
          $limit = $wpdb->prepare("LIMIT %d, %d", $limit, $per_page);
        }

        $forms_data = get_option('accua_forms_saved_forms', array());

        if ($del) {
          $afs_status_cond = 'afs_status < 0';
        } else {
          $afs_status_cond = 'afs_status >= 0';
        }

        if (isset($get['lead_status'])) {
          $afs_lead_status_cond = $wpdb->prepare(" AND afs_lead_status = %d ", $get['lead_status']);
        } elseif (!$del) {
          // Default view hides spam; it lives in its own view (lead_status=-1).
          $afs_lead_status_cond = ' AND afs_lead_status <> -1 ';
        } else {
          $afs_lead_status_cond = '';
        }


        if($search !== '') {
          // Escape special LIKE characters (% and _) in user input, then wrap with wildcards
          $like_search = '%' . $wpdb->esc_like($search) . '%';
          // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table prefix is safe
          $sql_search_where = $wpdb->prepare(
            " AND (
              afs_ip LIKE %s
              OR afs_uri LIKE %s
              OR afs_referrer LIKE %s
              OR afs_lang LIKE %s
              OR afs_created LIKE %s
              OR afs_submitted LIKE %s
              OR afs_id LIKE %s
              OR afs_id IN (
                SELECT DISTINCT (afsv_sub_id)
                FROM `{$wpdb->prefix}accua_forms_submissions_values`
                WHERE afsv_value LIKE %s)
              ) ",
            $like_search, $like_search, $like_search, $like_search,
            $like_search, $like_search, $like_search, $like_search
          );
        } else {
          $sql_search_where = '';
        }

        // Sorting
        $orderby_sql = 'afs_id';
        $order_sql = 'DESC';
        $sort_join = '';
        $null_sort = '';

        if (isset($get['order']) && in_array(strtoupper($get['order']), array('ASC', 'DESC'), true)) {
            $order_sql = strtoupper($get['order']);
        }

        if (isset($get['orderby']) && $get['orderby'] !== '') {
            $main_col_map = array(
                'ID'          => 'afs_id',
                'form_title'  => 'afs_form_id',
                'form_id'     => 'afs_form_id',
                'pid'         => 'afs_post_id',
                'ip'          => 'afs_ip',
                'uri'         => 'afs_uri',
                'referrer'    => 'afs_referrer',
                'lang'        => 'afs_lang',
                'created'     => 'afs_created',
                'submitted'   => 'afs_submitted',
                'lead_status' => 'afs_lead_status',
            );

            $orderby_param = $get['orderby'];
            if (isset($main_col_map[$orderby_param])) {
                $orderby_sql = $main_col_map[$orderby_param];
            } elseif (strpos($orderby_param, '_field_') === 0 && isset($columns[$orderby_param])) {
                $field_id = substr($orderby_param, 7); // strip '_field_' prefix
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table prefix is safe
                $sort_join = $wpdb->prepare(
                    " LEFT JOIN `{$wpdb->prefix}accua_forms_submissions_values` AS sort_field ON sort_field.afsv_sub_id = afs_id AND sort_field.afsv_field_id = %s ",
                    $field_id
                );
                $orderby_sql = 'sort_field.afsv_value';
                // Push NULLs (submissions without this field) to the end regardless of sort direction
                $null_sort = 'sort_field.afsv_value IS NULL,';
            }
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- All sort variables are validated against allowlists or prepared
        $query1 = "SELECT SQL_CALC_FOUND_ROWS
            afs_id AS ID,
            afs_form_id AS form_id,
            afs_post_id AS pid,
            afs_ip AS ip,
            afs_uri AS uri,
            afs_referrer AS referrer,
            afs_lang AS lang,
            afs_created AS created,
            afs_submitted AS submitted,
            afs_lead_status AS lead_status
          FROM `{$wpdb->prefix}accua_forms_submissions`
          {$sort_join}
          WHERE {$afs_status_cond} {$afs_lead_status_cond} {$filter} {$sql_search_where}
          ORDER BY {$null_sort} {$orderby_sql} {$order_sql}, afs_id DESC
          {$limit}";

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- All components pre-prepared using $wpdb->prepare and esc_like
        $data1 = $wpdb->get_results($query1, ARRAY_A);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Built-in MySQL function
        $total_items = $wpdb->get_var('SELECT FOUND_ROWS()');

        $data = array();
        $submissions = array();
        foreach ($data1 as $row) {
          $fid = $row['form_id'];

          if (isset($forms_data[$fid]['title']) && (trim($forms_data[$fid]['title']) !== '')) {
            $row['form_title'] = $forms_data[$fid]['title'];
          } else {
            $row['form_title'] = $fid;
          }

          $sid = (int) $row['ID'];

          foreach($row as $k => $v) {
            $row[$k] = esc_html($v);
          }

          $data[$sid] = $row;
          $submissions[] = $sid;
        }

        if ($submissions) {
          // Create dynamic placeholders for each ID
          $placeholders = implode(',', array_fill(0, count($submissions), '%d'));
          // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values are integers
          $query2 = $wpdb->prepare(
              "SELECT *
              FROM `{$wpdb->prefix}accua_forms_submissions_values`
              WHERE afsv_sub_id IN ($placeholders)",
              ...$submissions
          );
          // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
          // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
          $data2 = $wpdb->get_results($query2, OBJECT);

          if ($data2) {
            foreach ($data2 as $row) {
              switch ($row->afsv_type) {
                case 'file' :
                  $fieldid = rawurlencode($row->afsv_field_id);
                  $filename = rawurlencode($row->afsv_value);
                  $url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$row->afsv_sub_id}&field={$fieldid}&file={$filename}&nonce=" . wp_create_nonce('accua_forms_download_nonce') . "&_wpnonce=" . wp_create_nonce('download_file_' . $row->afsv_sub_id . '_' . $fieldid);
                  if ($this->export_xls) {
                    /* imposto token segreto 32 caratteri */
                    $token_xls = accua_forms_generate_download_token($row->afsv_sub_id);
                    $url .= '&html=1&token='.$token_xls;
                  }
                  $url = esc_url($url);
                  $filename = esc_attr($row->afsv_value);
                  $fielddata = "<a href='{$url}' target='_blank'>{$filename}</a>";
                break;
                case 'colorpicker':
                  if ($row->afsv_value === '') {
                    $fielddata = '';
                  } else {
                    $value_esc = esc_attr($row->afsv_value);
                    $fielddata = "<span style='color: {$value_esc}'><font color='{$value_esc}'>&#9608;</font></span> $value_esc";
                  }
                break;
                case 'password':
                case 'password-and-confirm':
                case 'hashed-password':
                  $fielddata = ($row->afsv_value !== '') ? '••••' : '';
                break;
                case 'token':
                  $fielddata = '';
                break;
                default:
                  $fielddata = esc_attr($row->afsv_value);
              }
              $data[$row->afsv_sub_id]['_field_'.$row->afsv_field_id] = $fielddata;
            }
          }
        }

        $this->items = $data;

        $this->set_pagination_args( array(
            'total_items' => $total_items,
            'per_page'    => $per_page,
            'total_pages' => ceil($total_items/$per_page)
        ) );


    }

    function process_bulk_action(){
      $current_action = $this->current_action();
      if ($current_action) {
        check_admin_referer('bulk-submissions'); //check nonce generated for 'bulk-'.$this->_args['plural']
        if ('delete' === $current_action) {
          $trashed = array();
          // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
          if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
            foreach ($_GET['submission'] as $i) {
              $i = (int)$i;
              $trashed[$i] = $i;
            }
          }
          if ($trashed) {
            global $wpdb;
            // Create placeholders for each ID and prepare the query safely
            $placeholders = implode(',', array_fill(0, count($trashed), '%d'));
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
            $query = $wpdb->prepare(
              "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id IN ($placeholders)",
              array_values($trashed)
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
            $res = $wpdb->query($query);
            if ($res === false) {
              $this->set_message(__("Error moving submissions to trash.", 'contact-forms') );
            } else if ($res == 1) {
              $this->set_message(__("Moved 1 submission to trash.", 'contact-forms') );
            } else {
              $this->set_message(strtr(__("Moved %res submissions to trash.", 'contact-forms'), array('%res' => $res)) );
            }
          } else {
            $this->set_message(__("No submission selected.", 'contact-forms') );
          }
        } else if ('shred' === $current_action) {
          $shredded = array();
          // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
          if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
            foreach ($_GET['submission'] as $i) {
              $i = (int)$i;
              $shredded[$i] = $i;
            }
          }
          if ($shredded) {
            $shredded = array_values($shredded);
            $placeholders = implode(',', array_fill(0, count($shredded), '%d'));
            global $wpdb;
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
            $query = $wpdb->prepare(
                "DELETE FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_id IN ($placeholders)",
                ...$shredded
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
            $res = $wpdb->query($query);
            if ($res === false) {
              $this->set_message(__('Error deleting submissions.', 'contact-forms') );
            } else {
              // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
              $query2 = $wpdb->prepare(
                  "DELETE FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id IN ($placeholders)",
                  ...$shredded
              );
              // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
              // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
              $res2 = $wpdb->query($query2);
              if ($res == 1) {
                $this->set_message(__("Deleted 1 submission.", 'contact-forms') );
              } else {
                $this->set_message(strtr(__("Deleted %res submissions.", 'contact-forms'), array('%res' => $res)) );
              }
            }
          } else {
            $this->set_message(__("No submission selected.", 'contact-forms') );
          }
        } else if ('spam' === $current_action || 'unspam' === $current_action) {
          $ids = array();
          // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
          if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
            foreach ($_GET['submission'] as $i) {
              $i = (int)$i;
              $ids[$i] = $i;
            }
          }
          if ($ids) {
            $ids = array_values($ids);
            $new_lead_status = ('spam' === $current_action) ? -1 : 0;
            $placeholders = implode(',', array_fill(0, count($ids), '%d'));
            global $wpdb;
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic placeholders, values cast to int
            $query = $wpdb->prepare(
                "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_lead_status = %d WHERE afs_id IN ($placeholders)",
                $new_lead_status,
                ...$ids
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
            $res = $wpdb->query($query);
            if ($res === false) {
              $this->set_message(('spam' === $current_action)
                ? __('Error marking submissions as spam.', 'contact-forms')
                : __('Error restoring submissions from spam.', 'contact-forms'));
            } elseif ('spam' === $current_action) {
              if ($res == 1) {
                $this->set_message(__('Marked 1 submission as spam.', 'contact-forms'));
              } else {
                $this->set_message(strtr(__('Marked %res submissions as spam.', 'contact-forms'), array('%res' => $res)));
              }
            } else {
              if ($res == 1) {
                $this->set_message(__('1 submission restored from spam.', 'contact-forms'));
              } else {
                $this->set_message(strtr(__('%res submissions restored from spam.', 'contact-forms'), array('%res' => $res)));
              }
            }
          } else {
            $this->set_message(__('No submission selected.', 'contact-forms'));
          }
        } else if ('anonymize' === $current_action) {
          $anonymized = array();
          // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
          if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
            foreach ($_GET['submission'] as $i) {
              $i = (int)$i;
              $anonymized[$i] = $i;
            }
          }
          if ($anonymized) {
            $count = 0;
            foreach ($anonymized as $sub_id) {
              if (accua_forms_erase_submission($sub_id, 'anonymize')) {
                $count++;
              }
            }
            if ($count == 1) {
              $this->set_message(__('Anonymized 1 submission.', 'contact-forms') );
            } else {
              $this->set_message(strtr(__('Anonymized %res submissions.', 'contact-forms'), array('%res' => $count)) );
            }
          } else {
            $this->set_message(__('No submission selected.', 'contact-forms') );
          }
        } else if ('restore' === $current_action) {
          $restored = array();
          // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
          if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
            foreach ($_GET['submission'] as $i) {
              $i = (int)$i;
              $restored[$i] = $i;
            }
          }
          if ($restored) {
            $restored = array_values($restored);
            $placeholders = implode(',', array_fill(0, count($restored), '%d'));
            global $wpdb;
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
            $query = $wpdb->prepare(
                "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = 0 WHERE afs_id IN ($placeholders)",
                ...$restored
            );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
            $res = $wpdb->query($query);
            if ($res === false) {
              $this->set_message(__("Error restoring submissions.", 'contact-forms') );
            } else if ($res == 1) {
              $this->set_message(__("Restored 1 submission.", 'contact-forms') );
            } else {
              $this->set_message(strtr(__("Restored %res submissions.", 'contact-forms'), array('%res' => $res)) );
            }
          } else {
            $this->set_message(__('No submission selected.', 'contact-forms') );
          }
        } else if ('export_visible' === $current_action || 'export_all' === $current_action) {
          // The export itself runs on the load hook, before the response has
          // started, and dies there with the file: a download has to send its
          // own headers. This method runs from admin_head, so reaching it at
          // all means the load hook found nothing ticked to export. WordPress
          // refuses an empty bulk action in the browser before it is sent, so
          // this answers the request that never went through that guard.
          $this->set_message(__('No submission selected.', 'contact-forms') );
        }
      }
    }

    /**
     * Render filter dropdowns and export buttons above the table.
     * @param string $which 'top' or 'bottom'
     */
    protected function extra_tablenav($which) {
        if ($which !== 'top') {
            return;
        }

        global $wpdb;

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter state
        $get = stripslashes_deep($_GET);

        $filter_form  = isset($get['fid']) && $get['fid'] != '-1' ? sanitize_text_field($get['fid']) : null;
        $filter_post  = isset($get['pid']) && $get['pid'] != '-1' ? (int) $get['pid'] : null;
        $filter_year  = isset($get['year']) && $get['year'] > 0 ? (int) $get['year'] : null;
        $filter_month = isset($get['month']) && $get['month'] > 0 ? (int) $get['month'] : null;

        // Forms data
        $forms_data = get_option('accua_forms_saved_forms', array());
        $saved_forms_id = array_keys($forms_data);

        if (!empty($saved_forms_id)) {
            $placeholders = implode(',', array_fill(0, count($saved_forms_id), '%s'));
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders
            $query = $wpdb->prepare(
                "SELECT DISTINCT afs_form_id FROM {$wpdb->prefix}accua_forms_submissions WHERE afs_form_id NOT IN ($placeholders) AND afs_status >= 0",
                ...$saved_forms_id
            );
            // phpcs:enable
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
            $deleted_form_ids = $wpdb->get_col($query);
        } else {
            $deleted_form_ids = array();
        }

        // Pages
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Static query
        $page_ids = $wpdb->get_col("SELECT DISTINCT afs_post_id FROM {$wpdb->prefix}accua_forms_submissions WHERE afs_post_id <> 0 AND afs_status >= 0");

        // Year range
        $cur_year = (int) wp_date('Y');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Static query
        $min_year = (int) $wpdb->get_var("SELECT YEAR(MIN(afs_submitted)) FROM {$wpdb->prefix}accua_forms_submissions");
        if ($min_year <= 0) {
            $min_year = $cur_year;
        }
        ?>
        <div class="alignleft actions">
            <label for="filter-by-page" class="screen-reader-text"><?php esc_html_e('Filter by page', 'contact-forms'); ?></label>
            <select name="pid" id="filter-by-page">
                <option value="-1"><?php esc_html_e('Show all pages', 'contact-forms'); ?></option>
                <?php foreach ($page_ids as $post_id) : ?>
                    <option value="<?php echo esc_attr($post_id); ?>" <?php selected($filter_post, (int) $post_id); ?>>
                        <?php echo esc_html(get_the_title($post_id)); ?>
                    </option>
                <?php endforeach; ?>
            </select>

            <label for="filter-by-form" class="screen-reader-text"><?php esc_html_e('Filter by form', 'contact-forms'); ?></label>
            <select name="fid" id="filter-by-form">
                <option value="-1"><?php esc_html_e('Show all forms', 'contact-forms'); ?></option>
                <?php foreach ($forms_data as $form_key => $form) : ?>
                    <option value="<?php echo esc_attr($form_key); ?>" <?php selected($filter_form, $form_key); ?>>
                        <?php echo esc_html(!empty($form['title']) ? $form['title'] : $form_key); ?>
                    </option>
                <?php endforeach; ?>
                <?php foreach ($deleted_form_ids as $del_form_id) : ?>
                    <option value="<?php echo esc_attr($del_form_id); ?>" <?php selected($filter_form, $del_form_id); ?>>
                        <?php echo esc_html($del_form_id); ?> (del)
                    </option>
                <?php endforeach; ?>
            </select>

            <label for="filter-by-year" class="screen-reader-text"><?php esc_html_e('Filter by year', 'contact-forms'); ?></label>
            <select name="year" id="filter-by-year">
                <option value="-1"><?php esc_html_e('All years', 'contact-forms'); ?></option>
                <?php for ($i = $min_year; $i <= $cur_year; $i++) : ?>
                    <option value="<?php echo esc_attr($i); ?>" <?php selected($filter_year, $i); ?>>
                        <?php echo esc_html($i); ?>
                    </option>
                <?php endfor; ?>
            </select>

            <label for="filter-by-month" class="screen-reader-text"><?php esc_html_e('Filter by month', 'contact-forms'); ?></label>
            <select name="month" id="filter-by-month">
                <option value="-1"><?php esc_html_e('All months', 'contact-forms'); ?></option>
                <?php for ($i = 1; $i <= 12; $i++) : ?>
                    <option value="<?php echo esc_attr($i); ?>" <?php selected($filter_month, $i); ?>>
                        <?php echo esc_html($i); ?>
                    </option>
                <?php endfor; ?>
            </select>

            <?php
            submit_button(__('Filter', 'contact-forms'), '', 'filter_action', false);

            // Tooltip: name the columns the button keeps, with their real
            // (localized) labels; skip keys absent on this site (e.g. no
            // email field submitted yet).
            $all_columns      = $this->get_columns();
            $essential_labels = array();
            foreach (self::essential_columns() as $essential_key) {
                if (isset($all_columns[$essential_key])) {
                    $essential_labels[] = wp_strip_all_tags($all_columns[$essential_key]);
                }
            }
            /* translators: %s: comma-separated list of column names */
            $essential_title = sprintf(__('Keep only these columns visible: %s', 'contact-forms'), implode(', ', $essential_labels));
            // Second line (rendered as such: title attributes honor newlines):
            // the main columns above are fixed, but the field ones follow the
            // per-field flag, so name the option and where it is edited.
            $essential_title .= "\n" . __('Choose which field columns are kept: edit the field on the Fields page and enable "Show in essential columns".', 'contact-forms');
            ?>
            <a onclick="setEssentialColumns();" class="button" title="<?php echo esc_attr($essential_title); ?>"><?php esc_html_e('Essential Columns', 'contact-forms'); ?></a>
            <a onclick="accuaToggleAllRows(true);" class="button accua-toggle-rows" title="<?php esc_attr_e('Expand All Rows', 'contact-forms'); ?>" aria-label="<?php esc_attr_e('Expand All Rows', 'contact-forms'); ?>"><span class="accua-rows-icon accua-rows-icon-expand"></span></a>
            <a onclick="accuaToggleAllRows(false);" class="button accua-toggle-rows" title="<?php esc_attr_e('Collapse All Rows', 'contact-forms'); ?>" aria-label="<?php esc_attr_e('Collapse All Rows', 'contact-forms'); ?>"><span class="accua-rows-icon accua-rows-icon-collapse"></span></a>
        </div>

        <?php
    }
}

function accua_forms_submissions_list_page($head = false){
    static $listTable = null;

    global $wpdb;
    if ($listTable === null) {
      $listTable = new Accua_Forms_Submissions_List_Table();

      // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below before processing
      if (isset($_POST['action'])) {
        // Verify nonce before processing bulk action
        check_admin_referer('bulk-submissions');
        
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
        if ($_POST['action'] === 'trash' && !empty($_POST['submission']) && is_array($_POST['submission'])) {
          $trashed = array();
          // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above, value cast to int
          foreach($_POST['submission'] as $i) {
            $i = (int) $i;
            $trashed[$i] = $i;
          }
          
          // Convert array keys to values for use in the prepared statement
          $trashed_values = array_keys($trashed);
          $placeholders = implode(',', array_fill(0, count($trashed_values), '%d'));
          
          // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
          $query = $wpdb->prepare(
              "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id IN ($placeholders)",
              ...$trashed_values
          );
          // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
          // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
          $res = $wpdb->query($query);
          if ($res === false) {
            $listTable->set_message(__("Error moving submissions to trash.", 'contact-forms'));
          } else {
            $listTable->set_message(strtr(__("Moved %res submissions to trash.", 'contact-forms'), array('%res' => $res)));
          }
        }
      }

      wp_enqueue_script('jquery');
      wp_enqueue_script('jquery-ui-core');
      wp_enqueue_script('jquery-ui-sortable');
      wp_enqueue_script('accua-forms-set-lead-status', plugins_url('assets/js/admin/set-lead-status.js', ACCUA_FORMS_FILE ), array( 'jquery' ), ACCUA_FORMS_JS_VERSION, true);

      $listTable->process_bulk_action();
      $listTable->prepare_items();

      // Separate removed fields into their own Screen Options section
      if (!empty($listTable->removed_columns)) {
          $screen_id = $listTable->screen->id;

          // Strip removed columns from the main "Columns" fieldset (priority 10, after WP_List_Table's priority 0)
          add_filter("manage_{$screen_id}_columns", function($columns) use ($listTable) {
              foreach ($listTable->removed_columns as $key => $clean_name) {
                  unset($columns[$key]);
              }
              return $columns;
          }, 10);

          // Render removed columns in a separate collapsible fieldset
          add_filter('screen_settings', function($settings) use ($listTable) {
              $hidden = get_hidden_columns(get_current_screen());
              $count = count($listTable->removed_columns);
              ob_start();
              ?>
              <fieldset class="metabox-prefs">
              <details>
              <summary><?php
                  echo esc_html(sprintf(
                      /* translators: %d: number of removed form fields */
                      _n('%d removed field', '%d removed fields', $count, 'contact-forms'),
                      $count
                  ));
              ?></summary>
              <?php
              foreach ($listTable->removed_columns as $column => $clean_name) {
                  $id = "$column-hide";
                  ?>
                  <label>
                      <input class="hide-column-tog" name="<?php echo esc_attr($id); ?>" type="checkbox" id="<?php echo esc_attr($id); ?>" value="<?php echo esc_attr($column); ?>" <?php checked(!in_array($column, $hidden, true)); ?> />
                      <?php echo esc_html($clean_name); ?>
                  </label>
                  <?php
              }
              ?>
              </details>
              </fieldset>
              <?php
              $settings .= ob_get_clean();
              return $settings;
          });
      }
    }

    if ($head === true) {
      return;
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
    $del = isset($_GET['del']) && $_GET['del'] == 1;
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
    $active_lead_status = isset($_GET['lead_status']) ? (int) $_GET['lead_status'] : null;

    // Build export URL parameters from current filters
    $export_params = '';
    if ( $del ) {
        $export_params .= '&del=1';
    }
    if ( $active_lead_status !== null ) {
        $export_params .= '&lead_status=' . (int) $active_lead_status;
    }

    wp_enqueue_script(
        'accua-forms-expandable-cells',
        plugins_url( 'assets/js/admin/expandable-cells.js', ACCUA_FORMS_FILE ),
        array(),
        ACCUA_FORMS_JS_VERSION,
        true
    );
    wp_enqueue_script(
        'accua-forms-submissions-list',
        plugins_url( 'assets/js/admin/submissions-list.js', ACCUA_FORMS_FILE ),
        array( 'jquery' ),
        ACCUA_FORMS_JS_VERSION,
        true
    );
    wp_localize_script( 'accua-forms-submissions-list', 'accuaSubmissionsList', array(
        'exportParams'  => $export_params,
        'exportNonce'   => wp_create_nonce( 'accua_forms_export_excel' ),
        'essentialCols' => Accua_Forms_Submissions_List_Table::essential_columns(),
    ) );
?>
    <div id="accua_forms_submissions_list_page" class="accua_forms_admin_page wrap">

        <h1><?php esc_html_e('Contact Forms - Submissions', 'contact-forms'); ?></h1>

        <?php if ($listTable->get_message() !== null) : ?>
            <div class="updated"><p><?php echo esc_html($listTable->get_message()); ?></p></div>
        <?php endif; ?>

        <p><?php esc_html_e('Use the screen options to add or remove columns from the table below. Only the visible columns will be exported.', 'contact-forms'); ?></p>
        <p>
            <a onclick="set_parameter(1);" id="esporta_link_visible_column" class="button-primary"><?php esc_html_e('Export visible columns to Excel', 'contact-forms'); ?></a>
            <a onclick="set_parameter(0);" id="esporta_link_all_column" class="button-primary"><?php esc_html_e('Export all columns to Excel', 'contact-forms'); ?></a>
        </p>

        <?php $listTable->views(); ?>

        <form id="submissions-filter" method="get">
            <input type="hidden" name="page" value="accua_forms_submissions_list" />
            <?php if ($del) : ?>
                <input type="hidden" name="del" value="1" />
            <?php endif; ?>
            <?php if ($active_lead_status !== null) : ?>
                <input type="hidden" name="lead_status" value="<?php echo esc_attr($active_lead_status); ?>" />
            <?php endif; ?>

            <?php $listTable->search_box(__('Search', 'contact-forms'), 'search_id'); ?>
            <?php $listTable->display(); ?>
        </form>
    </div>

    <?php
}


```
