- Step 1: Restrict Access to High-Privilege Users
- Step 2: Verify the Log Table Exists
- Step 3: Read and Sanitize Filter Inputs
- Step 4: Build the WHERE Clause Incrementally
- Step 5: Support Status Code Groups (4xx, 5xx)
- Step 6: Count Total Rows for Pagination
- Step 7: Query Paginated Results
- Step 8: Render the Logs Table
- Step 9: Add CSV Export for Offline Analysis
- Final Result
At the end of this part, Init Sentinel becomes a complete security observability system inside WordPress.
Step 1: Restrict Access to High-Privilege Users
The Security Logs page must never be accessible to regular users. The first line of defense is a strict capability check.
if ( ! current_user_can('manage_options') ) {
wp_die( esc_html__('You do not have permission to view this page.', 'init-html') );
}
This guarantees that only administrators can inspect security events.
Step 2: Verify the Log Table Exists
Before running any queries, the page verifies that the security log table exists. This avoids fatal errors during first-time activation or partial deployments.
$has_table = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = %s",
$table
) );
if ( ! $has_table ) {
echo '<div class="wrap"><h1>Security Logs</h1><p>Security log table not found.</p></div>';
return;
}
This defensive check keeps wp-admin stable even when Sentinel is not fully initialized.
Step 3: Read and Sanitize Filter Inputs
The page accepts multiple filters to support real investigation workflows.
$paged = max(1, absint($_GET['paged'] ?? 1));
$per_page = min( max(20, absint($_GET['per_page'] ?? 50)), 200 );
$q = trim((string)($_GET['s'] ?? ''));
$code = trim((string)($_GET['code'] ?? ''));
$endpoint = trim((string)($_GET['endpoint'] ?? ''));
$action = trim((string)($_GET['action_key'] ?? ''));
$user_id = trim((string)($_GET['user'] ?? ''));
$ip = trim((string)($_GET['ip'] ?? ''));
$date_from = trim((string)($_GET['from'] ?? ''));
$date_to = trim((string)($_GET['to'] ?? ''));
Each parameter maps directly to a column or behavior in the log table.
Step 4: Build the WHERE Clause Incrementally
Instead of hardcoding queries, Init Sentinel builds the WHERE clause dynamically based on provided filters.
$where = [];
$params = [];
if ($endpoint !== '') {
$where[] = 'endpoint LIKE %s';
$params[] = $endpoint . '%';
}
if ($action !== '') {
$where[] = 'action LIKE %s';
$params[] = '%' . $wpdb->esc_like($action) . '%';
}
if ($ip !== '') {
$where[] = 'ip_address LIKE %s';
$params[] = '%' . $wpdb->esc_like($ip) . '%';
}
This pattern keeps the query readable, safe, and extensible.
Step 5: Support Status Code Groups (4xx, 5xx)
Status codes can be filtered either by exact value or by group.
if (preg_match('/^\d{3}$/', $code)) {
$where[] = 'status_code = %d';
$params[] = (int)$code;
} elseif (preg_match('/^[245]xx$/', $code)) {
$prefix = (int)$code[0];
$where[] = 'status_code BETWEEN %d AND %d';
$params[] = $prefix * 100;
$params[] = $prefix * 100 + 99;
}
This makes it easy to focus on authorization failures or server errors.
Step 6: Count Total Rows for Pagination
Pagination requires knowing the total number of matching records.
$total = (int) $wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) FROM {$table} {$where_sql}", ...$params)
);
This query is separate from the main data query to keep logic clear.
Step 7: Query Paginated Results
The actual log data is retrieved using LIMIT and OFFSET.
$sql = "SELECT id,created_at,user_id,ip_address,endpoint,action,status_code,user_agent
FROM {$table}
{$where_sql}
ORDER BY {$orderby} {$order}
LIMIT %d OFFSET %d";
$rows = $wpdb->get_results(
$wpdb->prepare($sql, ...array_merge($params, [$per_page, $offset]))
);
Sorting is restricted to a whitelist of columns to avoid unsafe SQL.
Step 8: Render the Logs Table
Each row shows the essential attributes of a security event.
$time_local = get_date_from_gmt(
gmdate('Y-m-d H:i:s', strtotime($r->created_at)),
get_option('date_format').' '.get_option('time_format')
);
UTC is stored in the database, local time is only applied at display time.
Step 9: Add CSV Export for Offline Analysis
The page supports CSV export with a hard row limit and nonce protection.
if ( isset($_GET['export']) && $_GET['export'] === 'csv' ) {
check_admin_referer('init_html_sentinel_export');
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=security-logs.csv');
}
This allows incident analysis without exposing unlimited data.
Final Result
With this page in place, Init Sentinel now provides a complete workflow: log silently, observe quickly via widget, and investigate thoroughly via a dedicated admin interface.
This concludes the Init Sentinel build series.
Ideally, this page should always be empty. If it isn’t, someone is messing around.
Comments