Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
4.62% |
3 / 65 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| Logger | |
4.62% |
3 / 65 |
|
0.00% |
0 / 8 |
567.39 | |
0.00% |
0 / 1 |
| get_instance | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| __construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| prepare_file | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| debug | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
4.68 | |||
| log | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
| read | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
110 | |||
| get_log_file | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| delete_old_logs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /* |
| 3 | * This file may be called before WordPress is fully initialized. See the README file for info. |
| 4 | */ |
| 5 | |
| 6 | namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress; |
| 7 | |
| 8 | use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Filter_Older; |
| 9 | use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Simple_Delete; |
| 10 | |
| 11 | /** |
| 12 | * A utility that manages logging for the boost cache. |
| 13 | */ |
| 14 | class Logger { |
| 15 | /** |
| 16 | * The singleton instance of the logger. |
| 17 | * |
| 18 | * @var self |
| 19 | */ |
| 20 | private static $instance = null; |
| 21 | |
| 22 | /** |
| 23 | * The header to place on top of every log file. |
| 24 | */ |
| 25 | const LOG_HEADER = "<?php die(); // This file is not intended to be accessed directly. ?>\n\n"; |
| 26 | |
| 27 | /** |
| 28 | * The directory where log files are stored. |
| 29 | */ |
| 30 | const LOG_DIRECTORY = WP_CONTENT_DIR . '/boost-cache/logs'; |
| 31 | |
| 32 | /** |
| 33 | * The Process Identifier used by this Logger instance. |
| 34 | * |
| 35 | * @var int|float |
| 36 | */ |
| 37 | private $pid = null; |
| 38 | |
| 39 | /** |
| 40 | * Get the singleton instance of the logger. |
| 41 | */ |
| 42 | public static function get_instance() { |
| 43 | if ( self::$instance !== null ) { |
| 44 | return self::$instance; |
| 45 | } |
| 46 | |
| 47 | $instance = new Logger(); |
| 48 | $prepared_log_file = $instance->prepare_file(); |
| 49 | if ( $prepared_log_file instanceof Boost_Cache_Error ) { |
| 50 | return $prepared_log_file; |
| 51 | } |
| 52 | |
| 53 | self::$instance = $instance; |
| 54 | return $instance; |
| 55 | } |
| 56 | |
| 57 | private function __construct() { |
| 58 | if ( function_exists( 'getmypid' ) ) { |
| 59 | $this->pid = getmypid(); |
| 60 | } else { |
| 61 | // Where PID is not available, use the microtime of the first log of the session. |
| 62 | $this->pid = microtime( true ); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Ensure that the log file exists, and if not, create it. |
| 68 | */ |
| 69 | private function prepare_file() { |
| 70 | $log_file = $this->get_log_file(); |
| 71 | if ( file_exists( $log_file ) ) { |
| 72 | return true; |
| 73 | } |
| 74 | |
| 75 | $directory = dirname( $log_file ); |
| 76 | if ( ! Filesystem_Utils::create_directory( $directory ) ) { |
| 77 | return new Boost_Cache_Error( 'could-not-create-log-dir', 'Could not create boost cache log directory' ); |
| 78 | } |
| 79 | |
| 80 | return Filesystem_Utils::write_to_file( $log_file, self::LOG_HEADER ); |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Add a debug message to the log file after doing necessary checks. |
| 85 | */ |
| 86 | public static function debug( $message ) { |
| 87 | $settings = Boost_Cache_Settings::get_instance(); |
| 88 | if ( ! $settings->get_logging() ) { |
| 89 | return; |
| 90 | } |
| 91 | |
| 92 | $logger = self::get_instance(); |
| 93 | |
| 94 | // TODO: Check to make sure that current request IP is allowed to create logs. |
| 95 | |
| 96 | if ( $logger instanceof Boost_Cache_Error ) { |
| 97 | return; |
| 98 | } |
| 99 | |
| 100 | $logger->log( $message ); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Writes a message to the log file. |
| 105 | * |
| 106 | * @param string $message - The message to write to the log file. |
| 107 | */ |
| 108 | public function log( $message ) { |
| 109 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash |
| 110 | $request_uri = htmlspecialchars( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '<unknown request uri>', ENT_QUOTES, 'UTF-8' ); |
| 111 | |
| 112 | // don't log the ABSPATH constant. Logs may be copied to a public forum. |
| 113 | $message = str_replace( ABSPATH, '[...]/', $message ); |
| 114 | |
| 115 | // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode |
| 116 | $line = json_encode( |
| 117 | array( |
| 118 | 'time' => gmdate( 'Y-m-d H:i:s' ), |
| 119 | 'pid' => $this->pid, |
| 120 | 'uri' => $request_uri, |
| 121 | 'msg' => $message, |
| 122 | 'uid' => uniqid(), // Uniquely identify this log line. |
| 123 | ), |
| 124 | JSON_UNESCAPED_SLASHES |
| 125 | ); |
| 126 | |
| 127 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 128 | error_log( $line . PHP_EOL, 3, $this->get_log_file() ); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Reads the log file and returns the contents. |
| 133 | * |
| 134 | * @return string |
| 135 | */ |
| 136 | public static function read() { |
| 137 | $instance = self::get_instance(); |
| 138 | |
| 139 | // If we failed to set up a Logger instance (e.g.: unwriteable directory), return the error as log content. |
| 140 | if ( $instance instanceof Boost_Cache_Error ) { |
| 141 | return $instance->get_error_message(); |
| 142 | } |
| 143 | |
| 144 | $log_file = $instance->get_log_file(); |
| 145 | if ( ! file_exists( $log_file ) ) { |
| 146 | return ''; |
| 147 | } |
| 148 | |
| 149 | // Get the content after skipping the LOG_HEADER. |
| 150 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents |
| 151 | $logs = file_get_contents( $log_file, false, null, strlen( self::LOG_HEADER ) ) ?? ''; |
| 152 | $logs = explode( PHP_EOL, $logs ); |
| 153 | $lines = array(); |
| 154 | |
| 155 | foreach ( $logs as $log ) { |
| 156 | $line = json_decode( $log, true ); |
| 157 | if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $line ) ) { |
| 158 | continue; |
| 159 | } |
| 160 | |
| 161 | // The current log format requires time, pid, uri, and msg. |
| 162 | if ( ! isset( $line['time'] ) || ! isset( $line['pid'] ) || ! isset( $line['uri'] ) || ! isset( $line['msg'] ) ) { |
| 163 | continue; |
| 164 | } |
| 165 | |
| 166 | $info = sprintf( |
| 167 | '[%s] [%s] ', |
| 168 | $line['time'], |
| 169 | $line['pid'] |
| 170 | ); |
| 171 | |
| 172 | $formatted = $info . $line['uri']; |
| 173 | // Add msg to the next line offset by the length of the info string. |
| 174 | $formatted .= PHP_EOL . str_repeat( ' ', strlen( $info ) ) . $line['msg']; |
| 175 | |
| 176 | $lines[] = $formatted; |
| 177 | } |
| 178 | |
| 179 | return implode( PHP_EOL, $lines ); |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * Returns the path to the log file. |
| 184 | * |
| 185 | * @return string |
| 186 | */ |
| 187 | private static function get_log_file() { |
| 188 | $today = gmdate( 'Y-m-d' ); |
| 189 | return self::LOG_DIRECTORY . "/log-{$today}.log.php"; |
| 190 | } |
| 191 | |
| 192 | public static function delete_old_logs() { |
| 193 | Filesystem_Utils::iterate_directory( self::LOG_DIRECTORY, new Filter_Older( time() - 24 * 60 * 60, new Simple_Delete() ) ); |
| 194 | } |
| 195 | } |