FileV2
1 year ago
LogHandlerFileV2.php
1 year ago
PageController.php
1 year ago
Settings.php
1 year ago
LogHandlerFileV2.php
304 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Automattic\WooCommerce\Internal\Admin\Logging; |
| 4 | |
| 5 | use Automattic\Jetpack\Constants; |
| 6 | use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController }; |
| 7 | use WC_Log_Handler; |
| 8 | |
| 9 | /** |
| 10 | * LogHandlerFileV2 class. |
| 11 | */ |
| 12 | class LogHandlerFileV2 extends WC_Log_Handler { |
| 13 | /** |
| 14 | * Instance of the FileController class. |
| 15 | * |
| 16 | * @var FileController |
| 17 | */ |
| 18 | private $file_controller; |
| 19 | |
| 20 | /** |
| 21 | * Instance of the Settings class. |
| 22 | * |
| 23 | * @var Settings |
| 24 | */ |
| 25 | private $settings; |
| 26 | |
| 27 | /** |
| 28 | * LogHandlerFileV2 class. |
| 29 | */ |
| 30 | public function __construct() { |
| 31 | $this->file_controller = wc_get_container()->get( FileController::class ); |
| 32 | $this->settings = wc_get_container()->get( Settings::class ); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Handle a log entry. |
| 37 | * |
| 38 | * @param int $timestamp Log timestamp. |
| 39 | * @param string $level emergency|alert|critical|error|warning|notice|info|debug. |
| 40 | * @param string $message Log message. |
| 41 | * @param array $context { |
| 42 | * Optional. Additional information for log handlers. Any data can be added here, but there are some array |
| 43 | * keys that have special behavior. |
| 44 | * |
| 45 | * @type string $source Determines which log file to write to. Must be at least 3 characters in length. |
| 46 | * @type bool $backtrace True to include a backtrace that shows where the logging function got called. |
| 47 | * } |
| 48 | * |
| 49 | * @return bool False if value was not handled and true if value was handled. |
| 50 | */ |
| 51 | public function handle( $timestamp, $level, $message, $context ) { |
| 52 | $context = (array) $context; |
| 53 | |
| 54 | if ( isset( $context['source'] ) && is_string( $context['source'] ) && strlen( $context['source'] ) >= 3 ) { |
| 55 | $source = sanitize_title( trim( $context['source'] ) ); |
| 56 | } else { |
| 57 | $source = $this->determine_source(); |
| 58 | } |
| 59 | |
| 60 | $entry = static::format_entry( $timestamp, $level, $message, $context ); |
| 61 | |
| 62 | $written = $this->file_controller->write_to_file( $source, $entry, $timestamp ); |
| 63 | |
| 64 | if ( $written ) { |
| 65 | $this->file_controller->invalidate_cache(); |
| 66 | } |
| 67 | |
| 68 | return $written; |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Builds a log entry text from level, timestamp, and message. |
| 73 | * |
| 74 | * @param int $timestamp Log timestamp. |
| 75 | * @param string $level emergency|alert|critical|error|warning|notice|info|debug. |
| 76 | * @param string $message Log message. |
| 77 | * @param array $context Additional information for log handlers. |
| 78 | * |
| 79 | * @return string Formatted log entry. |
| 80 | */ |
| 81 | protected static function format_entry( $timestamp, $level, $message, $context ) { |
| 82 | $time_string = static::format_time( $timestamp ); |
| 83 | $level_string = strtoupper( $level ); |
| 84 | |
| 85 | if ( isset( $context['backtrace'] ) && true === filter_var( $context['backtrace'], FILTER_VALIDATE_BOOLEAN ) ) { |
| 86 | $context['backtrace'] = static::get_backtrace(); |
| 87 | } |
| 88 | |
| 89 | $context_for_entry = $context; |
| 90 | unset( $context_for_entry['source'] ); |
| 91 | |
| 92 | if ( ! empty( $context_for_entry ) ) { |
| 93 | $formatted_context = wp_json_encode( $context_for_entry, JSON_UNESCAPED_UNICODE ); |
| 94 | $message .= stripslashes( " CONTEXT: $formatted_context" ); |
| 95 | } |
| 96 | |
| 97 | $entry = "$time_string $level_string $message"; |
| 98 | |
| 99 | // phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment |
| 100 | /** This filter is documented in includes/abstracts/abstract-wc-log-handler.php */ |
| 101 | return apply_filters( |
| 102 | 'woocommerce_format_log_entry', |
| 103 | $entry, |
| 104 | array( |
| 105 | 'timestamp' => $timestamp, |
| 106 | 'level' => $level, |
| 107 | 'message' => $message, |
| 108 | 'context' => $context, |
| 109 | ) |
| 110 | ); |
| 111 | // phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Figures out a source string to use for a log entry based on where the log method was called from. |
| 116 | * |
| 117 | * @return string |
| 118 | */ |
| 119 | protected function determine_source(): string { |
| 120 | $source_roots = array( |
| 121 | 'mu-plugin' => trailingslashit( Constants::get_constant( 'WPMU_PLUGIN_DIR' ) ), |
| 122 | 'plugin' => trailingslashit( Constants::get_constant( 'WP_PLUGIN_DIR' ) ), |
| 123 | 'theme' => trailingslashit( get_theme_root() ), |
| 124 | ); |
| 125 | |
| 126 | $source = ''; |
| 127 | $backtrace = static::get_backtrace(); |
| 128 | |
| 129 | foreach ( $backtrace as $frame ) { |
| 130 | if ( ! isset( $frame['file'] ) ) { |
| 131 | continue; |
| 132 | } |
| 133 | |
| 134 | foreach ( $source_roots as $type => $path ) { |
| 135 | if ( 0 === strpos( $frame['file'], $path ) ) { |
| 136 | $relative_path = trim( substr( $frame['file'], strlen( $path ) ), DIRECTORY_SEPARATOR ); |
| 137 | |
| 138 | if ( 'mu-plugin' === $type ) { |
| 139 | $info = pathinfo( $relative_path ); |
| 140 | |
| 141 | if ( '.' === $info['dirname'] ) { |
| 142 | $source = "$type-" . $info['filename']; |
| 143 | } else { |
| 144 | $source = "$type-" . $info['dirname']; |
| 145 | } |
| 146 | |
| 147 | break 2; |
| 148 | } |
| 149 | |
| 150 | $segments = explode( DIRECTORY_SEPARATOR, $relative_path ); |
| 151 | if ( is_array( $segments ) ) { |
| 152 | $source = "$type-" . reset( $segments ); |
| 153 | } |
| 154 | |
| 155 | break 2; |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | if ( ! $source ) { |
| 161 | $source = 'log'; |
| 162 | } |
| 163 | |
| 164 | return sanitize_title( $source ); |
| 165 | } |
| 166 | |
| 167 | /** |
| 168 | * Delete all logs from a specific source. |
| 169 | * |
| 170 | * @param string $source The source of the log entries. |
| 171 | * @param bool $quiet Whether to suppress the deletion message. |
| 172 | * |
| 173 | * @return int The number of files that were deleted. |
| 174 | */ |
| 175 | public function clear( string $source, bool $quiet = false ): int { |
| 176 | $source = File::sanitize_source( $source ); |
| 177 | |
| 178 | $files = $this->file_controller->get_files( |
| 179 | array( |
| 180 | 'source' => $source, |
| 181 | ) |
| 182 | ); |
| 183 | |
| 184 | if ( is_wp_error( $files ) || count( $files ) < 1 ) { |
| 185 | return 0; |
| 186 | } |
| 187 | |
| 188 | $file_ids = array_map( |
| 189 | fn( $file ) => $file->get_file_id(), |
| 190 | $files |
| 191 | ); |
| 192 | |
| 193 | $deleted = $this->file_controller->delete_files( $file_ids ); |
| 194 | |
| 195 | if ( $deleted > 0 && ! $quiet ) { |
| 196 | $this->handle( |
| 197 | time(), |
| 198 | 'info', |
| 199 | sprintf( |
| 200 | esc_html( |
| 201 | // translators: %1$s is a number of log files, %2$s is a slug-style name for a file. |
| 202 | _n( |
| 203 | '%1$s log file from source %2$s was deleted.', |
| 204 | '%1$s log files from source %2$s were deleted.', |
| 205 | $deleted, |
| 206 | 'woocommerce' |
| 207 | ) |
| 208 | ), |
| 209 | number_format_i18n( $deleted ), |
| 210 | sprintf( |
| 211 | '<code>%s</code>', |
| 212 | esc_html( $source ) |
| 213 | ) |
| 214 | ), |
| 215 | array( |
| 216 | 'source' => 'wc_logger', |
| 217 | 'backtrace' => true, |
| 218 | ) |
| 219 | ); |
| 220 | } |
| 221 | |
| 222 | return $deleted; |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Delete all logs older than a specified timestamp. |
| 227 | * |
| 228 | * @param int $timestamp All files created before this timestamp will be deleted. |
| 229 | * |
| 230 | * @return int The number of files that were deleted. |
| 231 | */ |
| 232 | public function delete_logs_before_timestamp( int $timestamp = 0 ): int { |
| 233 | if ( ! $timestamp ) { |
| 234 | return 0; |
| 235 | } |
| 236 | |
| 237 | $files = $this->file_controller->get_files( |
| 238 | array( |
| 239 | 'date_filter' => 'created', |
| 240 | 'date_start' => 1, |
| 241 | 'date_end' => $timestamp, |
| 242 | ) |
| 243 | ); |
| 244 | |
| 245 | if ( is_wp_error( $files ) ) { |
| 246 | return 0; |
| 247 | } |
| 248 | |
| 249 | $files = array_filter( |
| 250 | $files, |
| 251 | function ( $file ) use ( $timestamp ) { |
| 252 | /** |
| 253 | * Allows preventing an expired log file from being deleted. |
| 254 | * |
| 255 | * @param bool $delete True to delete the file. |
| 256 | * @param File $file The log file object. |
| 257 | * @param int $timestamp The expiration threshold. |
| 258 | * |
| 259 | * @since 8.7.0 |
| 260 | */ |
| 261 | $delete = apply_filters( 'woocommerce_logger_delete_expired_file', true, $file, $timestamp ); |
| 262 | |
| 263 | return boolval( $delete ); |
| 264 | } |
| 265 | ); |
| 266 | |
| 267 | if ( count( $files ) < 1 ) { |
| 268 | return 0; |
| 269 | } |
| 270 | |
| 271 | $file_ids = array_map( |
| 272 | fn( $file ) => $file->get_file_id(), |
| 273 | $files |
| 274 | ); |
| 275 | |
| 276 | $deleted = $this->file_controller->delete_files( $file_ids ); |
| 277 | $retention_days = $this->settings->get_retention_period(); |
| 278 | |
| 279 | if ( $deleted > 0 ) { |
| 280 | $this->handle( |
| 281 | time(), |
| 282 | 'info', |
| 283 | sprintf( |
| 284 | esc_html( |
| 285 | // translators: %s is a number of log files. |
| 286 | _n( |
| 287 | '%s expired log file was deleted.', |
| 288 | '%s expired log files were deleted.', |
| 289 | $deleted, |
| 290 | 'woocommerce' |
| 291 | ) |
| 292 | ), |
| 293 | number_format_i18n( $deleted ) |
| 294 | ), |
| 295 | array( |
| 296 | 'source' => 'wc_logger', |
| 297 | ) |
| 298 | ); |
| 299 | } |
| 300 | |
| 301 | return $deleted; |
| 302 | } |
| 303 | } |
| 304 |