wp-discourse/lib/logs/handlers/file-handler.php
Angus McLeod 22ee91dfb5
Two Five Seven (#541)
* Don't try to add url to <head> if it's not present

* Update js config and formatting for comment block and sidebar

* PHP Linting

* FIX: Don't auto-publish updates to existing posts.

See: https://meta.discourse.org/t/disable-posting-wordpress-articles-to-discourse-when-theyre-updated/204488

* Bump version and release notes.

* Fix remote-post.php linting

* Update tests.yml to install svn

* Re-generate comments js build
2025-04-15 16:53:23 -07:00

359 lines
9.4 KiB
PHP
Vendored

<?php
/**
* Log handler that writes logs to a .log file
*
* @package WPDiscourse
* @todo Review phpcs exclusions.
*/
namespace WPDiscourse\Logs;
use WPDiscourse\Monolog\Handler\StreamHandler;
/**
* Class FileHandler
*/
class FileHandler extends StreamHandler {
const DATE_FORMAT = 'Y-m-d';
const FILE_NAMESPACE = 'wp-discourse';
/**
* Flag to determine whether file handler can be used.
*
* @var null|FileHandler
*/
public $enabled;
/**
* FileHandler's instance of FileManager
*
* @var null|FileHandler
*/
public $file_manager;
/**
* Maximum number of log files
*
* @access protected
* @var null|FileHandler
*/
protected $max_files;
/**
* Flag to determine when to rotate log files
*
* @access protected
* @var null|FileHandler
*/
protected $must_rotate;
/**
* Allows for custom datetime to be set for testing purposes
*
* @access protected
* @var null|FileHandler
*/
protected $datetime;
/**
* Size limit for log files
*
* @access protected
* @var null|FileHandler
*/
protected $file_size_limit;
/**
* Number to distinguish files with the same date
*
* @access protected
* @var null|FileHandler
*/
protected $file_number;
/**
* FileHandler constructor
*
* @param object $file_manager Instance of file manager.
* @param integer $file_size_limit Size limit of .log file.
* @param integer $max_files Maximum number of .log files.
* @param string $datetime Current datetime (for testing purposes).
*/
public function __construct( $file_manager, $file_size_limit = ( 5 * 1024 * 1024 ), $max_files = 10, $datetime = null ) {
$file_manager->validate();
$this->enabled = $file_manager->ready();
if ( ! $this->enabled ) {
return;
}
$this->file_manager = $file_manager;
$this->max_files = $max_files;
$this->file_size_limit = $file_size_limit;
$this->file_number = $this->current_file_number();
$this->datetime = $datetime;
// Arguments for StreamHandler.
$current_url = $this->current_file_url();
$url = $current_url ? $current_url : $this->build_new_file_url();
$level = Logger::DEBUG; // we want this handler for all levels.
$bubble = false; // we currently have only one handler, so no bubbling.
$file_permission = null; // we handle permissions in the file manager.
$use_locking = true; // we want a log file lock if possible.
StreamHandler::__construct( $url, $level, $bubble, $file_permission, $use_locking );
}
/**
* Public method to determine whether file handler is enabled
*/
public function enabled() {
return $this->enabled;
}
/**
* {@inheritdoc}
*/
public function close() {
parent::close();
if ( true === $this->must_rotate ) {
$this->rotate();
}
}
/**
* {@inheritdoc}
*/
public function reset() {
parent::reset();
if ( true === $this->must_rotate ) {
$this->rotate();
}
}
/**
* {@inheritdoc}
*
* @param array $record Log record being written.
*/
protected function write( array $record ) {
if ( null === $this->must_rotate ) {
$this->must_rotate = ! file_exists( $this->url );
}
// Ensure we're writing to today's log file.
$date = $record['datetime']->format( static::DATE_FORMAT );
if ( ! preg_match( '/' . $date . '/', $this->url ) ) {
$files_for_date = $this->list_files( "*$date*" );
if ( count( $files_for_date ) > 0 ) {
$this->url = $files_for_date[0];
} else {
$this->must_rotate = true;
$this->close();
}
}
// Ensure the log file is not too large.
if ( file_exists( $this->url ) && ! $this->validate_size() ) {
++$this->file_number;
$this->must_rotate = true;
$this->close();
}
StreamHandler::write( $record );
}
/**
* Returns the log file size limit
*/
public function get_file_size_limit() {
return $this->file_size_limit;
}
/**
* Lists log files in descending order by date and number
*
* @param string $filter optional. Regex pattern for filename.
*/
public function list_files( $filter = '*' ) {
$files = glob( $this->file_manager->logs_dir . "/$filter.log" );
usort(
$files,
function ( $a, $b ) {
$a_date = $this->get_date_from_url( $a );
$b_date = $this->get_date_from_url( $b );
if ( $a_date > $b_date ) {
return -1;
}
if ( $a_date < $b_date ) {
return 1;
}
$a_number = $this->get_number_from_url( $a );
$b_number = $this->get_number_from_url( $b );
if ( $a_number > $b_number ) {
return -1;
}
if ( $a_number < $b_number ) {
return 1;
}
return strcmp( $a, $b );
}
);
return $files;
}
/**
* Returns the url of the current log file
*/
public function current_file_url() {
$date = $this->get_date();
$files = $this->list_files( "*$date*" );
if ( count( $files ) > 0 ) {
return reset( $files );
} else {
return false;
}
}
/**
* Returns the current log file number
*/
public function current_file_number() {
$file_url = $this->current_file_url();
if ( $file_url ) {
return $this->get_number_from_url( $file_url );
} else {
return 1;
}
}
/**
* Handles log rotation
*/
protected function rotate() {
$this->url = $this->build_new_file_url();
$files = $this->list_files();
if ( count( $files ) >= ( $this->max_files - 1 ) ) {
foreach ( array_slice( $files, ( $this->max_files - 1 ) ) as $file ) {
if ( is_writable( $file ) ) {
// Note from monolog/monolog:
// "suppress errors here as unlink() might fail if two processes
// are cleaning up/rotating at the same time.".
// phpcs:disable WordPress.PHP.DevelopmentFunctions
set_error_handler(
function () {
return false;
}
);
unlink( $file );
restore_error_handler();
// phpcs:enabled WordPress.PHP.DevelopmentFunctions
}
}
}
$this->must_rotate = false;
}
/**
* Builds a new log file url
*/
protected function build_new_file_url() {
$dir_path = $this->file_manager->logs_dir;
$name = $this->file_name();
$hash = wp_hash( $name, 'nonce' );
$extension = 'log';
return "$dir_path/$name-$hash.$extension";
}
/**
* Returns date used by file handler
*/
public function get_date() {
if ( isset( $this->datetime ) ) {
return $this->datetime->format( static::DATE_FORMAT );
} else {
return gmdate( static::DATE_FORMAT );
}
}
/**
* Validates size of current log file against size limit
*/
protected function validate_size() {
// Note https://github.com/WordPress/WordPress-Coding-Standards/pull/1265#issuecomment-405143028.
// Note https://github.com/woocommerce/woocommerce/issues/6091.
$handle = fopen( $this->url, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions
$stat = fstat( $handle );
$last_line_byte_buffer = 100;
return $stat['size'] <= ( $this->file_size_limit - $last_line_byte_buffer );
}
/**
* Builds current log file name
*/
protected function file_name() {
$date = $this->get_date();
$number = $this->file_number;
return $this->build_filename( $date, $number );
}
/**
* Build file name
*
* @param string $date Log date.
* @param string $number Log number.
*/
protected function build_filename( $date, $number ) {
$namespace = static::FILE_NAMESPACE;
return "$namespace-$date-$number";
}
/**
* Retrieves file number from file url
*
* @param string $file_url URL of log file.
*/
public function get_number_from_url( $file_url ) {
$parts = explode( '-', $file_url );
end( $parts );
return (int) prev( $parts );
}
/**
* Retrieves file date from file url
*
* @param string $file_url URL of log file.
*/
public function get_date_from_url( $file_url ) {
$parts = explode( '-', $file_url );
$date_parts = array_slice( array_slice( $parts, -5 ), 0, 3 );
return implode( '-', $date_parts );
}
/**
* Retrieves file name from file url
*
* @param string $file_url URL of log file.
*/
public function get_filename( $file_url ) {
$date = $this->get_date_from_url( $file_url );
$number = $this->get_number_from_url( $file_url );
return $this->build_filename( $date, $number );
}
}