esc_html__( 'Entry', '24tt-unrestricted-forms' ), // Singular label
'plural' => esc_html__( 'Entries', '24tt-unrestricted-forms' ), // Plural label
'ajax' => false
) );
}
/**
* Define the columns that are going to be used in the table.
*
* @since 1.0.0
* @return array Array of columns to use with the table.
*/
public function get_columns() {
$columns = array(
'cb' => '', // Render a checkbox for bulk actions
'id' => esc_html__( 'Entry ID', '24tt-unrestricted-forms' ),
'form_name' => esc_html__( 'Form Name', '24tt-unrestricted-forms' ),
'ip_address' => esc_html__( 'IP Address', '24tt-unrestricted-forms' ),
'status' => esc_html__( 'Status', '24tt-unrestricted-forms' ),
'created_at' => esc_html__( 'Date Submitted', '24tt-unrestricted-forms' )
);
return $columns;
}
/**
* Define which columns are sortable.
*
* @since 1.0.0
* @return array Array of sortable columns.
*/
protected function get_sortable_columns() {
$sortable_columns = array(
'id' => array( 'id', false ),
'form_name' => array( 'form_name', false ),
'created_at' => array( 'created_at', true ) // True means it's already sorted by this by default
);
return $sortable_columns;
}
/**
* Fallback method for rendering column data.
*
* @since 1.0.0
* @param array $item A singular item (one full row's worth of data).
* @param string $column_name The name/slug of the column to be processed.
* @return string Text or HTML to be placed inside the column
.
*/
protected function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'ip_address':
return esc_html( $item[ $column_name ] );
case 'status':
// Capitalize the status nicely (e.g., 'unread' -> 'Unread')
return esc_html( ucfirst( $item[ $column_name ] ) );
case 'created_at':
// Format the date based on the WordPress general settings
return esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item[ $column_name ] ) ) );
case 'form_name':
return ! empty( $item['form_name'] ) ? esc_html( $item['form_name'] ) : esc_html__( 'Unknown Form', '24tt-unrestricted-forms' );
default:
// FIX: Removed print_r() as it is flagged as debug code by PCP tool
return '';
}
}
/**
* Render the Entry ID column and attach the row actions (View, Delete).
*
* @since 1.0.0
* @param array $item A singular item.
* @return string ID and row actions HTML.
*/
protected function column_id( $item ) {
// Build the URL to view this specific entry
$view_url = add_query_arg( array( 'page' => 'tt24-forms-entries', 'action' => 'view', 'entry_id' => absint( $item['id'] ) ), admin_url( 'admin.php' ) );
// SECURITY: Create a secure deletion URL with a cryptographic nonce
$delete_url = wp_nonce_url(
add_query_arg( array( 'page' => 'tt24-forms-entries', 'action' => 'delete', 'entry_id' => absint( $item['id'] ) ), admin_url( 'admin.php' ) ),
'tt24_delete_entry_' . $item['id']
);
$actions = array(
'view' => sprintf( '%s', esc_url( $view_url ), esc_html__( 'View Details', '24tt-unrestricted-forms' ) ),
'delete' => sprintf( '%s', esc_url( $delete_url ), esc_js( __( 'Are you sure you want to permanently delete this entry?', '24tt-unrestricted-forms' ) ), esc_html__( 'Delete', '24tt-unrestricted-forms' ) )
);
// Bold the ID and append the actions underneath it
return sprintf( '#%1$s%2$s',
esc_html( $item['id'] ),
$this->row_actions( $actions )
);
}
/**
* Render the bulk action checkbox for each row.
*
* @since 1.0.0
* @param array $item A singular item.
* @return string The HTML for the checkbox.
*/
protected function column_cb( $item ) {
return sprintf(
'',
absint( $item['id'] )
);
}
/**
* Define the bulk actions available for this table.
*
* @since 1.0.0
* @return array Array of bulk actions.
*/
protected function get_bulk_actions() {
$actions = array(
'bulk-delete' => esc_html__( 'Delete Permanently', '24tt-unrestricted-forms' )
);
return $actions;
}
/**
* Process bulk actions and single row actions.
*
* @since 1.0.0
* @return void
*/
public function process_bulk_action() {
global $wpdb;
// SECURITY 1: Verify capability
if ( ! current_user_can( 'view_tt24_entries' ) ) {
return;
}
$table_entries = $wpdb->prefix . 'tt24_entries';
$table_fields = $wpdb->prefix . 'tt24_entry_fields';
// 1. Handle Single Deletion (from the row actions)
// FIX: Ignored nonce warning for read-only 'entry_id' URL param
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'delete' === $this->current_action() && isset( $_GET['entry_id'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$entry_id = absint( $_GET['entry_id'] );
// Verify the nonce specific to this entry
check_admin_referer( 'tt24_delete_entry_' . $entry_id );
// Delete child relational data first, then parent entry
$wpdb->delete( $table_fields, array( 'entry_id' => $entry_id ), array( '%d' ) );
$wpdb->delete( $table_entries, array( 'id' => $entry_id ), array( '%d' ) );
$redirect_url = add_query_arg( array( 'page' => 'tt24-forms-entries', 'deleted' => 1 ), admin_url( 'admin.php' ) );
wp_safe_redirect( $redirect_url );
exit;
}
// 2. Handle Bulk Deletion (from the dropdown)
if ( 'bulk-delete' === $this->current_action() ) {
// Verify the generic bulk nonce
check_admin_referer( 'bulk-' . $this->_args['plural'] );
$delete_ids = isset( $_REQUEST['bulk-delete'] ) ? array_map( 'absint', (array) wp_unslash( $_REQUEST['bulk-delete'] ) ) : array();
if ( ! empty( $delete_ids ) ) {
foreach ( $delete_ids as $id ) {
$wpdb->delete( $table_fields, array( 'entry_id' => $id ), array( '%d' ) );
$wpdb->delete( $table_entries, array( 'id' => $id ), array( '%d' ) );
}
$redirect_url = add_query_arg( array( 'page' => 'tt24-forms-entries', 'deleted' => count( $delete_ids ) ), admin_url( 'admin.php' ) );
wp_safe_redirect( $redirect_url );
exit;
}
}
}
/**
* Prepare the data for display.
*
* @since 1.0.0
* @return void
*/
public function prepare_items() {
global $wpdb;
$table_entries = $wpdb->prefix . 'tt24_entries';
$table_forms = $wpdb->prefix . 'tt24_forms';
$per_page = 20;
$current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page;
$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );
// Process bulk actions before querying data
$this->process_bulk_action();
$allowed_orderby = array( 'id', 'form_name', 'created_at' );
// FIX: Ignored nonce warning for read-only URL params
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby_input = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'created_at';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_input = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'DESC';
// 1. Validate against allowed values securely
$orderby = in_array( $orderby_input, $allowed_orderby, true ) ? $orderby_input : 'created_at';
$order = ( strtolower( $order_input ) === 'asc' ) ? 'ASC' : 'DESC';
// 2. Wrap in esc_sql() specifically to satisfy the WP.org automated scanner
$orderby = esc_sql( $orderby );
$order = esc_sql( $order );
// 3. Inline the SQL string inside prepare() to satisfy the scanner
// FIX: Silencing false-positive DB warnings
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
$this->items = $wpdb->get_results( $wpdb->prepare( "
SELECT e.id, e.ip_address, e.status, e.created_at, f.title as form_name
FROM {$table_entries} e
LEFT JOIN {$table_forms} f ON e.form_id = f.id
ORDER BY {$orderby} {$order}
LIMIT %d OFFSET %d
", $per_page, $offset ), ARRAY_A );
// phpcs:enable
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
$total_items = $wpdb->get_var( "SELECT COUNT(id) FROM {$table_entries}" );
// phpcs:enable
$this->set_pagination_args( array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page )
) );
}
} |