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 ) ) ); } }