PluginProbe
Contact Forms by Cimatti / 2.2.32
Contact Forms by Cimatti v2.2.32
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 2.2.32, at admin/submissions-list-page.php

925 lines 45.1 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 $title = sprintf('<a href="%s"><strong>%s</strong></a>', esc_url($view_url), esc_html($item['ID']));
64
65 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
66 $del = isset($_GET['del']) && $_GET['del'] == 1;
67 $actions = array(
68 'view' => sprintf('<a href="%s">%s</a>', esc_url($view_url), esc_html__('View', 'contact-forms')),
69 );
70 if ($del) {
71 $restore_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&del=1&action=restore&submission[]=' . $item['ID']), 'bulk-submissions');
72 $actions['restore'] = sprintf('<a href="%s">%s</a>', esc_url($restore_url), esc_html__('Restore', 'contact-forms'));
73 $shred_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&del=1&action=shred&submission[]=' . $item['ID']), 'bulk-submissions');
74 $actions['shred'] = 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'));
75 } else {
76 $trash_url = wp_nonce_url(admin_url('admin.php?page=accua_forms_submissions_list&action=delete&submission[]=' . $item['ID']), 'bulk-submissions');
77 $actions['trash'] = sprintf('<a href="%s" class="submitdelete">%s</a>', esc_url($trash_url), esc_html__('Move to trash', 'contact-forms'));
78 }
79 return $title . $this->row_actions($actions);
80 }
81
82 function column_singlesub($item){
83 return '<a href="?page=accua_forms_submissions_list&sid='.$item['ID'].'" class="">'.
84 __('Open', 'contact-forms').'</a>';
85 }
86
87 function column_uri( $item ) {
88 $value = $item['uri'] ?? '';
89 $url = $value !== '' ? home_url( $value ) : '';
90 return $this->truncate_long_value( $value, 80, $url );
91 }
92
93 function column_referrer( $item ) {
94 $value = $item['referrer'] ?? '';
95 return $this->truncate_long_value( $value, 80, $value );
96 }
97
98 /**
99 * Render a potentially long value with a native <details> toggle when it exceeds 80 chars.
100 * When $url is provided, the expanded text becomes a clickable link.
101 */
102 private function truncate_long_value( $value, $max = 80, $url = '' ) {
103 if ( $value === '' ) {
104 return '';
105 }
106 if ( $url !== '' ) {
107 // JS handles overflow detection and the expandable widget for URL columns.
108 return '<a href="' . esc_url( $url ) . '" target="_blank" title="' . esc_attr( $value ) . '">' . esc_html( $value ) . '</a>';
109 }
110 // Plain text (no URL): keep PHP-side truncation.
111 if ( mb_strlen( $value ) <= $max ) {
112 return esc_html( $value );
113 }
114 $truncated = mb_substr( $value, 0, $max ) . "\u{2026}";
115 return '<details class="accua-expandable-cell">'
116 . '<summary title="' . esc_attr( $value ) . '"><span>' . esc_html( $truncated ) . '</span></summary>'
117 . esc_html( $value )
118 . '</details>';
119 }
120
121 protected function get_primary_column_name() {
122 return 'ID';
123 }
124
125 function no_items() {
126 esc_html_e('No submissions found.', 'contact-forms');
127 }
128
129 function set_message($single_message) {
130 $this->message=$single_message;
131 }
132
133 function get_message() {
134 if($this->message!=NULL)
135 return $this->message;
136 else
137 return NULL;
138 }
139
140 function get_columns(){
141 global $wpdb;
142 $columns = array(
143 'cb' => '<input type="checkbox" />', //Render a checkbox instead of text
144 'ID' => 'ID',
145 'form_title' => 'Form',
146 'form_id' => __("Form ID", 'contact-forms'),
147 'pid' => __("Page ID", 'contact-forms'),
148 'ip' => 'IP',
149 'uri' => __("Page", 'contact-forms'),
150 'referrer' => __("Referrer", 'contact-forms'),
151 'lang' => __("Language", 'contact-forms'),
152 'created' => __("Opened", 'contact-forms'),
153 'submitted' => __("Submitted", 'contact-forms'),
154 );
155 $query = "SELECT DISTINCT afsv_field_id FROM `{$wpdb->prefix}accua_forms_submissions_values`";
156 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
157 $res = $wpdb->get_col($query);
158
159 $avail_fields = get_option('accua_forms_avail_fields', array());
160 foreach ($res as $col) {
161 if (empty($avail_fields[$col]) || !isset($avail_fields[$col]['name'])) {
162 /* translators: %s: field slug/identifier */
163 $columns['_field_'.$col] = sprintf( __( '%s (removed)', 'contact-forms' ), $col );
164 $this->removed_columns['_field_'.$col] = $col;
165 } else {
166 $columns['_field_'.$col] = $avail_fields[$col]['name'];
167 }
168 }
169 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
170 if(isset($_GET['del']) && $_GET['del']==1){
171
172 } else{
173 $columns['lead_status'] = __('Lead Status', 'contact-forms');
174 $columns['singlesub'] = __('Open', 'contact-forms');
175 }
176 return $columns;
177 }
178
179 function get_bulk_actions() {
180 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view toggle, not a state-changing action
181 if(isset($_GET['del']) && $_GET['del']==1) {
182 $actions = array(
183 'restore' => __('Restore', 'contact-forms'),
184 'shred' => __('Permanently delete', 'contact-forms'),
185 );
186 } else {
187 $actions = array(
188 'delete' => __('Move to trash', 'contact-forms'),
189 'anonymize' => __('Anonymize', 'contact-forms'),
190 );
191 }
192 return $actions;
193 }
194
195 protected function get_views() {
196 $views = array();
197 $base_url = admin_url('admin.php?page=accua_forms_submissions_list');
198
199 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
200 $del = isset($_GET['del']) && $_GET['del'] == 1;
201 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
202 $active_lead_status = isset($_GET['lead_status']) ? (int) $_GET['lead_status'] : null;
203
204 $class = (!$del && $active_lead_status === null) ? 'current' : '';
205 $views['active'] = sprintf(
206 '<a href="%s" class="%s">%s</a> (%s)',
207 esc_url($base_url),
208 $class,
209 esc_html__('Active', 'contact-forms'),
210 number_format_i18n($this->active_items)
211 );
212
213 $class = $del ? 'current' : '';
214 $views['trash'] = sprintf(
215 '<a href="%s" class="%s">%s</a> (%s)',
216 esc_url(add_query_arg('del', '1', $base_url)),
217 $class,
218 esc_html__('Trash', 'contact-forms'),
219 number_format_i18n($this->del_items)
220 );
221
222 $lead_statuses = accua_forms_get_lead_statuses();
223 foreach ($lead_statuses as $lead_status_id => $lead_status_label) {
224 if (!empty($this->items_per_lead_status[$lead_status_id]->n)) {
225 $id = absint($lead_status_id);
226 $class = ($active_lead_status === $id) ? 'current' : '';
227 $views['lead_' . $id] = sprintf(
228 '<a href="%s" class="%s" title="%s">%s</a> (%s)',
229 esc_url(add_query_arg('lead_status', $id, $base_url)),
230 $class,
231 esc_attr__('Lead status', 'contact-forms'),
232 esc_html($lead_status_label),
233 number_format_i18n(absint($this->items_per_lead_status[$lead_status_id]->n))
234 );
235 }
236 }
237
238 return $views;
239 }
240
241 function prepare_items($all=false, $get = array()) {
242 global $wpdb, $hook_suffix;
243
244 if (!$get) {
245 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin list table uses WordPress nonce verification via check_admin_referer
246 $get = stripslashes_deep($_GET);
247 }
248
249 $per_page = $this->get_items_per_page('accua_forms_submissions_per_page', 100);
250 $del = isset($get['del']) && $get['del']==1;
251
252 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
253 $this->active_items = $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_status >= 0");
254 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
255 $this->del_items = $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_status < 0");
256
257 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input
258 $this->items_per_lead_status = $wpdb->get_results("SELECT afs_lead_status AS status, COUNT(*) AS n
259 FROM `{$wpdb->prefix}accua_forms_submissions`
260 WHERE afs_status >= 0
261 GROUP BY afs_lead_status", OBJECT_K);
262
263 $filter = $filter_query_custom_field = "";
264 $search = '';
265
266 if(isset($get['fid']) && ($get['fid'])!=-1) {
267 $filter .= $wpdb->prepare(" AND afs_form_id = %s ", $get['fid']);
268 }
269 if(isset($get['pid']) && ($get['pid']!=-1)) {
270 $filter .= $wpdb->prepare(" AND afs_post_id = %d ", $get['pid']);
271 }
272
273 if(isset($get['year']) && ($get['year']>0)) {
274 $filter .= $wpdb->prepare(" AND year(afs_submitted) = %d ", $get['year']);
275 }
276 if(isset($get['month']) && ($get['month']>0)) {
277 $filter .= $wpdb->prepare(" AND month(afs_submitted) = %d ", $get['month']);
278 }
279
280 if(isset($get['s'])) {
281 $search = trim($get['s']);
282 }
283
284 if (isset($get['date_from']) && $get['date_from'] !== '') {
285 $filter .= $wpdb->prepare(" AND afs_submitted >= %s ", $get['date_from']);
286 }
287 if (isset($get['date_to']) && $get['date_to'] !== '') {
288 $filter .= $wpdb->prepare(" AND afs_submitted < %s ", $get['date_to']);
289 }
290
291
292 $columns = $this->get_columns();
293 $hidden = get_hidden_columns($hook_suffix);
294
295 // Build sortable columns: all main DB columns + all field columns
296 $sortable = array(
297 'ID' => array('ID', true),
298 'form_title' => array('form_title', false),
299 'form_id' => array('form_id', false),
300 'pid' => array('pid', false),
301 'ip' => array('ip', false),
302 'uri' => array('uri', false),
303 'referrer' => array('referrer', false),
304 'lang' => array('lang', false),
305 'created' => array('created', true),
306 'submitted' => array('submitted', true),
307 'lead_status' => array('lead_status', false),
308 );
309 foreach ($columns as $key => $label) {
310 if (strpos($key, '_field_') === 0) {
311 $sortable[$key] = array($key, false);
312 }
313 }
314
315 $this->_column_headers = array($columns, $hidden, $sortable);
316 $current_page = $this->get_pagenum();
317
318 if ($all) {
319 $limit = '';
320 } else {
321 $limit = ($current_page - 1) * $per_page;
322 $limit = $wpdb->prepare("LIMIT %d, %d", $limit, $per_page);
323 }
324
325 $forms_data = get_option('accua_forms_saved_forms', array());
326
327 if ($del) {
328 $afs_status_cond = 'afs_status < 0';
329 } else {
330 $afs_status_cond = 'afs_status >= 0';
331 }
332
333 if (isset($get['lead_status'])) {
334 $afs_lead_status_cond = $wpdb->prepare(" AND afs_lead_status = %d ", $get['lead_status']);
335 } else {
336 $afs_lead_status_cond = '';
337 }
338
339
340 if($search !== '') {
341 // Escape special LIKE characters (% and _) in user input, then wrap with wildcards
342 $like_search = '%' . $wpdb->esc_like($search) . '%';
343 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table prefix is safe
344 $sql_search_where = $wpdb->prepare(
345 " AND (
346 afs_ip LIKE %s
347 OR afs_uri LIKE %s
348 OR afs_referrer LIKE %s
349 OR afs_lang LIKE %s
350 OR afs_created LIKE %s
351 OR afs_submitted LIKE %s
352 OR afs_id LIKE %s
353 OR afs_id IN (
354 SELECT DISTINCT (afsv_sub_id)
355 FROM `{$wpdb->prefix}accua_forms_submissions_values`
356 WHERE afsv_value LIKE %s)
357 ) ",
358 $like_search, $like_search, $like_search, $like_search,
359 $like_search, $like_search, $like_search, $like_search
360 );
361 } else {
362 $sql_search_where = '';
363 }
364
365 // Sorting
366 $orderby_sql = 'afs_id';
367 $order_sql = 'DESC';
368 $sort_join = '';
369 $null_sort = '';
370
371 if (isset($get['order']) && in_array(strtoupper($get['order']), array('ASC', 'DESC'), true)) {
372 $order_sql = strtoupper($get['order']);
373 }
374
375 if (isset($get['orderby']) && $get['orderby'] !== '') {
376 $main_col_map = array(
377 'ID' => 'afs_id',
378 'form_title' => 'afs_form_id',
379 'form_id' => 'afs_form_id',
380 'pid' => 'afs_post_id',
381 'ip' => 'afs_ip',
382 'uri' => 'afs_uri',
383 'referrer' => 'afs_referrer',
384 'lang' => 'afs_lang',
385 'created' => 'afs_created',
386 'submitted' => 'afs_submitted',
387 'lead_status' => 'afs_lead_status',
388 );
389
390 $orderby_param = $get['orderby'];
391 if (isset($main_col_map[$orderby_param])) {
392 $orderby_sql = $main_col_map[$orderby_param];
393 } elseif (strpos($orderby_param, '_field_') === 0 && isset($columns[$orderby_param])) {
394 $field_id = substr($orderby_param, 7); // strip '_field_' prefix
395 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table prefix is safe
396 $sort_join = $wpdb->prepare(
397 " 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 ",
398 $field_id
399 );
400 $orderby_sql = 'sort_field.afsv_value';
401 // Push NULLs (submissions without this field) to the end regardless of sort direction
402 $null_sort = 'sort_field.afsv_value IS NULL,';
403 }
404 }
405
406 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- All sort variables are validated against allowlists or prepared
407 $query1 = "SELECT SQL_CALC_FOUND_ROWS
408 afs_id AS ID,
409 afs_form_id AS form_id,
410 afs_post_id AS pid,
411 afs_ip AS ip,
412 afs_uri AS uri,
413 afs_referrer AS referrer,
414 afs_lang AS lang,
415 afs_created AS created,
416 afs_submitted AS submitted,
417 afs_lead_status AS lead_status
418 FROM `{$wpdb->prefix}accua_forms_submissions`
419 {$sort_join}
420 WHERE {$afs_status_cond} {$afs_lead_status_cond} {$filter} {$sql_search_where}
421 ORDER BY {$null_sort} {$orderby_sql} {$order_sql}, afs_id DESC
422 {$limit}";
423
424 // 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
425 $data1 = $wpdb->get_results($query1, ARRAY_A);
426
427 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Built-in MySQL function
428 $total_items = $wpdb->get_var('SELECT FOUND_ROWS()');
429
430 $data = array();
431 $submissions = array();
432 foreach ($data1 as $row) {
433 $fid = $row['form_id'];
434
435 if (isset($forms_data[$fid]['title']) && (trim($forms_data[$fid]['title']) !== '')) {
436 $row['form_title'] = $forms_data[$fid]['title'];
437 } else {
438 $row['form_title'] = $fid;
439 }
440
441 $sid = (int) $row['ID'];
442
443 foreach($row as $k => $v) {
444 $row[$k] = esc_html($v);
445 }
446
447 $data[$sid] = $row;
448 $submissions[] = $sid;
449 }
450
451 if ($submissions) {
452 // Create dynamic placeholders for each ID
453 $placeholders = implode(',', array_fill(0, count($submissions), '%d'));
454 // 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
455 $query2 = $wpdb->prepare(
456 "SELECT *
457 FROM `{$wpdb->prefix}accua_forms_submissions_values`
458 WHERE afsv_sub_id IN ($placeholders)",
459 ...$submissions
460 );
461 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
462 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
463 $data2 = $wpdb->get_results($query2, OBJECT);
464
465 if ($data2) {
466 foreach ($data2 as $row) {
467 switch ($row->afsv_type) {
468 case 'file' :
469 $fieldid = rawurlencode($row->afsv_field_id);
470 $filename = rawurlencode($row->afsv_value);
471 $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);
472 if ($this->export_xls) {
473 /* imposto token segreto 32 caratteri */
474 $token_xls = accua_forms_generate_download_token($row->afsv_sub_id);
475 $url .= '&html=1&token='.$token_xls;
476 }
477 $url = esc_url($url);
478 $filename = esc_attr($row->afsv_value);
479 $fielddata = "<a href='{$url}' target='_blank'>{$filename}</a>";
480 break;
481 case 'colorpicker':
482 if ($row->afsv_value === '') {
483 $fielddata = '';
484 } else {
485 $value_esc = esc_attr($row->afsv_value);
486 $fielddata = "<span style='color: {$value_esc}'><font color='{$value_esc}'>&#9608;</font></span> $value_esc";
487 }
488 break;
489 case 'password':
490 case 'password-and-confirm':
491 case 'hashed-password':
492 $fielddata = ($row->afsv_value !== '') ? '••••' : '';
493 break;
494 case 'token':
495 $fielddata = '';
496 break;
497 default:
498 $fielddata = esc_attr($row->afsv_value);
499 }
500 $data[$row->afsv_sub_id]['_field_'.$row->afsv_field_id] = $fielddata;
501 }
502 }
503 }
504
505 $this->items = $data;
506
507 $this->set_pagination_args( array(
508 'total_items' => $total_items,
509 'per_page' => $per_page,
510 'total_pages' => ceil($total_items/$per_page)
511 ) );
512
513
514 }
515
516 function process_bulk_action(){
517 $current_action = $this->current_action();
518 if ($current_action) {
519 check_admin_referer('bulk-submissions'); //check nonce generated for 'bulk-'.$this->_args['plural']
520 if ('delete' === $current_action) {
521 $trashed = array();
522 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
523 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
524 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
525 foreach ($_GET['submission'] as $i) {
526 $i = (int)$i;
527 $trashed[$i] = $i;
528 }
529 }
530 if ($trashed) {
531 global $wpdb;
532 // Create placeholders for each ID and prepare the query safely
533 $placeholders = implode(',', array_fill(0, count($trashed), '%d'));
534 // 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
535 $query = $wpdb->prepare(
536 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id IN ($placeholders)",
537 array_values($trashed)
538 );
539 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
540 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
541 $res = $wpdb->query($query);
542 if ($res === false) {
543 $this->set_message(__("Error moving submissions to trash.", 'contact-forms') );
544 } else if ($res == 1) {
545 $this->set_message(__("Moved 1 submission to trash.", 'contact-forms') );
546 } else {
547 $this->set_message(strtr(__("Moved %res submissions to trash.", 'contact-forms'), array('%res' => $res)) );
548 }
549 } else {
550 $this->set_message(__("No submission selected.", 'contact-forms') );
551 }
552 } else if ('shred' === $current_action) {
553 $shredded = array();
554 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
555 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
556 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
557 foreach ($_GET['submission'] as $i) {
558 $i = (int)$i;
559 $shredded[$i] = $i;
560 }
561 }
562 if ($shredded) {
563 $shredded = array_values($shredded);
564 $placeholders = implode(',', array_fill(0, count($shredded), '%d'));
565 global $wpdb;
566 // 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
567 $query = $wpdb->prepare(
568 "DELETE FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_id IN ($placeholders)",
569 ...$shredded
570 );
571 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
572 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
573 $res = $wpdb->query($query);
574 if ($res === false) {
575 $this->set_message(__('Error deleting submissions.', 'contact-forms') );
576 } else {
577 // 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
578 $query2 = $wpdb->prepare(
579 "DELETE FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id IN ($placeholders)",
580 ...$shredded
581 );
582 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
583 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
584 $res2 = $wpdb->query($query2);
585 if ($res == 1) {
586 $this->set_message(__("Deleted 1 submission.", 'contact-forms') );
587 } else {
588 $this->set_message(strtr(__("Deleted %res submissions.", 'contact-forms'), array('%res' => $res)) );
589 }
590 }
591 } else {
592 $this->set_message(__("No submission selected.", 'contact-forms') );
593 }
594 } else if ('anonymize' === $current_action) {
595 $anonymized = array();
596 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
597 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
598 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
599 foreach ($_GET['submission'] as $i) {
600 $i = (int)$i;
601 $anonymized[$i] = $i;
602 }
603 }
604 if ($anonymized) {
605 $count = 0;
606 foreach ($anonymized as $sub_id) {
607 if (accua_forms_erase_submission($sub_id, 'anonymize')) {
608 $count++;
609 }
610 }
611 if ($count == 1) {
612 $this->set_message(__('Anonymized 1 submission.', 'contact-forms') );
613 } else {
614 $this->set_message(strtr(__('Anonymized %res submissions.', 'contact-forms'), array('%res' => $count)) );
615 }
616 } else {
617 $this->set_message(__('No submission selected.', 'contact-forms') );
618 }
619 } else if ('restore' === $current_action) {
620 $restored = array();
621 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
622 if ((!empty($_GET['submission'])) && is_array($_GET['submission'])) {
623 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int
624 foreach ($_GET['submission'] as $i) {
625 $i = (int)$i;
626 $restored[$i] = $i;
627 }
628 }
629 if ($restored) {
630 $restored = array_values($restored);
631 $placeholders = implode(',', array_fill(0, count($restored), '%d'));
632 global $wpdb;
633 // 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
634 $query = $wpdb->prepare(
635 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = 0 WHERE afs_id IN ($placeholders)",
636 ...$restored
637 );
638 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
639 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
640 $res = $wpdb->query($query);
641 if ($res === false) {
642 $this->set_message(__("Error restoring submissions.", 'contact-forms') );
643 } else if ($res == 1) {
644 $this->set_message(__("Restored 1 submission.", 'contact-forms') );
645 } else {
646 $this->set_message(strtr(__("Restored %res submissions.", 'contact-forms'), array('%res' => $res)) );
647 }
648 } else {
649 $this->set_message(__('No submission selected.', 'contact-forms') );
650 }
651 }
652 }
653 }
654
655 /**
656 * Render filter dropdowns and export buttons above the table.
657 * @param string $which 'top' or 'bottom'
658 */
659 protected function extra_tablenav($which) {
660 if ($which !== 'top') {
661 return;
662 }
663
664 global $wpdb;
665
666 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter state
667 $get = stripslashes_deep($_GET);
668
669 $filter_form = isset($get['fid']) && $get['fid'] != '-1' ? sanitize_text_field($get['fid']) : null;
670 $filter_post = isset($get['pid']) && $get['pid'] != '-1' ? (int) $get['pid'] : null;
671 $filter_year = isset($get['year']) && $get['year'] > 0 ? (int) $get['year'] : null;
672 $filter_month = isset($get['month']) && $get['month'] > 0 ? (int) $get['month'] : null;
673
674 // Forms data
675 $forms_data = get_option('accua_forms_saved_forms', array());
676 $saved_forms_id = array_keys($forms_data);
677
678 if (!empty($saved_forms_id)) {
679 $placeholders = implode(',', array_fill(0, count($saved_forms_id), '%s'));
680 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic placeholders
681 $query = $wpdb->prepare(
682 "SELECT DISTINCT afs_form_id FROM {$wpdb->prefix}accua_forms_submissions WHERE afs_form_id NOT IN ($placeholders) AND afs_status >= 0",
683 ...$saved_forms_id
684 );
685 // phpcs:enable
686 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
687 $deleted_form_ids = $wpdb->get_col($query);
688 } else {
689 $deleted_form_ids = array();
690 }
691
692 // Pages
693 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Static query
694 $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");
695
696 // Year range
697 $cur_year = (int) wp_date('Y');
698 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Static query
699 $min_year = (int) $wpdb->get_var("SELECT YEAR(MIN(afs_submitted)) FROM {$wpdb->prefix}accua_forms_submissions");
700 if ($min_year <= 0) {
701 $min_year = $cur_year;
702 }
703 ?>
704 <div class="alignleft actions">
705 <label for="filter-by-page" class="screen-reader-text"><?php esc_html_e('Filter by page', 'contact-forms'); ?></label>
706 <select name="pid" id="filter-by-page">
707 <option value="-1"><?php esc_html_e('Show all pages', 'contact-forms'); ?></option>
708 <?php foreach ($page_ids as $post_id) : ?>
709 <option value="<?php echo esc_attr($post_id); ?>" <?php selected($filter_post, (int) $post_id); ?>>
710 <?php echo esc_html(get_the_title($post_id)); ?>
711 </option>
712 <?php endforeach; ?>
713 </select>
714
715 <label for="filter-by-form" class="screen-reader-text"><?php esc_html_e('Filter by form', 'contact-forms'); ?></label>
716 <select name="fid" id="filter-by-form">
717 <option value="-1"><?php esc_html_e('Show all forms', 'contact-forms'); ?></option>
718 <?php foreach ($forms_data as $form_key => $form) : ?>
719 <option value="<?php echo esc_attr($form_key); ?>" <?php selected($filter_form, $form_key); ?>>
720 <?php echo esc_html(!empty($form['title']) ? $form['title'] : $form_key); ?>
721 </option>
722 <?php endforeach; ?>
723 <?php foreach ($deleted_form_ids as $del_form_id) : ?>
724 <option value="<?php echo esc_attr($del_form_id); ?>" <?php selected($filter_form, $del_form_id); ?>>
725 <?php echo esc_html($del_form_id); ?> (del)
726 </option>
727 <?php endforeach; ?>
728 </select>
729
730 <label for="filter-by-year" class="screen-reader-text"><?php esc_html_e('Filter by year', 'contact-forms'); ?></label>
731 <select name="year" id="filter-by-year">
732 <option value="-1"><?php esc_html_e('All years', 'contact-forms'); ?></option>
733 <?php for ($i = $min_year; $i <= $cur_year; $i++) : ?>
734 <option value="<?php echo esc_attr($i); ?>" <?php selected($filter_year, $i); ?>>
735 <?php echo esc_html($i); ?>
736 </option>
737 <?php endfor; ?>
738 </select>
739
740 <label for="filter-by-month" class="screen-reader-text"><?php esc_html_e('Filter by month', 'contact-forms'); ?></label>
741 <select name="month" id="filter-by-month">
742 <option value="-1"><?php esc_html_e('All months', 'contact-forms'); ?></option>
743 <?php for ($i = 1; $i <= 12; $i++) : ?>
744 <option value="<?php echo esc_attr($i); ?>" <?php selected($filter_month, $i); ?>>
745 <?php echo esc_html($i); ?>
746 </option>
747 <?php endfor; ?>
748 </select>
749
750 <?php submit_button(__('Filter', 'contact-forms'), '', 'filter_action', false); ?>
751 <a onclick="setEssentialColumns();" class="button"><?php esc_html_e('Essential Columns', 'contact-forms'); ?></a>
752 </div>
753
754 <?php
755 }
756 }
757
758 function accua_forms_submissions_list_page($head = false){
759 static $listTable = null;
760
761 global $wpdb;
762 if ($listTable === null) {
763 $listTable = new Accua_Forms_Submissions_List_Table();
764
765 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below before processing
766 if (isset($_POST['action'])) {
767 // Verify nonce before processing bulk action
768 check_admin_referer('bulk-submissions');
769
770 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above
771 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int
772 if ($_POST['action'] === 'trash' && !empty($_POST['submission']) && is_array($_POST['submission'])) {
773 $trashed = array();
774 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above, value cast to int
775 foreach($_POST['submission'] as $i) {
776 $i = (int) $i;
777 $trashed[$i] = $i;
778 }
779
780 // Convert array keys to values for use in the prepared statement
781 $trashed_values = array_keys($trashed);
782 $placeholders = implode(',', array_fill(0, count($trashed_values), '%d'));
783
784 // 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
785 $query = $wpdb->prepare(
786 "UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id IN ($placeholders)",
787 ...$trashed_values
788 );
789 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
790 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above
791 $res = $wpdb->query($query);
792 if ($res === false) {
793 $listTable->set_message(__("Error moving submissions to trash.", 'contact-forms'));
794 } else {
795 $listTable->set_message(strtr(__("Moved %res submissions to trash.", 'contact-forms'), array('%res' => $res)));
796 }
797 }
798 }
799
800 wp_enqueue_script('jquery');
801 wp_enqueue_script('jquery-ui-core');
802 wp_enqueue_script('jquery-ui-sortable');
803 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);
804
805 $listTable->process_bulk_action();
806 $listTable->prepare_items();
807
808 // Separate removed fields into their own Screen Options section
809 if (!empty($listTable->removed_columns)) {
810 $screen_id = $listTable->screen->id;
811
812 // Strip removed columns from the main "Columns" fieldset (priority 10, after WP_List_Table's priority 0)
813 add_filter("manage_{$screen_id}_columns", function($columns) use ($listTable) {
814 foreach ($listTable->removed_columns as $key => $clean_name) {
815 unset($columns[$key]);
816 }
817 return $columns;
818 }, 10);
819
820 // Render removed columns in a separate collapsible fieldset
821 add_filter('screen_settings', function($settings) use ($listTable) {
822 $hidden = get_hidden_columns(get_current_screen());
823 $count = count($listTable->removed_columns);
824 ob_start();
825 ?>
826 <fieldset class="metabox-prefs">
827 <details>
828 <summary><?php
829 echo esc_html(sprintf(
830 /* translators: %d: number of removed form fields */
831 _n('%d removed field', '%d removed fields', $count, 'contact-forms'),
832 $count
833 ));
834 ?></summary>
835 <?php
836 foreach ($listTable->removed_columns as $column => $clean_name) {
837 $id = "$column-hide";
838 ?>
839 <label>
840 <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)); ?> />
841 <?php echo esc_html($clean_name); ?>
842 </label>
843 <?php
844 }
845 ?>
846 </details>
847 </fieldset>
848 <?php
849 $settings .= ob_get_clean();
850 return $settings;
851 });
852 }
853 }
854
855 if ($head === true) {
856 return;
857 }
858
859 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
860 $del = isset($_GET['del']) && $_GET['del'] == 1;
861 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing
862 $active_lead_status = isset($_GET['lead_status']) ? (int) $_GET['lead_status'] : null;
863
864 // Build export URL parameters from current filters
865 $export_params = '';
866 if ( $del ) {
867 $export_params .= '&del=1';
868 }
869 if ( $active_lead_status !== null ) {
870 $export_params .= '&lead_status=' . (int) $active_lead_status;
871 }
872
873 wp_enqueue_script(
874 'accua-forms-expandable-cells',
875 plugins_url( 'assets/js/admin/expandable-cells.js', ACCUA_FORMS_FILE ),
876 array(),
877 ACCUA_FORMS_JS_VERSION,
878 true
879 );
880 wp_enqueue_script(
881 'accua-forms-submissions-list',
882 plugins_url( 'assets/js/admin/submissions-list.js', ACCUA_FORMS_FILE ),
883 array( 'jquery' ),
884 ACCUA_FORMS_JS_VERSION,
885 true
886 );
887 wp_localize_script( 'accua-forms-submissions-list', 'accuaSubmissionsList', array(
888 'exportParams' => $export_params,
889 'exportNonce' => wp_create_nonce( 'accua_forms_export_excel' ),
890 ) );
891 ?>
892 <div id="accua_forms_submissions_list_page" class="accua_forms_admin_page wrap">
893
894 <h1><?php esc_html_e('Contact Forms - Submissions', 'contact-forms'); ?></h1>
895
896 <?php if ($listTable->get_message() !== null) : ?>
897 <div class="updated"><p><?php echo esc_html($listTable->get_message()); ?></p></div>
898 <?php endif; ?>
899
900 <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>
901 <p>
902 <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>
903 <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>
904 </p>
905
906 <?php $listTable->views(); ?>
907
908 <form id="submissions-filter" method="get">
909 <input type="hidden" name="page" value="accua_forms_submissions_list" />
910 <?php if ($del) : ?>
911 <input type="hidden" name="del" value="1" />
912 <?php endif; ?>
913 <?php if ($active_lead_status !== null) : ?>
914 <input type="hidden" name="lead_status" value="<?php echo esc_attr($active_lead_status); ?>" />
915 <?php endif; ?>
916
917 <?php $listTable->search_box(__('Search', 'contact-forms'), 'search_id'); ?>
918 <?php $listTable->display(); ?>
919 </form>
920 </div>
921
922 <?php
923 }
924
925