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(
'',
/*$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('%s', 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('%s', 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('%s', 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('%s', 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('%s', 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('%s', 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('%s', 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('%s', 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('%s', 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, '
');
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('
');
// 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)
. '
'
. substr($html, $start, $end - $start)
. '
'
. 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 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 '' . esc_html( $value ) . '';
}
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' => '', //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(
'%s (%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(
'%s (%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(
'%s (%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(
'%s (%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 = "{$filename}";
break;
case 'colorpicker':
if ($row->afsv_value === '') {
$fielddata = '';
} else {
$value_esc = esc_attr($row->afsv_value);
$fielddata = "█ $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;
}
?>
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');
?>
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();
?>
$export_params,
'exportNonce' => wp_create_nonce( 'accua_forms_export_excel' ),
'essentialCols' => Accua_Forms_Submissions_List_Table::essential_columns(),
) );
?>