, covered_from: int, has_more: bool} * * @since 11.3.0 */ public function read( string $source, int $from, int $to, int $limit = PHP_INT_MAX, ?callable $expand = null ): array { global $wpdb; $table = $wpdb->prefix . 'woocommerce_log'; $max_id = (int) $wpdb->get_var( "SELECT MAX(log_id) FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = array(); $upper = $max_id; $examined = 0; $covered_from = $from; $has_more = false; while ( $upper > 0 && $examined < self::MAX_ROWS_EXAMINED ) { $lower = max( 0, $upper - self::WINDOW_SIZE ); $results = $wpdb->get_results( $wpdb->prepare( "SELECT log_id, timestamp, level, message, context FROM {$table} WHERE log_id > %d AND log_id <= %d AND source = %s ORDER BY log_id ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $lower, $upper, $source ), ARRAY_A ); foreach ( (array) $results as $result ) { $row = self::build_row( $result ); if ( $row['timestamp'] < $from || $row['timestamp'] > $to ) { continue; } if ( null === $expand ) { $rows[] = $row; continue; } foreach ( $expand( $row ) as $expanded ) { $rows[] = $expanded; } } $examined += $upper - $lower; $oldest = $this->get_window_start_timestamp( $table, $lower ); $upper = $lower; /* * Windows are walked newest first, so everything left is older than * this window's oldest row. That row's second can carry on into the * next window, so stop only once a full page is newer than it. */ if ( count( array_filter( $rows, fn( array $row ) => null === $oldest || $row['timestamp'] > $oldest ) ) >= $limit ) { $covered_from = null === $oldest ? $from : $oldest; $has_more = $upper > 0 && $covered_from > $from; break; } if ( null === $oldest ) { continue; } if ( $oldest <= $from ) { $covered_from = $from; break; } $covered_from = $oldest; } if ( 0 === $upper ) { $covered_from = $from; } usort( $rows, fn( array $a, array $b ) => $a['timestamp'] <=> $b['timestamp'] ); return array( 'rows' => $rows, 'covered_from' => $covered_from, 'has_more' => $has_more, ); } /** * Returns the timestamp of the oldest row in the window just read, to * decide whether the scan has passed the start of the range. * * @param string $table The log table name. * @param int $lower The exclusive lower ID bound of the window just read. * @return int|null Null when the window held no rows at all. */ private function get_window_start_timestamp( string $table, int $lower ): ?int { global $wpdb; $timestamp = $wpdb->get_var( $wpdb->prepare( "SELECT timestamp FROM {$table} WHERE log_id > %d ORDER BY log_id ASC LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $lower ) ); return null === $timestamp ? null : (int) strtotime( $timestamp . ' UTC' ); } /** * Converts one table row to the shape the file reader produces. * * @param array $result The row as `wpdb` returned it. * @return array{timestamp: int, level: string, message: string, context: array|null, raw: string|null} */ private static function build_row( array $result ): array { $context = null; $raw = null; if ( ! empty( $result['context'] ) ) { $decoded = json_decode( (string) $result['context'], true ); if ( is_array( $decoded ) ) { $context = $decoded; } else { $raw = (string) $result['context']; } } return array( 'timestamp' => (int) strtotime( $result['timestamp'] . ' UTC' ), 'level' => (string) WC_Log_Levels::get_severity_level( (int) $result['level'] ), 'message' => (string) $result['message'], 'context' => $context, 'raw' => $raw, ); } }