admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'abilities_bridge_activity_log' ),
)
);
}
/**
* Verify the page navigation nonce.
*
* Returns true if nonce is valid or not present (first page load).
* Dies with error if nonce is present but invalid.
*
* @return bool True if request is valid.
*/
private static function verify_page_nonce() {
$nonce = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
// No nonce present = first page load, allow with defaults.
if ( empty( $nonce ) ) {
return false;
}
// Nonce present - verify it.
if ( ! wp_verify_nonce( $nonce, 'abilities_bridge_activity_log_nav' ) ) {
wp_die(
esc_html__( 'Security verification failed. Please use the navigation links provided.', 'abilities-bridge' ),
esc_html__( 'Security Check Failed', 'abilities-bridge' ),
array( 'response' => 403 )
);
}
return true;
}
/**
* Get a sanitized GET parameter for display purposes.
*
* Verifies nonce before accessing parameters. If no nonce is present
* (first page load), returns the default value.
*
* @param string $key Parameter key.
* @param string $default Default value.
* @return string Sanitized value.
*/
private static function get_display_param( $key, $default = '' ) {
// Verify nonce - returns false if no nonce (first load).
if ( ! self::verify_page_nonce() ) {
return $default;
}
$value = filter_input( INPUT_GET, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
return ! empty( $value ) ? sanitize_key( $value ) : $default;
}
/**
* Get a sanitized integer GET parameter for display purposes.
*
* Verifies nonce before accessing parameters. If no nonce is present
* (first page load), returns the default value.
*
* @param string $key Parameter key.
* @param int $default Default value.
* @return int Sanitized integer value.
*/
private static function get_display_int_param( $key, $default = 0 ) {
// Verify nonce - returns false if no nonce (first load).
if ( ! self::verify_page_nonce() ) {
return $default;
}
$value = filter_input( INPUT_GET, $key, FILTER_VALIDATE_INT );
return false !== $value && null !== $value ? max( 1, absint( $value ) ) : $default;
}
/**
* Render the page
*/
public static function render_page() {
$current_tab = self::get_display_param( 'tab', 'activity' );
?>
403 )
);
}
// Nonce is valid - build filters safely using filter_input.
// User ID filter.
$user_id = filter_input( INPUT_GET, 'user_id', FILTER_VALIDATE_INT );
if ( false !== $user_id && null !== $user_id && $user_id > 0 ) {
$filters['user_id'] = $user_id;
}
// Status filter.
$status = filter_input( INPUT_GET, 'status', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( ! empty( $status ) ) {
$filters['status'] = sanitize_key( $status );
}
// Search term filter.
$search = filter_input( INPUT_GET, 'search', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( ! empty( $search ) ) {
$filters['search'] = sanitize_text_field( $search );
}
return $filters;
}
/**
* Render Activity Log tab
*/
private static function render_activity_tab() {
// Get validated filters (handles all nonce checking internally).
$filters = self::get_validated_filters();
// Get filter options.
$users = get_users( array( 'fields' => array( 'ID', 'user_login' ) ) );
?>
1 ) : ?>
add_query_arg( 'paged', '%#%', $base_url ),
'format' => '',
'current' => $page,
'total' => $result['total_pages'],
'prev_text' => '«',
'next_text' => '»',
)
)
);
?>
|
|
|
|
MCP
Chat
|
|
|
✗
✓
|
|
1 ) :
?>
= 0 && $new_retention <= 90 ) {
update_option( 'abilities_bridge_log_retention_days', $new_retention );
$retention_days = $new_retention;
echo '' . esc_html__( 'Log retention settings saved.', 'abilities-bridge' ) . '
';
}
}
// Handle manual cleanup.
if ( isset( $_POST['abilities_bridge_manual_cleanup'] ) && check_admin_referer( 'abilities_bridge_cleanup_nonce' ) ) {
$results = Abilities_Bridge_Log_Cleanup::manual_cleanup();
echo '';
printf(
/* translators: 1: number of logs deleted, 2: number of conversations purged */
esc_html__( 'Cleanup complete: %1$d logs deleted, %2$d conversations purged.', 'abilities-bridge' ),
absint( $results['logs_deleted'] ),
absint( $results['conversations_deleted'] )
);
echo '
';
// Refresh stats.
$stats = Abilities_Bridge_Database::get_log_statistics();
$last_cleanup = Abilities_Bridge_Log_Cleanup::get_last_cleanup_info();
}
?>
'Unauthorized' ) );
}
// Extract and validate filters from POST (AJAX request).
$filters = array();
$page = 1;
// Retrieve filters from POST data (nonce verified above via check_ajax_referer).
$raw_filters = filter_input( INPUT_POST, 'filters', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( ! empty( $raw_filters ) && is_array( $raw_filters ) ) {
$posted_filters = map_deep( $raw_filters, 'sanitize_text_field' );
if ( ! empty( $posted_filters['user_id'] ) ) {
$filters['user_id'] = intval( $posted_filters['user_id'] );
}
if ( ! empty( $posted_filters['status'] ) ) {
$filters['status'] = sanitize_key( $posted_filters['status'] );
}
if ( ! empty( $posted_filters['search'] ) ) {
$filters['search'] = sanitize_text_field( $posted_filters['search'] );
}
if ( ! empty( $posted_filters['page'] ) ) {
$page = intval( $posted_filters['page'] );
}
}
$page = max( 1, $page );
$per_page = 50;
// Get logs with filters.
$result = Abilities_Bridge_Database::get_logs_filtered( $filters, $page, $per_page );
// Render table rows only (not entire table).
ob_start();
self::render_table_rows( $result );
$html = ob_get_clean();
// Render pagination.
ob_start();
self::render_pagination( $result, $page );
$pagination = ob_get_clean();
wp_send_json_success(
array(
'html' => $html,
'pagination' => $pagination,
)
);
}
/**
* AJAX: Export logs to CSV
*/
public static function ajax_export_logs() {
check_ajax_referer( 'abilities_bridge_activity_log', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Unauthorized' );
}
// Rate limiting - prevent export spam/abuse.
// Max 5 exports per minute per user.
$user_id = get_current_user_id();
$transient_key = 'abilities_bridge_export_attempts_' . $user_id;
$attempts = get_transient( $transient_key );
if ( false === $attempts ) {
$attempts = 0;
}
if ( $attempts >= 5 ) {
wp_die(
esc_html__( 'Too many export attempts. Please wait a moment and try again.', 'abilities-bridge' ),
esc_html__( 'Rate Limit Exceeded', 'abilities-bridge' ),
array( 'response' => 429 )
);
}
// Increment export attempts counter (expires in 60 seconds).
set_transient( $transient_key, $attempts + 1, MINUTE_IN_SECONDS );
// Build filters from POST data (nonce verified above).
$filters = array();
if ( ! empty( $_POST['user_id'] ) ) {
$filters['user_id'] = intval( wp_unslash( $_POST['user_id'] ) );
}
if ( ! empty( $_POST['status'] ) ) {
$filters['status'] = sanitize_key( wp_unslash( $_POST['status'] ) );
}
if ( ! empty( $_POST['search'] ) ) {
$filters['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
}
// Get logs (max 10000).
$result = Abilities_Bridge_Database::get_logs_filtered( $filters, 1, 10000 );
// Set headers for CSV download.
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=activity-log-' . gmdate( 'Y-m-d-His' ) . '.csv' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// Output CSV using PHP output stream (standard WordPress CSV export pattern).
$output = fopen( 'php://output', 'w' );
// Add UTF-8 BOM for Excel compatibility.
fwrite( $output, "\xEF\xBB\xBF" );
// CSV headers.
fputcsv( $output, array( 'Timestamp', 'User', 'Conversation', 'Function', 'Status', 'Action', 'Error' ) );
// CSV rows.
foreach ( $result['items'] as $log ) {
fputcsv(
$output,
array(
$log['created_at'],
$log['username'],
$log['conversation_title'] ? $log['conversation_title'] : 'N/A',
$log['function_name'],
$log['error_message'] ? 'Error' : 'Success',
$log['action'],
$log['error_message'] ? $log['error_message'] : '',
)
);
}
fclose( $output );
exit;
}
/**
* AJAX: Restore conversation
*/
public static function ajax_restore_conversation() {
check_ajax_referer( 'abilities_bridge_activity_log', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
}
$conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0;
if ( ! $conversation_id ) {
wp_send_json_error( array( 'message' => 'Invalid conversation ID' ) );
}
if ( ! self::get_accessible_conversation( $conversation_id, true ) ) {
wp_send_json_error( array( 'message' => 'Conversation not found' ) );
}
$result = Abilities_Bridge_Database::restore_conversation( $conversation_id, get_current_user_id() );
if ( $result ) {
wp_send_json_success( array( 'message' => 'Conversation restored successfully' ) );
} else {
wp_send_json_error( array( 'message' => 'Failed to restore conversation' ) );
}
}
/**
* AJAX: Permanently delete conversation
*/
public static function ajax_permanently_delete() {
check_ajax_referer( 'abilities_bridge_activity_log', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
}
$conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0;
if ( ! $conversation_id ) {
wp_send_json_error( array( 'message' => 'Invalid conversation ID' ) );
}
if ( ! self::get_accessible_conversation( $conversation_id, true ) ) {
wp_send_json_error( array( 'message' => 'Conversation not found' ) );
}
$result = Abilities_Bridge_Database::permanently_delete_conversation( $conversation_id, get_current_user_id() );
if ( $result ) {
wp_send_json_success( array( 'message' => 'Conversation permanently deleted' ) );
} else {
wp_send_json_error( array( 'message' => 'Failed to delete conversation' ) );
}
}
}