PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 2.3.3
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v2.3.3
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / wp-media / mcp-oauth / inc / Logging / McpLogger.php

McpLogger.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 2.3.3, at vendor/wp-media/mcp-oauth/inc/Logging/McpLogger.php

184 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP structured logger.
4 *
5 * Single choke-point for all [MCP] error_log calls so that:
6 * - every log line carries a consistent prefix for grepping
7 * - sensitive values (tokens, passwords) are never written
8 * - callers pass structured context arrays rather than building strings
9 *
10 * Usage:
11 * McpLogger::log( 'TOKEN', 'grant received', [ 'grant_type' => $gt ] );
12 *
13 * Grep all MCP logs:
14 * grep '\[MCP\]' /path/to/debug.log
15 */
16
17 declare(strict_types=1);
18
19 namespace WPMedia\MCP\OAuth\Logging;
20
21 /**
22 * McpLogger.
23 */
24 class McpLogger {
25
26 /**
27 * Write a structured [MCP] log entry.
28 *
29 * The entire logger is gated on WP_DEBUG_LOG AND WP_DEBUG both being true
30 * (see is_debug_enabled()): when either is not truthy, nothing is written
31 * at all.
32 *
33 * @param string $scope Short uppercase scope tag, e.g. 'TOKEN', 'VALIDATOR'.
34 * @param string $message Human-readable description.
35 * @param array<string, mixed> $context Key-value pairs serialised as JSON.
36 * @return void
37 */
38 public static function log( string $scope, string $message, array $context = [] ): void {
39 if ( ! self::is_debug_enabled() ) {
40 return;
41 }
42
43 $line = sprintf(
44 '[MCP][%s] %s %s',
45 strtoupper( $scope ),
46 $message,
47 empty( $context ) ? '' : wp_json_encode( $context )
48 );
49 error_log( trim( $line ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
50 }
51
52 /**
53 * Whether MCP debug logging is enabled.
54 *
55 * True when WP_DEBUG_LOG is truthy (bool true, or a string custom log file
56 * path per WP 5.1+) AND WP_DEBUG is also true. WordPress core only
57 * redirects PHP's error_log() output to wp-content/debug.log when
58 * WP_DEBUG is true, regardless of WP_DEBUG_LOG; requiring both here avoids
59 * a configuration where this gate passes but the output doesn't land
60 * where an operator would expect it (wp-content/debug.log).
61 *
62 * @return bool
63 */
64 private static function is_debug_enabled(): bool {
65 return defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG;
66 }
67
68 /**
69 * Capture and sanitize all HTTP request headers for logging.
70 *
71 * - Authorization values are truncated: first 12 chars + '...' + last 6 chars.
72 * - Cookie header is replaced with '<redacted>'.
73 * - All other values are preserved as-is.
74 *
75 * @return array<string, string> Sanitized header map (lowercase names).
76 */
77 public static function safe_request_headers(): array {
78 $headers = [];
79
80 if ( function_exists( 'getallheaders' ) ) {
81 // getallheaders() always returns an array on PHP 7.3+ once it exists.
82 foreach ( getallheaders() as $name => $value ) {
83 $key = strtolower( (string) $name );
84 $headers[ $key ] = self::sanitize_header( $key, (string) $value );
85 }
86 }
87
88 return $headers;
89 }
90
91 /**
92 * Capture the raw request body up to a safe size limit.
93 *
94 * JSON bodies are decoded and re-encoded for normalisation; form bodies
95 * are returned as-is. Authorization/password fields inside the body are
96 * redacted.
97 *
98 * @param int $max_bytes Maximum bytes to read from php://input (default 8 KB).
99 * @return string Sanitised body string suitable for logging.
100 */
101 public static function safe_request_body( int $max_bytes = 8192 ): string {
102 $raw = (string) file_get_contents( 'php://input' );
103
104 if ( '' === $raw ) {
105 return '(empty)';
106 }
107
108 $raw = substr( $raw, 0, $max_bytes );
109
110 $decoded = json_decode( $raw, true );
111 if ( is_array( $decoded ) ) {
112 $decoded = self::redact_sensitive_fields( $decoded );
113 return (string) wp_json_encode( $decoded );
114 }
115
116 // Form-encoded: redact known sensitive keys.
117 parse_str( $raw, $form );
118 $form = self::redact_sensitive_fields( $form );
119 return (string) http_build_query( $form );
120 }
121
122 /**
123 * Sanitize a single header value.
124 *
125 * @param string $name Lowercase header name.
126 * @param string $value Header value.
127 * @return string Safe value for log output.
128 */
129 private static function sanitize_header( string $name, string $value ): string {
130 if ( 'cookie' === $name ) {
131 return '<redacted>';
132 }
133
134 if ( 'authorization' === $name ) {
135 // Preserve scheme prefix (e.g. "Bearer ") and truncate the credential.
136 $space = strpos( $value, ' ' );
137 if ( false !== $space ) {
138 $scheme = substr( $value, 0, $space + 1 );
139 $credential = substr( $value, $space + 1 );
140 return $scheme . self::truncate_credential( $credential );
141 }
142 return self::truncate_credential( $value );
143 }
144
145 return $value;
146 }
147
148 /**
149 * Truncate a credential string for safe logging.
150 *
151 * Keeps first 12 chars + '...' + last 6 chars.
152 *
153 * @param string $credential Raw credential value.
154 * @return string Truncated representation.
155 */
156 private static function truncate_credential( string $credential ): string {
157 $len = strlen( $credential );
158 if ( $len <= 20 ) {
159 return str_repeat( '*', $len );
160 }
161 return substr( $credential, 0, 12 ) . '...' . substr( $credential, -6 );
162 }
163
164 /**
165 * Recursively redact known sensitive field names from an array.
166 *
167 * @param array<string, mixed> $data Input data.
168 * @return array<string, mixed> Redacted copy.
169 */
170 private static function redact_sensitive_fields( array $data ): array {
171 $sensitive = [ 'code', 'code_verifier', 'client_secret', 'access_token', 'refresh_token', 'password' ];
172
173 foreach ( $data as $key => $value ) {
174 if ( in_array( strtolower( (string) $key ), $sensitive, true ) ) {
175 $data[ $key ] = '<redacted>';
176 } elseif ( is_array( $value ) ) {
177 $data[ $key ] = self::redact_sensitive_fields( $value );
178 }
179 }
180
181 return $data;
182 }
183 }
184