PluginProbe
Contact Forms by Cimatti / trunk
Contact Forms by Cimatti vtrunk
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / admin / submissions-list-page.php

submissions-list-page.php in Contact Forms by Cimatti trunk, at admin/submissions-list-page.php

1,157 lines 60.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) exit;
3
4 if(!class_exists('WP_List_Table')){
5 require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
6 }
7
8 class Accua_Forms_Submissions_List_Table extends WP_List_Table {
9
10 var $message = NULL;
11 var $active_items = 0;
12 var $del_items = 0;
13
14 var $items_per_lead_status = array();
15
16 public $export_xls = false;
17 public $removed_columns = array();
18
19 function __construct(){
20 global $status, $page;
21 $this->message = '';
22 parent::__construct( array(
23 'singular' => 'submission',
24 'plural' => 'submissions',
25 'ajax' => false
26 ) );
27 }
28
29 function get_num_of_active_items () {
30 return $this->active_items;
31 }
32
33 function get_num_of_del_items () {
34 return $this->del_items;
35 }
36
37 function get_items_per_lead_status() {
38 return $this->items_per_lead_status;
39 }
40
41 function column_default($item, $column_name){
42 if (isset($item[$column_name])) {
43 return $item[$column_name];
44 } else {
45 return '';
46 }
47 }
48
49 function column_cb($item){
50 return sprintf(
51 '<input type="checkbox" name="%1$s[]" value="%2$s" />',
52 /*$1%s*/ $this->_args['singular'],
53 /*$2%s*/ $item['ID']
54 );
55 }
56
57 function column_lead_status($item) {
58 return accua_forms_select_lead_status($item['ID'], $item['lead_status']);
59 }
60
61 function column_ID($item) {
62 $view_url = admin_url('admin.php?page=accua_forms_submissions_list&sid=' . $item['ID']);
63 return sprintf('<a href="%s"><strong>%s</strong></a>', esc_url($view_url), esc_html($item['ID']));
64 }
65
66 function column_singlesub($item){
67 $view_url = admin_url('admin.php?page=accua_forms_submissions_list&sid=' . $item['ID']);
68
69 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
70 $del = isset($_GET['del']) && $_GET['del'] == 1;
71 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
72 $spam_view = !$del && isset($_GET['lead_status']) && (int) $_GET['lead_status'] === -1;
73
74 $links = array(
75 sprintf('<a href="%s">%s</a>', esc_url($view_url), esc_html__('Open', 'contact-forms')),
76 );
77 if ($del) {
78 $restore_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&del=1&action=restore&submission[]=' . $item['ID']), 'bulk-submissions');
79 $links[] = sprintf('<a href="%s">%s</a>', esc_url($restore_url), esc_html__('Restore', 'contact-forms'));
80 $shred_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&del=1&action=shred&submission[]=' . $item['ID']), 'bulk-submissions');
81 $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'));
82 } elseif ($spam_view) {
83 $unspam_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&lead_status=-1&action=unspam&submission[]=' . $item['ID']), 'bulk-submissions');
84 $links[] = sprintf('<a href="%s">%s</a>', esc_url($unspam_url), esc_html__('Not spam', 'contact-forms'));
85 $trash_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&lead_status=-1&action=delete&submission[]=' . $item['ID']), 'bulk-submissions');
86 $links[] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($trash_url), esc_html_x('Trash', 'row action', 'contact-forms'));
87 $shred_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&lead_status=-1&action=shred&submission[]=' . $item['ID']), 'bulk-submissions');
88 $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'));
89 } else {
90 $trash_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&action=delete&submission[]=' . $item['ID']), 'bulk-submissions');
91 $links[] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($trash_url), esc_html_x('Trash', 'row action', 'contact-forms'));
92 $spam_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&action=spam&submission[]=' . $item['ID']), 'bulk-submissions');
93 $links[] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($spam_url), esc_html_x('Spam', 'row action', 'contact-forms'));
94 }
95 return implode(' | ', $links);
96 }
97
98 /**
99 * Drop the core 'fixed' class: with dynamic per-field columns, content-based
100 * (auto) column sizing reads better than evenly divided fixed widths.
101 */
102 protected function get_table_classes() {
103 return array_diff(parent::get_table_classes(), array('fixed'));
104 }
105
106 /**
107 * Wrap the table - and only the table - in a horizontal scroll container.
108 *
109 * With 'fixed' dropped above the table sizes to its content, so once
110 * enough columns are on it is wider than the screen. Unwrapped, that made
111 * the *document* scroll sideways: the filters, the pagination and the
112 * heading slid out of view, the fixed admin bar stopped short of the
113 * content, and where WordPress does not add body.sticky-menu (it only does
114 * when the admin menu is shorter than the page, so tall menus miss out)
115 * #adminmenuwrap sat in normal flow and slid away too, while the fixed
116 * #adminmenuback stayed behind as an empty coloured strip.
117 *
118 * submissions-list.js gives this box a sticky scrollbar pinned to the
119 * bottom of the viewport - its own one sits at the foot of a table that
120 * can be a hundred rows tall, far below the fold.
121 *
122 * Buffering parent::display() rather than copying the core table markup
123 * keeps the override safe across WordPress versions: print_table_description()
124 * only exists since 6.4 and the plugin still supports 5.9.
125 */
126 public function display() {
127 ob_start();
128 parent::display();
129 $html = ob_get_clean();
130
131 $start = strpos($html, '<table');
132 $end = strrpos($html, '</table>');
133
134 if ($start === false || $end === false || $end < $start) {
135 // Unexpected markup: emit it untouched rather than mangle it.
136 echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WP_List_Table display() outputs safe HTML
137 return;
138 }
139
140 $end += strlen('</table>');
141
142 // 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.
143 echo substr($html, 0, $start)
144 . '<div class="accua-table-scroll">'
145 . substr($html, $start, $end - $start)
146 . '</div>'
147 . substr($html, $end);
148 // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
149 }
150
151 /**
152 * Column keys kept visible by the "Essential Columns" toolbar button.
153 *
154 * Actions, ID, Form and Submitted are always essential; a field column is
155 * essential when its definition has the "Show in essential columns" flag
156 * (Fields page checkbox, since 2.2.47 - the upgrade migration flags the
157 * email field, the only field column of the previously hardcoded list).
158 *
159 * Single source of truth: used for the button tooltip here and mirrored
160 * to setEssentialColumns() in submissions-list.js via wp_localize_script.
161 */
162 public static function essential_columns() {
163 $essential = array('singlesub', 'ID', 'form_title', 'submitted');
164 $avail_fields = get_option('accua_forms_avail_fields', array());
165 if (is_array($avail_fields)) {
166 foreach ($avail_fields as $slug => $field_data) {
167 if (!empty($field_data['essential_column'])) {
168 $essential[] = '_field_' . $slug;
169 }
170 }
171 }
172 return $essential;
173 }
174
175 function column_uri( $item ) {
176 $value = $item['uri'] ?? '';
177 $url = $value !== '' ? home_url( $value ) : '';
178 return $this->truncate_long_value( $value, 80, $url );
179 }
180
181 function column_referrer( $item ) {
182 $value = $item['referrer'] ?? '';
183 return $this->truncate_long_value( $value, 80, $value );
184 }
185
186 /**
187 * Render a URL column value as a clickable link. The cell is CSS-truncated
188 * (max-width + ellipsis) and expandable-cells.js turns cells that overflow
189 * into the [+] expandable widget client-side. Both callers always pass a
190 * non-empty $url for a non-empty $value, so no server-side truncation runs
191 * (the old plain-text <details> branch was unreachable and duplicated the
192 * text, which made browser find-in-page count matches twice).
193 */
194 private function truncate_long_value( $value, $max = 80, $url = '' ) {
195 if ( $value === '' ) {
196 return '';
197 }
198 return '<a href="' . esc_url( $url ) . '" target="_blank" title="' . esc_attr( $value ) . '">' . esc_html( $value ) . '</a>';
199 }
200
201 protected function get_primary_column_name() {
202 return 'ID';
203 }
204
205 function no_items() {
206 esc_html_e('No submissions found.', 'contact-forms');
207 }
208
209 function set_message($single_message) {
210 $this->message=$single_message;
211 }
212
213 function get_message() {
214 if($this->message!=NULL)
215 return $this->message;
216 else
217 return NULL;
218 }
219
220 function get_columns(){
221 global $wpdb;
222 $columns = array(
223 'cb' => '<input type="checkbox" />', //Render a checkbox instead of text
224 'singlesub' => __('Actions', 'contact-forms'),
225 'ID' => 'ID',
226 'form_title' => 'Form',
227 'form_id' => __("Form ID", 'contact-forms'),
228 'pid' => __("Page ID", 'contact-forms'),
229 'ip' => 'IP',
230 'uri' => __("Page", 'contact-forms'),
231 'referrer' => __("Referrer", 'contact-forms'),
232 'lang' => __("Language", 'contact-forms'),
233 'created' => __("Opened", 'contact-forms'),
234 'submitted' => __("Submitted", 'contact-forms'),
235 );
236 $query = "SELECT DISTINCT afsv_field_id FROM `{$wpdb->prefix}accua_forms_submissions_values`";
237 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
238 $res = $wpdb->get_col($query);
239
240 $avail_fields = get_option('accua_forms_avail_fields', array());
241 foreach ($res as $col) {
242 // Internal values the plugin stores alongside the submitted ones are
243 // not fields and get no column: the "__" prefix is reserved (the
244 // Fields page refuses it as a slug) and "_accua_" carries plumbing
245 // such as the file download token, whose value has no business being
246 // listed - or exported. The single submission page skips the same two
247 // prefixes.
248 if (str_starts_with($col, '__') || str_starts_with($col, '_accua_')) {
249 continue;
250 }
251 if (empty($avail_fields[$col]) || !isset($avail_fields[$col]['name'])) {
252 /* translators: %s: field slug/identifier */
253 $columns['_field_'.$col] = sprintf( __( '%s (removed)', 'contact-forms' ), $col );
254 $this->removed_columns['_field_'.$col] = $col;
255 } else {
256 $columns['_field_'.$col] = $avail_fields[$col]['name'];
257 }
258 }
259 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
260 if(isset($_GET['del']) && $_GET['del']==1){
261
262 } else{
263 $columns['lead_status'] = __('Lead Status', 'contact-forms');
264 }
265 return $columns;
266 }
267
268 function get_bulk_actions() {
269 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
270 $del = isset($_GET['del']) && $_GET['del'] == 1;
271 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
272 $spam_view = !$del && isset($_GET['lead_status']) && (int) $_GET['lead_status'] === -1;
273 if ($del) {
274 $actions = array(
275 'restore' => __('Restore', 'contact-forms'),
276 'shred' => __('Permanently delete', 'contact-forms'),
277 );
278 } elseif ($spam_view) {
279 $actions = array(
280 'unspam' => __('Not spam', 'contact-forms'),
281 'delete' => __('Move to trash', 'contact-forms'),
282 'shred' => __('Permanently delete', 'contact-forms'),
283 );
284 } else {
285 $actions = array(
286 'delete' => __('Move to trash', 'contact-forms'),
287 'spam' => __('Mark as spam', 'contact-forms'),
288 'anonymize' => __('Anonymize', 'contact-forms'),
289 );
290 }
291 // The two buttons above the table export the whole filtered view; these
292 // are the same two exports narrowed to the ticked rows, so they carry
293 // the same labels. Offered in every view, since the buttons are too and
294 // the trash and spam views are exactly where a partial export is wanted.
295 $actions['export_visible'] = __('Export visible columns to Excel', 'contact-forms');
296 $actions['export_all'] = __('Export all columns to Excel', 'contact-forms');
297 return $actions;
298 }
299
300 /**
301 * The column keys an export writes, in table order.
302 *
303 * The buttons above the table answer this in the browser, from the Screen
304 * Options checkboxes; a bulk action arrives as a plain form submit, so the
305 * answer has to come from the server instead. The two agree: core saves a
306 * column toggle to the user option as soon as it is clicked, and the
307 * checkbox list is the column list minus the columns core refuses to hide,
308 * which here is only the checkbox column.
309 *
310 * 'singlesub' carries the row actions, links back to this same screen with
311 * nothing to export; the JS drops it too.
312 *
313 * @param bool $only_visible Leave out the columns hidden in Screen Options.
314 * @return array
315 */
316 public function export_column_keys($only_visible) {
317 $keys = array_diff(array_keys($this->get_columns()), array('cb', 'singlesub'));
318 if ($only_visible) {
319 $keys = array_diff($keys, get_hidden_columns($this->screen));
320 }
321 return array_values($keys);
322 }
323
324 protected function get_views() {
325 $views = array();
326 $base_url = admin_url('admin.php?page=accua_forms_submissions_list');
327
328 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
329 $del = isset($_GET['del']) && $_GET['del'] == 1;
330 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
331 $active_lead_status = isset($_GET['lead_status']) ? (int) $_GET['lead_status'] : null;
332
333 $class = (!$del && $active_lead_status === null) ? 'current' : '';
334 $views['active'] = sprintf(
335 '<a href="%s" class="%s">%s</a> (%s)',
336 esc_url($base_url),
337 $class,
338 esc_html__('Active', 'contact-forms'),
339 number_format_i18n($this->active_items)
340 );
341
342 $lead_statuses = accua_forms_get_lead_statuses();
343 foreach ($lead_statuses as $lead_status_id => $lead_status_label) {
344 $id = (int) $lead_status_id;
345 // Spam gets its own always-visible view below (comments-like).
346 if ($id === -1) {
347 continue;
348 }
349 if (!empty($this->items_per_lead_status[$lead_status_id]->n)) {
350 $class = ($active_lead_status === $id) ? 'current' : '';
351 $views['lead_' . $id] = sprintf(
352 '<a href="%s" class="%s" title="%s">%s</a> (%s)',
353 esc_url(add_query_arg('lead_status', $id, $base_url)),
354 $class,
355 esc_attr__('Lead status', 'contact-forms'),
356 esc_html($lead_status_label),
357 number_format_i18n(absint($this->items_per_lead_status[$lead_status_id]->n))
358 );
359 }
360 }
361
362 $spam_count = !empty($this->items_per_lead_status[-1]->n) ? absint($this->items_per_lead_status[-1]->n) : 0;
363 $class = (!$del && $active_lead_status === -1) ? 'current' : '';
364 $views['spam'] = sprintf(
365 '<a href="%s" class="%s">%s</a> (%s)',
366 esc_url(add_query_arg('lead_status', '-1', $base_url)),
367 $class,
368 esc_html__('Spam', 'contact-forms'),
369 number_format_i18n($spam_count)
370 );
371
372 $class = $del ? 'current' : '';
373 $views['trash'] = sprintf(
374 '<a href="%s" class="%s">%s</a> (%s)',
375 esc_url(add_query_arg('del', '1', $base_url)),
376 $class,
377 esc_html__('Trash', 'contact-forms'),
378 number_format_i18n($this->del_items)
379 );
380
381 return $views;
382 }
383
384 function prepare_items($all=false, $get = array()) {
385 global $wpdb, $hook_suffix;
386
387 if (!$get) {
388 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin list table uses WordPress nonce verification via check_admin_referer
389 $get = stripslashes_deep($_GET);
390 }
391
392 $per_page = $this->get_items_per_page('accua_forms_submissions_per_page', 100);
393 $del = isset($get['del']) && $get['del']==1;
394
395 // Active excludes spam (afs_lead_status = -1), like the WordPress comments screen.
396 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
397 $this->active_items = $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_status >= 0 AND afs_lead_status <> -1");
398 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
399 $this->del_items = $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_status < 0");
400
401 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
402 $this->items_per_lead_status = $wpdb->get_results("SELECT afs_lead_status AS status, COUNT(*) AS n
403 FROM `{$wpdb->prefix}accua_forms_submissions`
404 WHERE afs_status >= 0
405 GROUP BY afs_lead_status", OBJECT_K);
406
407 $filter = $filter_query_custom_field = "";
408 $search = '';
409
410 if(isset($get['fid']) && ($get['fid'])!=-1) {
411 $filter .= $wpdb->prepare(" AND afs_form_id = %s ", $get['fid']);
412 }
413 if(isset($get['pid']) && ($get['pid']!=-1)) {
414 $filter .= $wpdb->prepare(" AND afs_post_id = %d ", $get['pid']);
415 }
416
417 if(isset($get['year']) && ($get['year']>0)) {
418 $filter .= $wpdb->prepare(" AND year(afs_submitted) = %d ", $get['year']);
419 }
420 if(isset($get['month']) && ($get['month']>0)) {
421 $filter .= $wpdb->prepare(" AND month(afs_submitted) = %d ", $get['month']);
422 }
423
424 if(isset($get['s'])) {
425 $search = trim($get['s']);
426 }
427
428 if (isset($get['date_from']) && $get['date_from'] !== '') {
429 $filter .= $wpdb->prepare(" AND afs_submitted >= %s ", $get['date_from']);
430 }
431 if (isset($get['date_to']) && $get['date_to'] !== '') {
432 $filter .= $wpdb->prepare(" AND afs_submitted < %s ", $get['date_to']);
433 }
434
435 // Bulk export of the ticked rows. Read only in export mode ($all):
436 // the normal render reads $_GET, where a row action leaves its
437 // submission[] behind, and honoring that there would silently narrow
438 // the table to the single row the action was aimed at.
439 if ($all && !empty($get['accua_export_ids']) && is_array($get['accua_export_ids'])) {
440 $export_ids = array_values(array_unique(array_map('intval', $get['accua_export_ids'])));
441 $placeholders = implode(',', array_fill(0, count($export_ids), '%d'));
442 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders, values cast to int
443 $filter .= $wpdb->prepare(" AND afs_id IN ($placeholders) ", ...$export_ids);
444 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
445 }
446
447 $columns = $this->get_columns();
448 $hidden = get_hidden_columns($hook_suffix);
449
450 // Build sortable columns: all main DB columns + all field columns
451 $sortable = array(
452 'ID' => array('ID', true),
453 'form_title' => array('form_title', false),
454 'form_id' => array('form_id', false),
455 'pid' => array('pid', false),
456 'ip' => array('ip', false),
457 'uri' => array('uri', false),
458 'referrer' => array('referrer', false),
459 'lang' => array('lang', false),
460 'created' => array('created', true),
461 'submitted' => array('submitted', true),
462 'lead_status' => array('lead_status', false),
463 );
464 foreach ($columns as $key => $label) {
465 if (strpos($key, '_field_') === 0) {
466 $sortable[$key] = array($key, false);
467 }
468 }
469
470 $this->_column_headers = array($columns, $hidden, $sortable);
471 $current_page = $this->get_pagenum();
472
473 if ($all) {
474 $limit = '';
475 } else {
476 $limit = ($current_page - 1) * $per_page;
477 $limit = $wpdb->prepare("LIMIT %d, %d", $limit, $per_page);
478 }
479
480 $forms_data = get_option('accua_forms_saved_forms', array());
481
482 if ($del) {
483 $afs_status_cond = 'afs_status < 0';
484 } else {
485 $afs_status_cond = 'afs_status >= 0';
486 }
487
488 if (isset($get['lead_status'])) {
489 $afs_lead_status_cond = $wpdb->prepare(" AND afs_lead_status = %d ", $get['lead_status']);
490 } elseif (!$del) {
491 // Default view hides spam; it lives in its own view (lead_status=-1).
492 $afs_lead_status_cond = ' AND afs_lead_status <> -1 ';
493 } else {
494 $afs_lead_status_cond = '';
495 }
496
497
498 if($search !== '') {
499 // Escape special LIKE characters (% and _) in user input, then wrap with wildcards
500 $like_search = '%' . $wpdb->esc_like($search) . '%';
501 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table prefix is safe
502 $sql_search_where = $wpdb->prepare(
503 " AND (
504 afs_ip LIKE %s
505 OR afs_uri LIKE %s
506 OR afs_referrer LIKE %s
507 OR afs_lang LIKE %s
508 OR afs_created LIKE %s
509 OR afs_submitted LIKE %s
510 OR afs_id LIKE %s
511 OR afs_id IN (
512 SELECT DISTINCT (afsv_sub_id)
513 FROM `{$wpdb->prefix}accua_forms_submissions_values`
514 WHERE afsv_value LIKE %s)
515 ) ",
516 $like_search, $like_search, $like_search, $like_search,
517 $like_search, $like_search, $like_search, $like_search
518 );
519 } else {
520 $sql_search_where = '';
521 }
522
523 // Sorting
524 $orderby_sql = 'afs_id';
525 $order_sql = 'DESC';
526 $sort_join = '';
527 $null_sort = '';
528
529 if (isset($get['order']) && in_array(strtoupper($get['order']), array('ASC', 'DESC'), true)) {
530 $order_sql = strtoupper($get['order']);
531 }
532
533 if (isset($get['orderby']) && $get['orderby'] !== '') {
534 $main_col_map = array(
535 'ID' => 'afs_id',
536 'form_title' => 'afs_form_id',
537 'form_id' => 'afs_form_id',
538 'pid' => 'afs_post_id',
539 'ip' => 'afs_ip',
540 'uri' => 'afs_uri',
541 'referrer' => 'afs_referrer',
542 'lang' => 'afs_lang',
543 'created' => 'afs_created',
544 'submitted' => 'afs_submitted',
545 'lead_status' => 'afs_lead_status',
546 );
547
548 $orderby_param = $get['orderby'];
549 if (isset($main_col_map[$orderby_param])) {
550 $orderby_sql = $main_col_map[$orderby_param];
551 } elseif (strpos($orderby_param, '_field_') === 0 && isset($columns[$orderby_param])) {
552 $field_id = substr($orderby_param, 7); // strip '_field_' prefix
553 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table prefix is safe
554 $sort_join = $wpdb->prepare(
555 " 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 ",
556 $field_id
557 );
558 $orderby_sql = 'sort_field.afsv_value';
559 // Push NULLs (submissions without this field) to the end regardless of sort direction
560 $null_sort = 'sort_field.afsv_value IS NULL,';
561 }
562 }
563
564 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- All sort variables are validated against allowlists or prepared
565 $query1 = "SELECT SQL_CALC_FOUND_ROWS
566 afs_id AS ID,
567 afs_form_id AS form_id,
568 afs_post_id AS pid,
569 afs_ip AS ip,
570 afs_uri AS uri,
571 afs_referrer AS referrer,
572 afs_lang AS lang,
573 afs_created AS created,
574 afs_submitted AS submitted,
575 afs_lead_status AS lead_status
576 FROM `{$wpdb->prefix}accua_forms_submissions`
577 {$sort_join}
578 WHERE {$afs_status_cond} {$afs_lead_status_cond} {$filter} {$sql_search_where}
579 ORDER BY {$null_sort} {$orderby_sql} {$order_sql}, afs_id DESC
580 {$limit}";
581
582 // 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
583 $data1 = $wpdb->get_results($query1, ARRAY_A);
584
585 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Built-in MySQL function
586 $total_items = $wpdb->get_var('SELECT FOUND_ROWS()');
587
588 $data = array();
589 $submissions = array();
590 foreach ($data1 as $row) {
591 $fid = $row['form_id'];
592
593 if (isset($forms_data[$fid]['title']) && (trim($forms_data[$fid]['title']) !== '')) {
594 $row['form_title'] = $forms_data[$fid]['title'];
595 } else {
596 $row['form_title'] = $fid;
597 }
598
599 $sid = (int) $row['ID'];
600
601 foreach($row as $k => $v) {
602 $row[$k] = esc_html($v);
603 }
604
605 $data[$sid] = $row;
606 $submissions[] = $sid;
607 }
608
609 if ($submissions) {
610 // Create dynamic placeholders for each ID
611 $placeholders = implode(',', array_fill(0, count($submissions), '%d'));
612 // 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
613 $query2 = $wpdb->prepare(
614 "SELECT *
615 FROM `{$wpdb->prefix}accua_forms_submissions_values`
616 WHERE afsv_sub_id IN ($placeholders)",
617 ...$submissions
618 );
619 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
620 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
621 $data2 = $wpdb->get_results($query2, OBJECT);
622
623 if ($data2) {
624 foreach ($data2 as $row) {
625 switch ($row->afsv_type) {
626 case 'file' :
627 $fieldid = rawurlencode($row->afsv_field_id);
628 $filename = rawurlencode($row->afsv_value);
629 $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);
630 if ($this->export_xls) {
631 /* imposto token segreto 32 caratteri */
632 $token_xls = accua_forms_generate_download_token($row->afsv_sub_id);
633 $url .= '&html=1&token='.$token_xls;
634 }
635 $url = esc_url($url);
636 $filename = esc_attr($row->afsv_value);
637 $fielddata = "<a href='{$url}' target='_blank'>{$filename}</a>";
638 break;
639 case 'colorpicker':
640 if ($row->afsv_value === '') {
641 $fielddata = '';
642 } else {
643 $value_esc = esc_attr($row->afsv_value);
644 $fielddata = "<span style='color: {$value_esc}'><font color='{$value_esc}'>&#9608;</font></span> $value_esc";
645 }
646 break;
647 case 'password':
648 case 'password-and-confirm':
649 case 'hashed-password':
650 $fielddata = ($row->afsv_value !== '') ? '••••' : '';
651 break;
652 case 'token':
653 $fielddata = '';
654 break;
655 default:
656 $fielddata = esc_attr($row->afsv_value);
657 }
658 $data[$row->afsv_sub_id]['_field_'.$row->afsv_field_id] = $fielddata;
659 }
660 }
661 }
662
663 $this->items = $data;
664
665 $this->set_pagination_args( array(
666 'total_items' => $total_items,
667 'per_page' => $per_page,
668 'total_pages' => ceil($total_items/$per_page)
669 ) );
670
671
672 }
673
674 function process_bulk_action(){
675 $current_action = $this->current_action();
676 if ($current_action) {
677 check_admin_referer('bulk-submissions'); //check nonce generated for 'bulk-'.$this->_args['plural']
678 if ('delete' === $current_action) {
679 $trashed = array();
680 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
681 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
682 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
683 foreach ($_GET['submission'] as $i) {
684 $i = (int)$i;
685 $trashed[$i] = $i;
686 }
687 }
688 if ($trashed) {
689 global $wpdb;
690 // Create placeholders for each ID and prepare the query safely
691 $placeholders = implode(',', array_fill(0, count($trashed), '%d'));
692 // 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
693 $query = $wpdb->prepare(
694 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id IN ($placeholders)",
695 array_values($trashed)
696 );
697 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
698 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
699 $res = $wpdb->query($query);
700 if ($res === false) {
701 $this->set_message(__("Error moving submissions to trash.", 'contact-forms') );
702 } else if ($res == 1) {
703 $this->set_message(__("Moved 1 submission to trash.", 'contact-forms') );
704 } else {
705 $this->set_message(strtr(__("Moved %res submissions to trash.", 'contact-forms'), array('%res' => $res)) );
706 }
707 } else {
708 $this->set_message(__("No submission selected.", 'contact-forms') );
709 }
710 } else if ('shred' === $current_action) {
711 $shredded = array();
712 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
713 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
714 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
715 foreach ($_GET['submission'] as $i) {
716 $i = (int)$i;
717 $shredded[$i] = $i;
718 }
719 }
720 if ($shredded) {
721 $shredded = array_values($shredded);
722 $placeholders = implode(',', array_fill(0, count($shredded), '%d'));
723 global $wpdb;
724 // 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
725 $query = $wpdb->prepare(
726 "DELETE FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_id IN ($placeholders)",
727 ...$shredded
728 );
729 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
730 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
731 $res = $wpdb->query($query);
732 if ($res === false) {
733 $this->set_message(__('Error deleting submissions.', 'contact-forms') );
734 } else {
735 // 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
736 $query2 = $wpdb->prepare(
737 "DELETE FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id IN ($placeholders)",
738 ...$shredded
739 );
740 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
741 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
742 $res2 = $wpdb->query($query2);
743 if ($res == 1) {
744 $this->set_message(__("Deleted 1 submission.", 'contact-forms') );
745 } else {
746 $this->set_message(strtr(__("Deleted %res submissions.", 'contact-forms'), array('%res' => $res)) );
747 }
748 }
749 } else {
750 $this->set_message(__("No submission selected.", 'contact-forms') );
751 }
752 } else if ('spam' === $current_action || 'unspam' === $current_action) {
753 $ids = array();
754 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
755 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
756 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
757 foreach ($_GET['submission'] as $i) {
758 $i = (int)$i;
759 $ids[$i] = $i;
760 }
761 }
762 if ($ids) {
763 $ids = array_values($ids);
764 $new_lead_status = ('spam' === $current_action) ? -1 : 0;
765 $placeholders = implode(',', array_fill(0, count($ids), '%d'));
766 global $wpdb;
767 // 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
768 $query = $wpdb->prepare(
769 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_lead_status = %d WHERE afs_id IN ($placeholders)",
770 $new_lead_status,
771 ...$ids
772 );
773 // 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
774 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
775 $res = $wpdb->query($query);
776 if ($res === false) {
777 $this->set_message(('spam' === $current_action)
778 ? __('Error marking submissions as spam.', 'contact-forms')
779 : __('Error restoring submissions from spam.', 'contact-forms'));
780 } elseif ('spam' === $current_action) {
781 if ($res == 1) {
782 $this->set_message(__('Marked 1 submission as spam.', 'contact-forms'));
783 } else {
784 $this->set_message(strtr(__('Marked %res submissions as spam.', 'contact-forms'), array('%res' => $res)));
785 }
786 } else {
787 if ($res == 1) {
788 $this->set_message(__('1 submission restored from spam.', 'contact-forms'));
789 } else {
790 $this->set_message(strtr(__('%res submissions restored from spam.', 'contact-forms'), array('%res' => $res)));
791 }
792 }
793 } else {
794 $this->set_message(__('No submission selected.', 'contact-forms'));
795 }
796 } else if ('anonymize' === $current_action) {
797 $anonymized = array();
798 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
799 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
800 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
801 foreach ($_GET['submission'] as $i) {
802 $i = (int)$i;
803 $anonymized[$i] = $i;
804 }
805 }
806 if ($anonymized) {
807 $count = 0;
808 foreach ($anonymized as $sub_id) {
809 if (accua_forms_erase_submission($sub_id, 'anonymize')) {
810 $count++;
811 }
812 }
813 if ($count == 1) {
814 $this->set_message(__('Anonymized 1 submission.', 'contact-forms') );
815 } else {
816 $this->set_message(strtr(__('Anonymized %res submissions.', 'contact-forms'), array('%res' => $count)) );
817 }
818 } else {
819 $this->set_message(__('No submission selected.', 'contact-forms') );
820 }
821 } else if ('restore' === $current_action) {
822 $restored = array();
823 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
824 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
825 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
826 foreach ($_GET['submission'] as $i) {
827 $i = (int)$i;
828 $restored[$i] = $i;
829 }
830 }
831 if ($restored) {
832 $restored = array_values($restored);
833 $placeholders = implode(',', array_fill(0, count($restored), '%d'));
834 global $wpdb;
835 // 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
836 $query = $wpdb->prepare(
837 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = 0 WHERE afs_id IN ($placeholders)",
838 ...$restored
839 );
840 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
841 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
842 $res = $wpdb->query($query);
843 if ($res === false) {
844 $this->set_message(__("Error restoring submissions.", 'contact-forms') );
845 } else if ($res == 1) {
846 $this->set_message(__("Restored 1 submission.", 'contact-forms') );
847 } else {
848 $this->set_message(strtr(__("Restored %res submissions.", 'contact-forms'), array('%res' => $res)) );
849 }
850 } else {
851 $this->set_message(__('No submission selected.', 'contact-forms') );
852 }
853 } else if ('export_visible' === $current_action || 'export_all' === $current_action) {
854 // The export itself runs on the load hook, before the response has
855 // started, and dies there with the file: a download has to send its
856 // own headers. This method runs from admin_head, so reaching it at
857 // all means the load hook found nothing ticked to export. WordPress
858 // refuses an empty bulk action in the browser before it is sent, so
859 // this answers the request that never went through that guard.
860 $this->set_message(__('No submission selected.', 'contact-forms') );
861 }
862 }
863 }
864
865 /**
866 * Render filter dropdowns and export buttons above the table.
867 * @param string $which 'top' or 'bottom'
868 */
869 protected function extra_tablenav($which) {
870 if ($which !== 'top') {
871 return;
872 }
873
874 global $wpdb;
875
876 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter state
877 $get = stripslashes_deep($_GET);
878
879 $filter_form = isset($get['fid']) && $get['fid'] != '-1' ? sanitize_text_field($get['fid']) : null;
880 $filter_post = isset($get['pid']) && $get['pid'] != '-1' ? (int) $get['pid'] : null;
881 $filter_year = isset($get['year']) && $get['year'] > 0 ? (int) $get['year'] : null;
882 $filter_month = isset($get['month']) && $get['month'] > 0 ? (int) $get['month'] : null;
883
884 // Forms data
885 $forms_data = get_option('accua_forms_saved_forms', array());
886 $saved_forms_id = array_keys($forms_data);
887
888 if (!empty($saved_forms_id)) {
889 $placeholders = implode(',', array_fill(0, count($saved_forms_id), '%s'));
890 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders
891 $query = $wpdb->prepare(
892 "SELECT DISTINCT afs_form_id FROM {$wpdb->prefix}accua_forms_submissions WHERE afs_form_id NOT IN ($placeholders) AND afs_status >= 0",
893 ...$saved_forms_id
894 );
895 // phpcs:enable
896 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
897 $deleted_form_ids = $wpdb->get_col($query);
898 } else {
899 $deleted_form_ids = array();
900 }
901
902 // Pages
903 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Static query
904 $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");
905
906 // Year range
907 $cur_year = (int) wp_date('Y');
908 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Static query
909 $min_year = (int) $wpdb->get_var("SELECT YEAR(MIN(afs_submitted)) FROM {$wpdb->prefix}accua_forms_submissions");
910 if ($min_year <= 0) {
911 $min_year = $cur_year;
912 }
913 ?>
914 <div class="alignleft actions">
915 <label for="filter-by-page" class="screen-reader-text"><?php esc_html_e('Filter by page', 'contact-forms'); ?></label>
916 <select name="pid" id="filter-by-page">
917 <option value="-1"><?php esc_html_e('Show all pages', 'contact-forms'); ?></option>
918 <?php foreach ($page_ids as $post_id) : ?>
919 <option value="<?php echo esc_attr($post_id); ?>" <?php selected($filter_post, (int) $post_id); ?>>
920 <?php echo esc_html(get_the_title($post_id)); ?>
921 </option>
922 <?php endforeach; ?>
923 </select>
924
925 <label for="filter-by-form" class="screen-reader-text"><?php esc_html_e('Filter by form', 'contact-forms'); ?></label>
926 <select name="fid" id="filter-by-form">
927 <option value="-1"><?php esc_html_e('Show all forms', 'contact-forms'); ?></option>
928 <?php foreach ($forms_data as $form_key => $form) : ?>
929 <option value="<?php echo esc_attr($form_key); ?>" <?php selected($filter_form, $form_key); ?>>
930 <?php echo esc_html(!empty($form['title']) ? $form['title'] : $form_key); ?>
931 </option>
932 <?php endforeach; ?>
933 <?php foreach ($deleted_form_ids as $del_form_id) : ?>
934 <option value="<?php echo esc_attr($del_form_id); ?>" <?php selected($filter_form, $del_form_id); ?>>
935 <?php echo esc_html($del_form_id); ?> (del)
936 </option>
937 <?php endforeach; ?>
938 </select>
939
940 <label for="filter-by-year" class="screen-reader-text"><?php esc_html_e('Filter by year', 'contact-forms'); ?></label>
941 <select name="year" id="filter-by-year">
942 <option value="-1"><?php esc_html_e('All years', 'contact-forms'); ?></option>
943 <?php for ($i = $min_year; $i <= $cur_year; $i++) : ?>
944 <option value="<?php echo esc_attr($i); ?>" <?php selected($filter_year, $i); ?>>
945 <?php echo esc_html($i); ?>
946 </option>
947 <?php endfor; ?>
948 </select>
949
950 <label for="filter-by-month" class="screen-reader-text"><?php esc_html_e('Filter by month', 'contact-forms'); ?></label>
951 <select name="month" id="filter-by-month">
952 <option value="-1"><?php esc_html_e('All months', 'contact-forms'); ?></option>
953 <?php for ($i = 1; $i <= 12; $i++) : ?>
954 <option value="<?php echo esc_attr($i); ?>" <?php selected($filter_month, $i); ?>>
955 <?php echo esc_html($i); ?>
956 </option>
957 <?php endfor; ?>
958 </select>
959
960 <?php
961 submit_button(__('Filter', 'contact-forms'), '', 'filter_action', false);
962
963 // Tooltip: name the columns the button keeps, with their real
964 // (localized) labels; skip keys absent on this site (e.g. no
965 // email field submitted yet).
966 $all_columns = $this->get_columns();
967 $essential_labels = array();
968 foreach (self::essential_columns() as $essential_key) {
969 if (isset($all_columns[$essential_key])) {
970 $essential_labels[] = wp_strip_all_tags($all_columns[$essential_key]);
971 }
972 }
973 /* translators: %s: comma-separated list of column names */
974 $essential_title = sprintf(__('Keep only these columns visible: %s', 'contact-forms'), implode(', ', $essential_labels));
975 // Second line (rendered as such: title attributes honor newlines):
976 // the main columns above are fixed, but the field ones follow the
977 // per-field flag, so name the option and where it is edited.
978 $essential_title .= "\n" . __('Choose which field columns are kept: edit the field on the Fields page and enable "Show in essential columns".', 'contact-forms');
979 ?>
980 <a onclick="setEssentialColumns();" class="button" title="<?php echo esc_attr($essential_title); ?>"><?php esc_html_e('Essential Columns', 'contact-forms'); ?></a>
981 <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>
982 <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>
983 </div>
984
985 <?php
986 }
987 }
988
989 function accua_forms_submissions_list_page($head = false){
990 static $listTable = null;
991
992 global $wpdb;
993 if ($listTable === null) {
994 $listTable = new Accua_Forms_Submissions_List_Table();
995
996 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below before processing
997 if (isset($_POST['action'])) {
998 // Verify nonce before processing bulk action
999 check_admin_referer('bulk-submissions');
1000
1001 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
1002 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
1003 if ($_POST['action'] === 'trash' && !empty($_POST['submission']) && is_array($_POST['submission'])) {
1004 $trashed = array();
1005 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above, value cast to int
1006 foreach($_POST['submission'] as $i) {
1007 $i = (int) $i;
1008 $trashed[$i] = $i;
1009 }
1010
1011 // Convert array keys to values for use in the prepared statement
1012 $trashed_values = array_keys($trashed);
1013 $placeholders = implode(',', array_fill(0, count($trashed_values), '%d'));
1014
1015 // 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
1016 $query = $wpdb->prepare(
1017 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id IN ($placeholders)",
1018 ...$trashed_values
1019 );
1020 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1021 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
1022 $res = $wpdb->query($query);
1023 if ($res === false) {
1024 $listTable->set_message(__("Error moving submissions to trash.", 'contact-forms'));
1025 } else {
1026 $listTable->set_message(strtr(__("Moved %res submissions to trash.", 'contact-forms'), array('%res' => $res)));
1027 }
1028 }
1029 }
1030
1031 wp_enqueue_script('jquery');
1032 wp_enqueue_script('jquery-ui-core');
1033 wp_enqueue_script('jquery-ui-sortable');
1034 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);
1035
1036 $listTable->process_bulk_action();
1037 $listTable->prepare_items();
1038
1039 // Separate removed fields into their own Screen Options section
1040 if (!empty($listTable->removed_columns)) {
1041 $screen_id = $listTable->screen->id;
1042
1043 // Strip removed columns from the main "Columns" fieldset (priority 10, after WP_List_Table's priority 0)
1044 add_filter("manage_{$screen_id}_columns", function($columns) use ($listTable) {
1045 foreach ($listTable->removed_columns as $key => $clean_name) {
1046 unset($columns[$key]);
1047 }
1048 return $columns;
1049 }, 10);
1050
1051 // Render removed columns in a separate collapsible fieldset
1052 add_filter('screen_settings', function($settings) use ($listTable) {
1053 $hidden = get_hidden_columns(get_current_screen());
1054 $count = count($listTable->removed_columns);
1055 ob_start();
1056 ?>
1057 <fieldset class="metabox-prefs">
1058 <details>
1059 <summary><?php
1060 echo esc_html(sprintf(
1061 /* translators: %d: number of removed form fields */
1062 _n('%d removed field', '%d removed fields', $count, 'contact-forms'),
1063 $count
1064 ));
1065 ?></summary>
1066 <?php
1067 foreach ($listTable->removed_columns as $column => $clean_name) {
1068 $id = "$column-hide";
1069 ?>
1070 <label>
1071 <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)); ?> />
1072 <?php echo esc_html($clean_name); ?>
1073 </label>
1074 <?php
1075 }
1076 ?>
1077 </details>
1078 </fieldset>
1079 <?php
1080 $settings .= ob_get_clean();
1081 return $settings;
1082 });
1083 }
1084 }
1085
1086 if ($head === true) {
1087 return;
1088 }
1089
1090 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
1091 $del = isset($_GET['del']) && $_GET['del'] == 1;
1092 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
1093 $active_lead_status = isset($_GET['lead_status']) ? (int) $_GET['lead_status'] : null;
1094
1095 // Build export URL parameters from current filters
1096 $export_params = '';
1097 if ( $del ) {
1098 $export_params .= '&del=1';
1099 }
1100 if ( $active_lead_status !== null ) {
1101 $export_params .= '&lead_status=' . (int) $active_lead_status;
1102 }
1103
1104 wp_enqueue_script(
1105 'accua-forms-expandable-cells',
1106 plugins_url( 'assets/js/admin/expandable-cells.js', ACCUA_FORMS_FILE ),
1107 array(),
1108 ACCUA_FORMS_JS_VERSION,
1109 true
1110 );
1111 wp_enqueue_script(
1112 'accua-forms-submissions-list',
1113 plugins_url( 'assets/js/admin/submissions-list.js', ACCUA_FORMS_FILE ),
1114 array( 'jquery' ),
1115 ACCUA_FORMS_JS_VERSION,
1116 true
1117 );
1118 wp_localize_script( 'accua-forms-submissions-list', 'accuaSubmissionsList', array(
1119 'exportParams' => $export_params,
1120 'exportNonce' => wp_create_nonce( 'accua_forms_export_excel' ),
1121 'essentialCols' => Accua_Forms_Submissions_List_Table::essential_columns(),
1122 ) );
1123 ?>
1124 <div id="accua_forms_submissions_list_page" class="accua_forms_admin_page wrap">
1125
1126 <h1><?php esc_html_e('Contact Forms - Submissions', 'contact-forms'); ?></h1>
1127
1128 <?php if ($listTable->get_message() !== null) : ?>
1129 <div class="updated"><p><?php echo esc_html($listTable->get_message()); ?></p></div>
1130 <?php endif; ?>
1131
1132 <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>
1133 <p>
1134 <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>
1135 <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>
1136 </p>
1137
1138 <?php $listTable->views(); ?>
1139
1140 <form id="submissions-filter" method="get">
1141 <input type="hidden" name="page" value="accua_forms_submissions_list" />
1142 <?php if ($del) : ?>
1143 <input type="hidden" name="del" value="1" />
1144 <?php endif; ?>
1145 <?php if ($active_lead_status !== null) : ?>
1146 <input type="hidden" name="lead_status" value="<?php echo esc_attr($active_lead_status); ?>" />
1147 <?php endif; ?>
1148
1149 <?php $listTable->search_box(__('Search', 'contact-forms'), 'search_id'); ?>
1150 <?php $listTable->display(); ?>
1151 </form>
1152 </div>
1153
1154 <?php
1155 }
1156
1157