handle_log_deletion(); $this->handle_csv_export(); $this->handle_saved_view(); $this->handle_watchlist_action(); $this->sync_report_schedule(); } /** * Delete single or bulk execution logs. */ private function handle_log_deletion(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Action is gated by the request-specific nonce check below. $page = isset( $_REQUEST['page'] ) ? sanitize_key( wp_unslash( $_REQUEST['page'] ) ) : ''; if ( 'ability-guard-logs' !== $page ) { return; } $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; if ( '' === $action && isset( $_REQUEST['action2'] ) ) { $action = sanitize_key( wp_unslash( $_REQUEST['action2'] ) ); } if ( 'delete' === $action ) { $log_id = isset( $_GET['log_id'] ) ? absint( wp_unslash( $_GET['log_id'] ) ) : 0; if ( $log_id > 0 ) { check_admin_referer( 'ability_guard_delete_log_' . $log_id ); $repository = new Ability_Guard_Log_Repository(); $repository->delete_logs( array( $log_id ) ); wp_safe_redirect( admin_url( 'admin.php?page=ability-guard-logs' ) ); exit; } } if ( 'bulk-delete' === $action ) { check_admin_referer( 'bulk-ability-guard-logs' ); $log_ids = isset( $_REQUEST['log_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_REQUEST['log_ids'] ) ) : array(); if ( ! empty( $log_ids ) ) { $repository = new Ability_Guard_Log_Repository(); $repository->delete_logs( $log_ids ); } wp_safe_redirect( admin_url( 'admin.php?page=ability-guard-logs' ) ); exit; } } /** * Export filtered logs as CSV. */ private function handle_csv_export(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is checked before exporting. if ( empty( $_GET['ability_guard_export'] ) ) { return; } check_admin_referer( 'ability_guard_export_logs' ); $repository = new Ability_Guard_Log_Repository(); $args = $this->get_log_filter_args(); $args = $this->add_inventory_filters( $args ); $args['per_page'] = 5000; $args['page'] = 1; $logs = $repository->get_logs( $args ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=abilityguard-logs-' . gmdate( 'Y-m-d-His' ) . '.csv' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Streaming a generated CSV download. $output = fopen( 'php://output', 'w' ); if ( false === $output ) { exit; } fputcsv( $output, array( 'ID', 'Ability', 'User ID', 'Via', 'Status', 'Error Code', 'Duration MS', 'IP Address', 'Created At' ) ); foreach ( $logs as $log ) { fputcsv( $output, array( absint( $log->id ), (string) $log->ability_name, absint( $log->user_id ), (string) $log->triggered_via, (string) $log->status, (string) $log->error_code, absint( $log->execution_ms ), (string) $log->ip_address, (string) $log->created_at, ) ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing streamed CSV output. fclose( $output ); exit; } /** * Save the current log filter as a named view. */ private function handle_saved_view(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is checked before saving. if ( empty( $_POST['ability_guard_save_view'] ) ) { return; } check_admin_referer( 'ability_guard_save_view' ); $name = isset( $_POST['ability_guard_view_name'] ) ? sanitize_text_field( wp_unslash( $_POST['ability_guard_view_name'] ) ) : ''; if ( '' === $name ) { return; } $views = get_option( 'ability_guard_saved_views', array() ); $views = is_array( $views ) ? $views : array(); $views[] = array( 'id' => wp_generate_uuid4(), 'name' => $name, 'filters' => $this->get_log_filter_args(), ); update_option( 'ability_guard_saved_views', array_slice( $views, -20 ) ); wp_safe_redirect( admin_url( 'admin.php?page=ability-guard-logs&ability_guard_view_saved=1' ) ); exit; } /** * Handle watch/unwatch links from inventory. */ private function handle_watchlist_action(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is checked before mutating. if ( empty( $_GET['ability_guard_watch_action'] ) || ! isset( $_GET['ability_name'] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately after URL decoding. $ability_name = rawurldecode( wp_unslash( $_GET['ability_name'] ) ); $ability_name = sanitize_text_field( $ability_name ); check_admin_referer( 'ability_guard_watch_' . $ability_name ); $watchlist = get_option( 'ability_guard_watchlist', array() ); $watchlist = is_array( $watchlist ) ? $watchlist : array(); $action = sanitize_key( wp_unslash( $_GET['ability_guard_watch_action'] ) ); if ( 'watch' === $action ) { $watchlist[] = $ability_name; } elseif ( 'unwatch' === $action ) { $watchlist = array_diff( $watchlist, array( $ability_name ) ); } update_option( 'ability_guard_watchlist', array_values( array_unique( $watchlist ) ) ); wp_safe_redirect( admin_url( 'admin.php?page=ability-guard&ability_name=' . rawurlencode( $ability_name ) ) ); exit; } /** * Keep the scheduled report event aligned with settings. */ private function sync_report_schedule(): void { $enabled = (bool) get_option( 'ability_guard_reports_enabled', false ); $frequency = (string) get_option( 'ability_guard_report_frequency', 'weekly' ); $scheduled = wp_next_scheduled( 'ability_guard_send_scheduled_report' ); $current = (string) get_option( 'ability_guard_report_scheduled_frequency', '' ); if ( ! $enabled ) { if ( $scheduled ) { wp_clear_scheduled_hook( 'ability_guard_send_scheduled_report' ); } delete_option( 'ability_guard_report_scheduled_frequency' ); return; } if ( $scheduled && $current !== $frequency ) { wp_clear_scheduled_hook( 'ability_guard_send_scheduled_report' ); $scheduled = false; } if ( ! $scheduled ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily' === $frequency ? 'daily' : 'weekly', 'ability_guard_send_scheduled_report' ); update_option( 'ability_guard_report_scheduled_frequency', $frequency ); } } /** * Read log filters from request. * * @return array */ private function get_log_filter_args(): array { return array( // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'orderby' => isset( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'order' => isset( $_REQUEST['order'] ) ? sanitize_key( wp_unslash( $_REQUEST['order'] ) ) : 'DESC', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'status' => isset( $_REQUEST['status'] ) ? sanitize_key( wp_unslash( $_REQUEST['status'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'triggered_via' => isset( $_REQUEST['triggered_via'] ) ? sanitize_key( wp_unslash( $_REQUEST['triggered_via'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'user_id' => isset( $_REQUEST['user_id'] ) && '' !== trim( (string) wp_unslash( $_REQUEST['user_id'] ) ) ? absint( wp_unslash( $_REQUEST['user_id'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'date_from' => isset( $_REQUEST['date_from'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['date_from'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'date_to' => isset( $_REQUEST['date_to'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['date_to'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'risk' => isset( $_REQUEST['risk'] ) ? sanitize_key( wp_unslash( $_REQUEST['risk'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter reused after caller-specific nonce checks. 'search' => isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '', ); } /** * Convert inventory filters into ability name filters. * * @param array $args Query args. * @return array */ private function add_inventory_filters( array $args ): array { if ( empty( $args['risk'] ) ) { return $args; } $scanner = new Ability_Guard_Ability_Scanner(); $ability_names = array(); foreach ( $scanner->get_abilities() as $ability ) { if ( ! empty( $args['risk'] ) && $ability['risk'] !== $args['risk'] ) { continue; } $ability_names[] = $ability['name']; } $args['ability_names'] = $ability_names; return $args; } }