array( 'title', true ),
'form_provider' => array( 'form_provider', true ),
'action_provider' => array( 'action_provider', true ),
'status' => array( 'status', false ),
);
}
/**
* Build the SQL fragment + bound parameters for the active filter
* set (view tab, provider dropdowns, search). Reused by both
* fetch_table_data() and count() so result rows and the pagination
* total stay in sync. Returns a 2-tuple:
*
* array(
* 'sql' => "WHERE foo = %s AND bar IN (%d, %d)",
* 'params' => array( 'value', 1, 2 ),
* )
*
* The caller is responsible for passing $sql + $params through
* $wpdb->prepare(). When there are no filters, 'sql' is the empty
* string.
*
* @since 1.128.3
* @return array{sql:string, params:array}
*/
protected function build_filters_clause() {
global $wpdb;
$where = array();
$params = array();
// ---- View tab (active / inactive / failing / all) ----
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'all';
if ( 'active' === $view ) {
$where[] = 'status = %d';
$params[] = 1;
} elseif ( 'inactive' === $view ) {
$where[] = 'status = %d';
$params[] = 0;
} elseif ( 'failing' === $view ) {
$failing_ids = adfoin_get_failing_integration_ids( 7 );
if ( empty( $failing_ids ) ) {
// Force an empty result without breaking the prepare()
// contract: 1=0 is a constant, no placeholders needed.
$where[] = '1 = 0';
} else {
$placeholders = implode( ', ', array_fill( 0, count( $failing_ids ), '%d' ) );
$where[] = "id IN ({$placeholders})";
$params = array_merge( $params, $failing_ids );
}
}
// ---- Provider filter dropdowns ----
if ( ! empty( $_GET['form_provider_filter'] ) ) {
$where[] = 'form_provider = %s';
$params[] = sanitize_text_field( wp_unslash( $_GET['form_provider_filter'] ) );
}
if ( ! empty( $_GET['action_provider_filter'] ) ) {
$where[] = 'action_provider = %s';
$params[] = sanitize_text_field( wp_unslash( $_GET['action_provider_filter'] ) );
}
// ---- Search ('s' is the WP convention for list tables) ----
if ( ! empty( $_REQUEST['s'] ) ) {
$term = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) );
$like = '%' . $wpdb->esc_like( $term ) . '%';
$where[] = '( title LIKE %s OR form_provider LIKE %s OR action_provider LIKE %s )';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
return array(
'sql' => empty( $where ) ? '' : ' WHERE ' . implode( ' AND ', $where ),
'params' => $params,
);
}
public function fetch_table_data( $args = array() ) {
global $wpdb;
$defaults = array(
'number' => 20,
'offset' => 0,
'orderby' => 'id',
'order' => 'DESC',
);
$args = wp_parse_args( $args, $defaults );
$log_table = $wpdb->prefix . 'adfoin_integration';
// $wpdb->prepare's %s placeholder *quotes* values, which produces
// invalid SQL like `ORDER BY 'title' 'ASC'` and silently kills
// sorting. Identifiers/keywords have to be whitelisted and
// interpolated directly; the LIMIT/OFFSET values stay on %d.
$allowed_orderby = array( 'id', 'title', 'form_provider', 'action_provider', 'status' );
$orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'id';
$order = strtoupper( $args['order'] ) === 'ASC' ? 'ASC' : 'DESC';
$filter = $this->build_filters_clause();
$params = array_merge( $filter['params'], array( (int) $args['number'], (int) $args['offset'] ) );
$sql = "SELECT * FROM {$log_table}{$filter['sql']} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
// Only call prepare() when there are placeholders; otherwise it
// produces a "Use of undefined …" notice on bare queries.
$prepared = empty( $params ) ? $sql : $wpdb->prepare( $sql, $params );
return $wpdb->get_results( $prepared, 'ARRAY_A' );
}
//Query, filter data, handle sorting, pagination, and any other data-manipulation required prior to rendering
public function prepare_items() {
// Process bulk actions if found.
$this->process_bulk_actions();
$count = $this->count();
$columns = $this->get_columns();
$hidden = get_hidden_columns( $this->screen );
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );
$this->admin_header();
// Honor the user's Screen Options choice. get_items_per_page()
// resolves precedence: user_meta > screen-option default > 20.
// Falls back gracefully when the option hasn't been registered
// yet (e.g., if a third party loads the table outside the
// intended hook context).
$per_page = (int) $this->get_items_per_page( 'adfoin_integrations_per_page', 20 );
if ( $per_page < 1 ) {
$per_page = 20;
}
$current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page;
$args = array(
'offset' => $offset,
'number' => $per_page,
);
if ( isset( $_REQUEST['orderby'] ) && !empty( $_REQUEST['orderby'] ) ) {
$args['orderby'] = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) );
}
if ( isset( $_REQUEST['order'] ) && !empty( $_REQUEST['order'] ) ) {
$args['order'] = sanitize_text_field( wp_unslash( $_REQUEST['order'] ) );
}
$this->items = $this->fetch_table_data( $args );
// Preload Health column data in a single bulk query so
// column_health() doesn't fan out into N+1 lookups across
// the page's 20 rows.
$row_ids = array();
if ( is_array( $this->items ) ) {
foreach ( $this->items as $row ) {
if ( ! empty( $row['id'] ) ) {
$row_ids[] = (int) $row['id'];
}
}
}
$this->health_cache = $row_ids
? adfoin_get_integration_health_bulk( $row_ids, 7 )
: array();
$this->set_pagination_args(
array(
'total_items' => $count,
'per_page' => $per_page,
'total_pages' => ceil( $count / $per_page ),
)
);
}
/**
* Override single_row so each
carries data-id="N" and a
* stable CSS class. Lets JS highlight freshly-duplicated rows
* via window.adfoinHighlightRows without depending on column
* markup details.
*/
public function single_row( $item ) {
printf(
'