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(); ?>
removed_columns as $column => $clean_name) { $id = "$column-hide"; ?>
$export_params, 'exportNonce' => wp_create_nonce( 'accua_forms_export_excel' ), 'essentialCols' => Accua_Forms_Submissions_List_Table::essential_columns(), ) ); ?>

get_message() !== null) : ?>

get_message()); ?>

views(); ?>
search_box(__('Search', 'contact-forms'), 'search_id'); ?> display(); ?>