$context Additional context data. * @return void */ private function log( string $level, string $message, array $context = array() ): void { if ( ! $this->is_enabled() ) { return; } $user_id = get_current_user_id(); $user_info = $user_id ? get_userdata( $user_id ) : null; $username = $user_info ? $user_info->user_login : 'anonymous'; $log_entry = sprintf( '[iDrivee2] [%s] [User: %s] %s', $level, $username, $message ); // Add context if provided. if ( ! empty( $context ) ) { $log_entry .= ' | Context: ' . wp_json_encode( $context ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $log_entry ); } /** * Log an info message. * * @since 0.3.1 * * @param string $message Log message. * @param array $context Additional context data. * @return void */ public function info( string $message, array $context = array() ): void { $this->log( self::LEVEL_INFO, $message, $context ); } /** * Log a warning message. * * @since 0.3.1 * * @param string $message Log message. * @param array $context Additional context data. * @return void */ public function warning( string $message, array $context = array() ): void { $this->log( self::LEVEL_WARNING, $message, $context ); } /** * Log an error message. * * @since 0.3.1 * * @param string $message Log message. * @param array $context Additional context data. * @return void */ public function error( string $message, array $context = array() ): void { $this->log( self::LEVEL_ERROR, $message, $context ); } /** * Log a security event. * * @since 0.3.1 * * @param string $message Log message. * @param array $context Additional context data. * @return void */ public function security( string $message, array $context = array() ): void { $this->log( self::LEVEL_SECURITY, $message, $context ); } /** * Log a configuration change. * * @since 0.3.1 * * @param string $field Configuration field that changed. * @param string $old_value Old value (masked if sensitive). * @param string $new_value New value (masked if sensitive). * @return void */ public function config_change( string $field, string $old_value, string $new_value ): void { // Mask sensitive fields. $sensitive_fields = array( 'secret', 'key' ); if ( in_array( $field, $sensitive_fields, true ) ) { $old_value = $this->mask_value( $old_value ); $new_value = $this->mask_value( $new_value ); } $this->security( sprintf( 'Configuration changed: %s', $field ), array( 'field' => $field, 'old_value' => $old_value, 'new_value' => $new_value, ) ); } /** * Log an S3 operation. * * @since 0.3.1 * * @param string $operation Operation type (upload, delete, headBucket, etc.). * @param bool $success Whether the operation succeeded. * @param string $file_name Optional file name. * @param string $error Optional error message. * @return void */ public function s3_operation( string $operation, bool $success, string $file_name = '', string $error = '' ): void { $level = $success ? self::LEVEL_INFO : self::LEVEL_ERROR; $message = sprintf( 'S3 %s %s', $operation, $success ? 'succeeded' : 'failed' ); $context = array( 'operation' => $operation, 'success' => $success, ); if ( $file_name ) { $context['file'] = $file_name; } if ( $error ) { $context['error'] = $error; } $this->log( $level, $message, $context ); // Track operation count. $this->track_s3_operation( $operation ); } /** * Log and track the result of a batch S3 upload. * * Performs a single DB write for all uploads in the batch instead of one * write per file, reducing database load on bulk media imports. * * @since 1.2.0 * * @param string $operation S3 operation name (e.g. 'putObject'). * @param int $success_count Number of files successfully uploaded. * @param int $fail_count Number of files that failed. * @return void */ public function s3_batch( string $operation, int $success_count, int $fail_count ): void { if ( $success_count > 0 ) { $this->track_bulk_s3_operations( $operation, $success_count ); } if ( $fail_count > 0 ) { $this->log( self::LEVEL_ERROR, sprintf( 'S3 %s batch: %d failure(s)', $operation, $fail_count ) ); } } /** * Increment S3 operation counters by an arbitrary count in a single DB write. * * @since 1.2.0 * * @param string $operation S3 operation name. * @param int $count Number of operations to add. * @return void */ private function track_bulk_s3_operations( string $operation, int $count ): void { $option_key = 'idrivee2_s3_operations'; $raw = get_option( $option_key, array() ); /** * Daily S3 operation counts, keyed by date and operation name. * * @var array> $stats */ $stats = is_array( $raw ) ? $raw : array(); $today = gmdate( 'Y-m-d' ); if ( ! isset( $stats[ $today ] ) ) { $stats[ $today ] = array(); } if ( ! isset( $stats[ $today ][ $operation ] ) ) { $stats[ $today ][ $operation ] = 0; } $stats[ $today ][ $operation ] += $count; $cutoff_date = gmdate( 'Y-m-d', time() - ( 30 * DAY_IN_SECONDS ) ); foreach ( array_keys( $stats ) as $date ) { if ( $date < $cutoff_date ) { unset( $stats[ $date ] ); } } update_option( $option_key, $stats ); } /** * Log an authentication failure. * * @since 0.3.1 * * @param string $reason Reason for authentication failure. * @return void */ public function auth_failure( string $reason ): void { $this->security( 'Authentication failure: ' . $reason, array( 'ip' => $this->get_client_ip(), 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) && is_string( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', ) ); } /** * Log a rate limit violation. * * @since 0.3.1 * * @param string $action Action that was rate limited. * @return void */ public function rate_limit_exceeded( string $action ): void { $this->security( 'Rate limit exceeded: ' . $action, array( 'action' => $action, 'ip' => $this->get_client_ip(), ) ); } /** * Get client IP address. * * Validates the address with filter_var() so malformed or spoofed * REMOTE_ADDR values are rejected rather than sanitised into garbage. * * @since 0.3.1 * * @return string Client IP address, or empty string if invalid. */ private function get_client_ip(): string { if ( ! isset( $_SERVER['REMOTE_ADDR'] ) || ! is_string( $_SERVER['REMOTE_ADDR'] ) ) { return ''; } $valid_ip = filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ); return is_string( $valid_ip ) ? $valid_ip : ''; } /** * Mask a sensitive value. * * @since 0.3.1 * * @param string $value Value to mask. * @return string Masked value. */ private function mask_value( string $value ): string { $length = strlen( $value ); if ( $length <= 4 ) { return str_repeat( '*', $length ); } // Show first 2 and last 2 characters. return substr( $value, 0, 2 ) . str_repeat( '*', $length - 4 ) . substr( $value, -2 ); } /** * Track S3 operation count. * * @since 0.3.1 * * @param string $operation Operation type. * @return void */ private function track_s3_operation( string $operation ): void { $option_key = 'idrivee2_s3_operations'; $raw = get_option( $option_key, array() ); /** * Daily S3 operation counts, keyed by date and operation name. * * @var array> $stats */ $stats = is_array( $raw ) ? $raw : array(); // Initialize stats for today if not exists. $today = gmdate( 'Y-m-d' ); if ( ! isset( $stats[ $today ] ) ) { $stats[ $today ] = array(); } if ( ! isset( $stats[ $today ][ $operation ] ) ) { $stats[ $today ][ $operation ] = 0; } ++$stats[ $today ][ $operation ]; // Keep only last 30 days. Use time() arithmetic to stay in UTC (avoids strtotime local-tz). $cutoff_date = gmdate( 'Y-m-d', time() - ( 30 * DAY_IN_SECONDS ) ); foreach ( array_keys( $stats ) as $date ) { if ( $date < $cutoff_date ) { unset( $stats[ $date ] ); } } update_option( $option_key, $stats ); } /** * Get S3 operation statistics. * * @since 0.3.1 * * @param int $days Number of days to retrieve (default 7, max 30). * @return array> Statistics array indexed by date and operation. */ public function get_s3_stats( int $days = 7 ): array { $days = min( $days, 30 ); $raw = get_option( 'idrivee2_s3_operations', array() ); /** * Daily S3 operation counts, keyed by date and operation name. * * @var array> $stats */ $stats = is_array( $raw ) ? $raw : array(); $result = array(); for ( $i = 0; $i < $days; $i++ ) { $date = gmdate( 'Y-m-d', time() - ( $i * DAY_IN_SECONDS ) ); if ( isset( $stats[ $date ] ) ) { $result[ $date ] = $stats[ $date ]; } } return $result; } }