*/ private $start_times = array(); /** * Log repository. * * @var Ability_Guard_Log_Repository */ private $repository; /** * Constructor. * * @param Ability_Guard_Log_Repository $repository Log repository. */ public function __construct( Ability_Guard_Log_Repository $repository ) { $this->repository = $repository; } /** * Capture ability start time. * * @param string $ability_name Ability name. * @param mixed $input Ability input. */ public function on_before_execute( string $ability_name, $input ): void { if ( ! $this->should_log( $ability_name, $input ) ) { return; } $this->start_times[ $ability_name ] = microtime( true ); } /** * Write the ability execution log. * * @param string $ability_name Ability name. * @param mixed $input Ability input. * @param mixed|WP_Error $result Ability result. */ public function on_after_execute( string $ability_name, $input, $result ): void { if ( ! $this->should_log( $ability_name, $input ) ) { unset( $this->start_times[ $ability_name ] ); return; } $execution_ms = 0; if ( isset( $this->start_times[ $ability_name ] ) ) { $execution_ms = (int) round( ( microtime( true ) - $this->start_times[ $ability_name ] ) * 1000 ); unset( $this->start_times[ $ability_name ] ); } $is_error = is_wp_error( $result ); $log_input_data = (bool) get_option( 'ability_guard_log_input_data', true ); $log_output_data = (bool) get_option( 'ability_guard_log_output_data', false ); $this->repository->insert( array( 'ability_name' => sanitize_text_field( $ability_name ), 'user_id' => absint( get_current_user_id() ), 'triggered_via' => $this->get_trigger_context(), 'input_data' => $log_input_data ? $this->encode_json( $input ) : null, 'output_data' => ( $log_output_data && ! $is_error ) ? $this->encode_json( $result ) : null, 'status' => $is_error ? 'error' : 'success', 'error_code' => $is_error ? sanitize_key( $result->get_error_code() ) : null, 'execution_ms' => absint( $execution_ms ), 'ip_address' => $this->get_client_ip(), 'created_at' => current_time( 'mysql', true ), ) ); $retention_limit = absint( get_option( 'ability_guard_log_retention_limit', ABILITY_GUARD_LOG_LIMIT ) ); $this->repository->enforce_limit( $retention_limit ); } /** * Determine whether an ability should be logged. * * @param string $ability_name Ability name. * @param mixed $input Ability input. * @return bool */ private function should_log( string $ability_name, $input ): bool { return (bool) apply_filters( 'ability_guard_should_log_ability', true, $ability_name, $input ); } /** * Encode data for storage. * * @param mixed $data Data to encode. * @return string|null */ private function encode_json( $data ): ?string { $encoded = wp_json_encode( $data ); return false === $encoded ? null : $encoded; } /** * Determine execution context. * * @return string */ private function get_trigger_context(): string { $context = 'php'; if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { $context = 'rest'; } elseif ( defined( 'WP_CLI' ) && WP_CLI ) { $context = 'cli'; } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) { $context = 'cron'; } return sanitize_key( apply_filters( 'ability_guard_trigger_context', $context ) ); } /** * Get sanitized client IP address. * * @return string|null */ private function get_client_ip(): ?string { if ( empty( $_SERVER['REMOTE_ADDR'] ) ) { return null; } $ip_address = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); $ip_address = apply_filters( 'ability_guard_client_ip', $ip_address ); return is_string( $ip_address ) ? sanitize_text_field( $ip_address ) : null; } }