declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Admin\Logging;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\File;
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
use WP_Filesystem_Direct;
* Default values for logging settings.
private const DEFAULTS = array(
'logging_enabled' => true,
'default_handler' => LogHandlerFileV2::class,
'retention_period_days' => 30,
'level_threshold' => 'none',
* The prefix for settings keys used in the options table.
private const PREFIX = 'woocommerce_logs_';
public function __construct() {
add_action( 'wc_logs_load_tab', array( $this, 'save_settings' ) );
* Get the directory for storing log files.
* The `wp_upload_dir` function takes into account the possibility of multisite, and handles changing
* the directory if the context is switched to a different site in the network mid-request.
* @param bool $create_dir Optional. True to attempt to create the log directory if it doesn't exist. Default true.
* @return string The full directory path, with trailing slash.
public static function get_log_directory( bool $create_dir = true ): string {
if ( true === Constants::get_constant( 'WC_LOG_DIR_CUSTOM' ) ) {
$dir = Constants::get_constant( 'WC_LOG_DIR' );
$upload_dir = wc_get_container()->get( LegacyProxy::class )->call_function( 'wp_upload_dir', null, $create_dir );
* Filter to change the directory for storing WooCommerce's log files.
* @param string $dir The full directory path, with trailing slash.
$dir = apply_filters( 'woocommerce_log_directory', $upload_dir['basedir'] . '/wc-logs/' );
$dir = trailingslashit( $dir );
if ( true === $create_dir ) {
$realpath = realpath( $dir );
if ( false === $realpath ) {
$result = wp_mkdir_p( $dir );
if ( true === $result ) {
// Create infrastructure to prevent listing contents of the logs directory.
$filesystem = FilesystemUtil::get_wp_filesystem();
$filesystem->put_contents( $dir . '.htaccess', 'deny from all' );
$filesystem->put_contents( $dir . 'index.html', '' );
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
* The definitions used by WC_Admin_Settings to render and save settings controls.
private function get_settings_definitions(): array {
'title' => __( 'Logs settings', 'woocommerce' ),
'id' => self::PREFIX . 'settings',
'logging_enabled' => array(
'title' => __( 'Logger', 'woocommerce' ),
'desc' => __( 'Enable logging', 'woocommerce' ),
'id' => self::PREFIX . 'logging_enabled',
'value' => $this->logging_is_enabled() ? 'yes' : 'no',
'default' => self::DEFAULTS['logging_enabled'] ? 'yes' : 'no',
'default_handler' => array(),
'retention_period_days' => array(),
'level_threshold' => array(),
'id' => self::PREFIX . 'settings',
if ( true === $this->logging_is_enabled() ) {
$settings['default_handler'] = $this->get_default_handler_setting_definition();
$settings['retention_period_days'] = $this->get_retention_period_days_setting_definition();
$settings['level_threshold'] = $this->get_level_threshold_setting_definition();
$default_handler = $this->get_default_handler();
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
$settings += $this->get_filesystem_settings_definitions();
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
$settings += $this->get_database_settings_definitions();
* The definition for the default_handler setting.
private function get_default_handler_setting_definition(): array {
$handler_options = array(
LogHandlerFileV2::class => __( 'File system (default)', 'woocommerce' ),
WC_Log_Handler_DB::class => __( 'Database (not recommended on live sites)', 'woocommerce' ),
* Filter the list of logging handlers that can be set as the default handler.
* @param array $handler_options An associative array of class_name => description.
$handler_options = apply_filters( 'woocommerce_logger_handler_options', $handler_options );
$current_value = $this->get_default_handler();
if ( ! array_key_exists( $current_value, $handler_options ) ) {
$handler_options[ $current_value ] = $current_value;
$desc[] = __( 'Note that if this setting is changed, any log entries that have already been recorded will remain stored in their current location, but will not migrate.', 'woocommerce' );
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_HANDLER' ) );
// translators: %s is the name of a code variable.
__( 'This setting cannot be changed here because it is defined in the %s constant.', 'woocommerce' ),
'<code>WC_LOG_HANDLER</code>'
'title' => __( 'Log storage', 'woocommerce' ),
'desc_tip' => __( 'This determines where log entries are saved.', 'woocommerce' ),
'id' => self::PREFIX . 'default_handler',
'value' => $current_value,
'default' => self::DEFAULTS['default_handler'],
'options' => $handler_options,
'disabled' => $hardcoded ? array_keys( $handler_options ) : array(),
'desc' => implode( '<br><br>', $desc ),
* The definition for the retention_period_days setting.
private function get_retention_period_days_setting_definition(): array {
$custom_attributes = array(
$hardcoded = has_filter( 'woocommerce_logger_days_to_retain_logs' );
$custom_attributes['disabled'] = 'true';
// translators: %s is the name of a filter hook.
__( 'This setting cannot be changed here because it is being set by a filter on the %s hook.', 'woocommerce' ),
'<code>woocommerce_logger_days_to_retain_logs</code>'
$file_delete_has_filter = LogHandlerFileV2::class === $this->get_default_handler() && has_filter( 'woocommerce_logger_delete_expired_file' );
if ( $file_delete_has_filter ) {
// translators: %s is the name of a filter hook.
__( 'The %s hook has a filter set, so some log files may have different retention settings.', 'woocommerce' ),
'<code>woocommerce_logger_delete_expired_file</code>'
'title' => __( 'Retention period', 'woocommerce' ),
'desc_tip' => __( 'This sets how many days log entries will be kept before being auto-deleted.', 'woocommerce' ),
'id' => self::PREFIX . 'retention_period_days',
'value' => $this->get_retention_period(),
'default' => self::DEFAULTS['retention_period_days'],
'custom_attributes' => $custom_attributes,
'row_class' => 'logs-retention-period-days',
__( 'days', 'woocommerce' ),
'desc' => implode( '<br><br>', $desc ),
* The definition for the level_threshold setting.
private function get_level_threshold_setting_definition(): array {
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_THRESHOLD' ) );
// translators: %1$s is the name of a code variable. %2$s is the name of a file.
__( 'This setting cannot be changed here because it is defined in the %1$s constant, probably in your %2$s file.', 'woocommerce' ),
'<code>WC_LOG_THRESHOLD</code>',
$labels = WC_Log_Levels::get_all_level_labels();
$labels['none'] = __( 'None', 'woocommerce' );
$custom_attributes = array();
$custom_attributes['disabled'] = 'true';
'title' => __( 'Level threshold', 'woocommerce' ),
'desc_tip' => __( 'This sets the minimum severity level of logs that will be stored. Lower severity levels will be ignored. "None" means all logs will be stored.', 'woocommerce' ),
'id' => self::PREFIX . 'level_threshold',
'value' => $this->get_level_threshold(),
'default' => self::DEFAULTS['level_threshold'],
'custom_attributes' => $custom_attributes,
* The definitions used by WC_Admin_Settings to render settings related to filesystem log handlers.
private function get_filesystem_settings_definitions(): array {
$location_info = array();
$directory = self::get_log_directory();
$filesystem = FilesystemUtil::get_wp_filesystem();
if ( $filesystem instanceof WP_Filesystem_Direct ) {
$status_info[] = __( '✅ Ready', 'woocommerce' );
$status_info[] = __( '⚠️ The file system is not configured for direct writes. This could cause problems for the logger.', 'woocommerce' );
$status_info[] = __( 'You may want to switch to the database for log storage.', 'woocommerce' );
} catch ( Exception $exception ) {
$status_info[] = __( '⚠️ The file system connection could not be initialized.', 'woocommerce' );
$status_info[] = __( 'You may want to switch to the database for log storage.', 'woocommerce' );
$location_info[] = sprintf(
// translators: %s is a location in the filesystem.
__( 'Log files are stored in this directory: %s', 'woocommerce' ),
if ( ! wp_is_writable( $directory ) ) {
$location_info[] = __( '⚠️ This directory does not appear to be writable.', 'woocommerce' );
$location_info[] = sprintf(
// translators: %s is an amount of computer disk space, e.g. 5 KB.
__( 'Directory size: %s', 'woocommerce' ),
size_format( wc_get_container()->get( FileController::class )->get_log_directory_size() )
'title' => __( 'File system settings', 'woocommerce' ),
'id' => self::PREFIX . 'settings',
'title' => __( 'Status', 'woocommerce' ),
'text' => implode( "\n\n", $status_info ),
'log_directory' => array(
'title' => __( 'Location', 'woocommerce' ),
'text' => implode( "\n\n", $location_info ),
'entry_format' => array(),
'id' => self::PREFIX . 'settings',
* The definitions used by WC_Admin_Settings to render settings related to database log handlers.
private function get_database_settings_definitions(): array {
$table = "{$wpdb->prefix}woocommerce_log";
$location_info = sprintf(
// translators: %s is the name of a table in the database.
__( 'Log entries are stored in this database table: %s', 'woocommerce' ),
'title' => __( 'Database settings', 'woocommerce' ),
'id' => self::PREFIX . 'settings',
'database_table' => array(
'title' => __( 'Location', 'woocommerce' ),
'text' => $location_info,
'id' => self::PREFIX . 'settings',
* Handle the submission of the settings form and update the settings values.
* @param string $view The current view within the Logs tab.
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
public function save_settings( string $view ): void {
$is_saving = 'settings' === $view && isset( $_POST['save_settings'] );
check_admin_referer( self::PREFIX . 'settings' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to manage logging settings.', 'woocommerce' ) );
$settings = $this->get_settings_definitions();
WC_Admin_Settings::save_fields( $settings );
* Render the settings page.
public function render_form(): void {
$settings = $this->get_settings_definitions();
<form id="mainform" class="wc-logs-settings" method="post">
<?php WC_Admin_Settings::output_fields( $settings ); ?>
* Action fires after the built-in logging settings controls have been rendered.
* This is intended as a way to allow other logging settings controls to be added by extensions.
* @param bool $enabled True if logging is currently enabled.
do_action( 'wc_logs_settings_form_fields', $this->logging_is_enabled() );
<?php wp_nonce_field( self::PREFIX . 'settings' ); ?>
<?php submit_button( __( 'Save changes', 'woocommerce' ), 'primary', 'save_settings' ); ?>
* Determine the current value of the logging_enabled setting.
public function logging_is_enabled(): bool {
$key = self::PREFIX . 'logging_enabled';
$enabled = WC_Admin_Settings::get_option( $key, self::DEFAULTS['logging_enabled'] );
$enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
if ( is_null( $enabled ) ) {
$enabled = self::DEFAULTS['logging_enabled'];
* Determine the current value of the default_handler setting.
public function get_default_handler(): string {
$key = self::PREFIX . 'default_handler';
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
if ( is_null( $handler ) ) {
$handler = WC_Admin_Settings::get_option( $key );
if ( ! class_exists( $handler ) || ! is_a( $handler, 'WC_Log_Handler_Interface', true ) ) {
$handler = self::DEFAULTS['default_handler'];
* Determine the current value of the retention_period_days setting.
public function get_retention_period(): int {
$key = self::PREFIX . 'retention_period_days';
$retention_period = self::DEFAULTS['retention_period_days'];
if ( has_filter( 'woocommerce_logger_days_to_retain_logs' ) ) {
* Filter the retention period of log entries.
* @param int $days The number of days to retain log entries.
$retention_period = apply_filters( 'woocommerce_logger_days_to_retain_logs', $retention_period );
$retention_period = WC_Admin_Settings::get_option( $key );