\n\n"; /** * The directory where log files are stored. */ const LOG_DIRECTORY = WP_CONTENT_DIR . '/boost-cache/logs'; /** * The Process Identifier used by this Logger instance. * * @var int|float */ private $pid = null; /** * Get the singleton instance of the logger. */ public static function get_instance() { if ( self::$instance !== null ) { return self::$instance; } $instance = new Logger(); $prepared_log_file = $instance->prepare_file(); if ( $prepared_log_file instanceof Boost_Cache_Error ) { return $prepared_log_file; } self::$instance = $instance; return $instance; } private function __construct() { if ( function_exists( 'getmypid' ) ) { $this->pid = getmypid(); } else { // Where PID is not available, use the microtime of the first log of the session. $this->pid = microtime( true ); } } /** * Ensure that the log file exists, and if not, create it. */ private function prepare_file() { $log_file = $this->get_log_file(); if ( file_exists( $log_file ) ) { return true; } $directory = dirname( $log_file ); if ( ! Filesystem_Utils::create_directory( $directory ) ) { return new Boost_Cache_Error( 'could-not-create-log-dir', 'Could not create boost cache log directory' ); } return Filesystem_Utils::write_to_file( $log_file, self::LOG_HEADER ); } /** * Add a debug message to the log file after doing necessary checks. */ public static function debug( $message ) { $settings = Boost_Cache_Settings::get_instance(); if ( ! $settings->get_logging() ) { return; } $logger = self::get_instance(); // TODO: Check to make sure that current request IP is allowed to create logs. if ( $logger instanceof Boost_Cache_Error ) { return; } $logger->log( $message ); } /** * Writes a message to the log file. * * @param string $message - The message to write to the log file. */ public function log( $message ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $request_uri = htmlspecialchars( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '', ENT_QUOTES, 'UTF-8' ); // don't log the ABSPATH constant. Logs may be copied to a public forum. $message = str_replace( ABSPATH, '[...]/', $message ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode $line = json_encode( array( 'time' => gmdate( 'Y-m-d H:i:s' ), 'pid' => $this->pid, 'uri' => $request_uri, 'msg' => $message, 'uid' => uniqid(), // Uniquely identify this log line. ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $line . PHP_EOL, 3, $this->get_log_file() ); } /** * Reads the log file and returns the contents. * * @return string */ public static function read() { $instance = self::get_instance(); // If we failed to set up a Logger instance (e.g.: unwriteable directory), return the error as log content. if ( $instance instanceof Boost_Cache_Error ) { return $instance->get_error_message(); } $log_file = $instance->get_log_file(); if ( ! file_exists( $log_file ) ) { return ''; } // Get the content after skipping the LOG_HEADER. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $logs = file_get_contents( $log_file, false, null, strlen( self::LOG_HEADER ) ) ?? ''; $logs = explode( PHP_EOL, $logs ); $lines = array(); foreach ( $logs as $log ) { $line = json_decode( $log, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $line ) ) { continue; } // The current log format requires time, pid, uri, and msg. if ( ! isset( $line['time'] ) || ! isset( $line['pid'] ) || ! isset( $line['uri'] ) || ! isset( $line['msg'] ) ) { continue; } $info = sprintf( '[%s] [%s] ', $line['time'], $line['pid'] ); $formatted = $info . $line['uri']; // Add msg to the next line offset by the length of the info string. $formatted .= PHP_EOL . str_repeat( ' ', strlen( $info ) ) . $line['msg']; $lines[] = $formatted; } return implode( PHP_EOL, $lines ); } /** * Returns the path to the log file. * * @return string */ private static function get_log_file() { $today = gmdate( 'Y-m-d' ); return self::LOG_DIRECTORY . "/log-{$today}.log.php"; } public static function delete_old_logs() { Filesystem_Utils::gc_expired_files( self::LOG_DIRECTORY, 24 * 60 * 60 ); } }