declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Admin\Logging;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Logging\{ LogHandlerFileV2, Settings };
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController, FileListTable, SearchListTable };
use WC_Log_Handler_File, WC_Log_Handler_DB;
* Instance of FileController.
private $file_controller;
* Instance of FileListTable or SearchListTable.
* @var FileListTable|SearchListTable
* Initialize dependencies.
* @param FileController $file_controller Instance of FileController.
* @param Settings $settings Instance of Settings.
final public function init(
FileController $file_controller,
$this->file_controller = $file_controller;
$this->settings = $settings;
* Add callbacks to hooks.
private function init_hooks(): void {
add_action( 'load-woocommerce_page_wc-status', array( $this, 'maybe_do_logs_tab_action' ), 2 );
add_action( 'wc_logs_load_tab', array( $this, 'setup_screen_options' ) );
add_action( 'wc_logs_load_tab', array( $this, 'handle_list_table_bulk_actions' ) );
add_action( 'wc_logs_load_tab', array( $this, 'notices' ) );
* Determine if the current tab on the Status page is Logs, and if so, fire an action.
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
public function maybe_do_logs_tab_action(): void {
$is_logs_tab = 'logs' === filter_input( INPUT_GET, 'tab' );
$params = $this->get_query_params( array( 'view' ) );
* Action fires when the Logs tab starts loading.
* @param string $view The current view within the Logs tab.
do_action( 'wc_logs_load_tab', $params['view'] );
* Notices to display on Logs screens.
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
public function notices() {
if ( ! $this->settings->logging_is_enabled() ) {
<div class="notice notice-warning">
// translators: %s is a URL to another admin screen.
wp_kses_post( __( 'Logging is disabled. It can be enabled in <a href="%s">Logs Settings</a>.', 'woocommerce' ) ),
esc_url( add_query_arg( 'view', 'settings', $this->get_logs_tab_url() ) )
* Get the canonical URL for the Logs tab of the Status admin page.
public function get_logs_tab_url(): string {
* Render the "Logs" tab, depending on the current default log handler.
public function render(): void {
$handler = $this->settings->get_default_handler();
$params = $this->get_query_params( array( 'view' ) );
$this->render_section_nav();
if ( 'settings' === $params['view'] ) {
$this->settings->render_form();
case LogHandlerFileV2::class:
case WC_Log_Handler_DB::class:
WC_Admin_Status::status_logs_db();
case WC_Log_Handler_File::class:
WC_Admin_Status::status_logs_file();
* Action fires only if there is not a built-in rendering method for the current default log handler.
* This is intended as a way for extensions to render log views for custom handlers.
do_action( 'wc_logs_render_page', $handler );
* Render navigation to switch between logs browsing and settings.
private function render_section_nav(): void {
$params = $this->get_query_params( array( 'view' ) );
$browse_url = $this->get_logs_tab_url();
$settings_url = add_query_arg( 'view', 'settings', $this->get_logs_tab_url() );
'<a href="%1$s"%2$s>%3$s</a>',
'settings' !== $params['view'] ? ' class="current"' : '',
esc_html__( 'Browse', 'woocommerce' )
'<a href="%1$s"%2$s>%3$s</a>',
esc_url( $settings_url ),
'settings' === $params['view'] ? ' class="current"' : '',
esc_html__( 'Settings', 'woocommerce' )
* Render the views for the FileV2 log handler.
private function render_filev2(): void {
$params = $this->get_query_params( array( 'view' ) );
switch ( $params['view'] ) {
$this->render_list_files_view();
$this->render_search_results_view();
$this->render_single_file_view();
* Render the file list view.
private function render_list_files_view(): void {
$params = $this->get_query_params( array( 'order', 'orderby', 'source', 'view' ) );
$defaults = $this->get_query_param_defaults();
$list_table = $this->get_list_table( $params['view'] );
$list_table->prepare_items();
<header id="logs-header" class="wc-logs-header">
<?php esc_html_e( 'Browse log files', 'woocommerce' ); ?>
<?php $this->render_search_field(); ?>
<form id="logs-list-table-form" method="get">
<input type="hidden" name="page" value="wc-status" />
<input type="hidden" name="tab" value="logs" />
<?php foreach ( $params as $key => $value ) : ?>
<?php if ( $value !== $defaults[ $key ] ) : ?>
name="<?php echo esc_attr( $key ); ?>"
value="<?php echo esc_attr( $value ); ?>"
<?php $list_table->display(); ?>
* Render the single file view.
private function render_single_file_view(): void {
$params = $this->get_query_params( array( 'file_id', 'view' ) );
$file = $this->file_controller->get_file_by_id( $params['file_id'] );
if ( is_wp_error( $file ) ) {
<div class="notice notice-error notice-inline">
<?php echo wp_kses_post( wpautop( $file->get_error_message() ) ); ?>
'<p><a href="%1$s">%2$s</a></p>',
esc_url( $this->get_logs_tab_url() ),
esc_html__( 'Return to the file list.', 'woocommerce' )
$rotations = $this->file_controller->get_file_rotations( $file->get_file_id() );
$rotation_url_base = add_query_arg( 'view', 'single_file', $this->get_logs_tab_url() );
$download_url = add_query_arg(
'file_id' => array( $file->get_file_id() ),
wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' )
$delete_url = add_query_arg(
'file_id' => array( $file->get_file_id() ),
wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' )
$delete_confirmation_js = sprintf(
"return window.confirm( '%s' )",
esc_js( __( 'Delete this log file permanently?', 'woocommerce' ) )
$stream = $file->get_stream();
<header id="logs-header" class="wc-logs-header">
// translators: %s is the name of a log file.
esc_html__( 'Viewing log file %s', 'woocommerce' ),
'<span class="file-id">%s</span>',
esc_html( $file->get_file_id() )
<?php if ( count( $rotations ) > 1 ) : ?>
<nav class="wc-logs-single-file-rotations">
<h3><?php esc_html_e( 'File rotations:', 'woocommerce' ); ?></h3>
<ul class="wc-logs-rotation-links">
<?php if ( isset( $rotations['current'] ) ) : ?>
'<li><a href="%1$s" class="button button-small button-%2$s">%3$s</a></li>',
esc_url( add_query_arg( 'file_id', $rotations['current']->get_file_id(), $rotation_url_base ) ),
$file->get_file_id() === $rotations['current']->get_file_id() ? 'primary' : 'secondary',
esc_html__( 'Current', 'woocommerce' )
unset( $rotations['current'] );
<?php foreach ( $rotations as $rotation ) : ?>
'<li><a href="%1$s" class="button button-small button-%2$s">%3$s</a></li>',
esc_url( add_query_arg( 'file_id', $rotation->get_file_id(), $rotation_url_base ) ),
$file->get_file_id() === $rotation->get_file_id() ? 'primary' : 'secondary',
absint( $rotation->get_rotation() )
<div class="wc-logs-single-file-actions">
'<a href="%1$s" class="button button-secondary">%2$s</a>',
esc_url( $download_url ),
esc_html__( 'Download', 'woocommerce' )
'<a href="%1$s" class="button button-secondary" onclick="%2$s">%3$s</a>',
esc_attr( $delete_confirmation_js ),
esc_html__( 'Delete permanently', 'woocommerce' )
<section id="logs-entries" class="wc-logs-entries">
<?php while ( ! feof( $stream ) ) : ?>
$line = fgets( $stream );
if ( is_string( $line ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_line does the escaping.
echo $this->format_line( $line, $line_number );
// Clear the line number hash and highlight with a click.
document.documentElement.addEventListener( 'click', ( event ) => {
if ( window.location.hash && ! event.target.classList.contains( 'line-anchor' ) ) {
let scrollPos = document.documentElement.scrollTop;
window.location.hash = '';
document.documentElement.scrollTop = scrollPos;
history.replaceState( null, '', window.location.pathname + window.location.search );
* Render the search results view.
private function render_search_results_view(): void {
$params = $this->get_query_params( array( 'view' ) );
$list_table = $this->get_list_table( $params['view'] );
$list_table->prepare_items();
<header id="logs-header" class="wc-logs-header">
<h2><?php esc_html_e( 'Search results', 'woocommerce' ); ?></h2>
<?php $this->render_search_field(); ?>
<?php $list_table->display(); ?>
* Get the default values for URL query params for FileV2 views.
public function get_query_param_defaults(): array {
'order' => $this->file_controller::DEFAULTS_GET_FILES['order'],
'orderby' => $this->file_controller::DEFAULTS_GET_FILES['orderby'],
'source' => $this->file_controller::DEFAULTS_GET_FILES['source'],
* Get and validate URL query params for FileV2 views.
* @param array $param_keys Optional. The names of the params you want to get.
public function get_query_params( array $param_keys = array() ): array {
$defaults = $this->get_query_param_defaults();
$params = filter_input_array(
'filter' => FILTER_CALLBACK,
'options' => function ( $file_id ) {
return sanitize_file_name( wp_unslash( $file_id ) );
'filter' => FILTER_VALIDATE_REGEXP,
'regexp' => '/^(asc|desc)$/i',
'default' => $defaults['order'],
'filter' => FILTER_VALIDATE_REGEXP,
'regexp' => '/^(created|modified|source|size)$/',
'default' => $defaults['orderby'],
'filter' => FILTER_CALLBACK,
'options' => function ( $search ) {
return esc_html( wp_unslash( $search ) );
'filter' => FILTER_CALLBACK,
'options' => function ( $source ) {
return File::sanitize_source( wp_unslash( $source ) );
'filter' => FILTER_VALIDATE_REGEXP,