esc_html__( 'Form', '24tt-unrestricted-forms' ),
'plural' => esc_html__( 'Forms', '24tt-unrestricted-forms' ),
'ajax' => false
) );
}
/**
* Define the columns that are going to be used in the table.
*
* @since 1.0.0
* @return array Array of columns.
*/
public function get_columns() {
$columns = array(
'cb' => '', // Bulk actions checkbox
'title' => esc_html__( 'Form Title', '24tt-unrestricted-forms' ),
'shortcode' => esc_html__( 'Shortcode', '24tt-unrestricted-forms' ),
'status' => esc_html__( 'Status', '24tt-unrestricted-forms' ),
'created_at' => esc_html__( 'Date Created', '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(
'title' => array( 'title', false ),
'created_at' => array( 'created_at', true ) // Default sort
);
return $sortable_columns;
}
/**
* Fallback method for rendering column data.
*
* @since 1.0.0
* @param array $item A singular item (one row's data).
* @param string $column_name The name/slug of the column.
* @return string HTML output.
*/
protected function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'status':
return ucfirst( esc_html( $item[ $column_name ] ) );
case 'created_at':
// Format the date nicely
return esc_html( date_i18n( get_option( 'date_format' ), strtotime( $item[ $column_name ] ) ) );
default:
return esc_html( $item[ $column_name ] );
}
}
/**
* Render the checkbox column.
*
* @since 1.0.0
* @param array $item A singular item.
* @return string Checkbox HTML.
*/
protected function column_cb( $item ) {
return sprintf(
'',
absint( $item['id'] )
);
}
/**
* Render the title column and attach the row actions (Edit, Entries, Delete).
*
* @since 1.0.0
* @param array $item A singular item.
* @return string Title and row actions HTML.
*/
protected function column_title( $item ) {
$edit_url = add_query_arg( array( 'page' => 'tt24-forms-builder', 'id' => absint( $item['id'] ) ), admin_url( 'admin.php' ) );
$entries_url = add_query_arg( array( 'page' => 'tt24-forms-entries' ), admin_url( 'admin.php' ) ); // We can add filtering by form ID later
// SECURITY: Create a secure deletion URL
$delete_url = wp_nonce_url(
add_query_arg( array( 'page' => 'tt24-forms', 'action' => 'delete', 'form_id' => absint( $item['id'] ) ), admin_url( 'admin.php' ) ),
'tt24_delete_form_' . $item['id']
);
$actions = array(
'edit' => sprintf( '%s', esc_url( $edit_url ), esc_html__( 'Edit', '24tt-unrestricted-forms' ) ),
'entries' => sprintf( '%s', esc_url( $entries_url ), esc_html__( 'View Entries', '24tt-unrestricted-forms' ) ),
'delete' => sprintf( '%s', esc_url( $delete_url ), esc_js( __( 'Are you sure you want to permanently delete this form and all its entries?', '24tt-unrestricted-forms' ) ), esc_html__( 'Delete', '24tt-unrestricted-forms' ) )
);
return sprintf( '%2$s%3$s',
esc_url( $edit_url ),
esc_html( $item['title'] ),
$this->row_actions( $actions )
);
}
/**
* Render the shortcode column so the user can easily copy it.
*
* @since 1.0.0
* @param array $item A singular item.
* @return string Shortcode HTML.
*/
protected function column_shortcode( $item ) {
$shortcode = sprintf( '[tt24_form id="%d"]', absint( $item['id'] ) );
return sprintf( '', esc_attr( $shortcode ) );
}
/**
* Define the bulk actions available.
*
* @since 1.0.0
* @return array
*/
protected function get_bulk_actions() {
return array(
'bulk-delete' => esc_html__( 'Delete Permanently', '24tt-unrestricted-forms' )
);
}
/**
* Process bulk and single actions (like deleting a form).
*
* @since 1.0.0
* @return void
*/
public function process_bulk_action() {
global $wpdb;
// Ensure user has permissions
if ( ! current_user_can( 'manage_tt24_forms' ) ) {
return;
}
$table_forms = $wpdb->prefix . 'tt24_forms';
$table_entries = $wpdb->prefix . 'tt24_entries';
$table_entry_fields = $wpdb->prefix . 'tt24_entry_fields';
// 1. Handle Single Deletion
// FIX: Ignored nonce warning for read-only 'form_id' URL param
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'delete' === $this->current_action() && isset( $_GET['form_id'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$form_id = absint( $_GET['form_id'] );
check_admin_referer( 'tt24_delete_form_' . $form_id );
// Cascade delete: Fields -> Entries -> Form
$wpdb->delete( $table_entry_fields, array( 'form_id' => $form_id ), array( '%d' ) );
$wpdb->delete( $table_entries, array( 'form_id' => $form_id ), array( '%d' ) );
$wpdb->delete( $table_forms, array( 'id' => $form_id ), array( '%d' ) );
$redirect_url = add_query_arg( array( 'page' => 'tt24-forms', 'deleted' => 1 ), admin_url( 'admin.php' ) );
wp_safe_redirect( $redirect_url );
exit;
}
// 2. Handle Bulk Deletion
if ( 'bulk-delete' === $this->current_action() ) {
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 $form_id ) {
// Cascade delete
$wpdb->delete( $table_entry_fields, array( 'form_id' => $form_id ), array( '%d' ) );
$wpdb->delete( $table_entries, array( 'form_id' => $form_id ), array( '%d' ) );
$wpdb->delete( $table_forms, array( 'id' => $form_id ), array( '%d' ) );
}
$redirect_url = add_query_arg( array( 'page' => 'tt24-forms', '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_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 any deletions before we fetch the remaining list
$this->process_bulk_action();
$allowed_orderby = array( 'title', '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 * FROM {$table_forms}
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_forms}" );
// phpcs:enable
$this->set_pagination_args( array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page )
) );
}
}