Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
19.15% |
9 / 47 |
|
25.00% |
1 / 4 |
CRAP | |
0.00% |
0 / 1 |
HeaderCallback | |
19.57% |
9 / 46 |
|
25.00% |
1 / 4 |
149.22 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
callback | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
90 | |||
warnIfHeadersSent | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
sanitizeSetCookie | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Request; |
4 | |
5 | use MediaWiki\Http\Telemetry; |
6 | use RuntimeException; |
7 | |
8 | /** |
9 | * @since 1.29 |
10 | */ |
11 | class HeaderCallback { |
12 | private static $headersSentException; |
13 | private static $messageSent = false; |
14 | |
15 | /** |
16 | * Register a callback to be called when headers are sent. There can only |
17 | * be one of these handlers active, so all relevant actions have to be in |
18 | * here. |
19 | * |
20 | * @since 1.29 |
21 | */ |
22 | public static function register() { |
23 | // T261260 load the WebRequest class, which will be needed in callback(). |
24 | // Autoloading seems unreliable in header callbacks, and in the case of a web |
25 | // request (ie. in all cases where the request might be performance-sensitive) |
26 | // it will have to be loaded at some point anyway. |
27 | // This can be removed once we require PHP 8.0+. |
28 | class_exists( WebRequest::class ); |
29 | class_exists( Telemetry::class ); |
30 | |
31 | header_register_callback( [ __CLASS__, 'callback' ] ); |
32 | } |
33 | |
34 | /** |
35 | * The callback, which is called by the transport |
36 | * |
37 | * @since 1.29 |
38 | */ |
39 | public static function callback() { |
40 | // Prevent caching of responses with cookies (T127993) |
41 | $headers = []; |
42 | foreach ( headers_list() as $header ) { |
43 | $header = explode( ':', $header, 2 ); |
44 | |
45 | // Note: The code below (currently) does not care about value-less headers |
46 | if ( isset( $header[1] ) ) { |
47 | $headers[ strtolower( trim( $header[0] ) ) ][] = trim( $header[1] ); |
48 | } |
49 | } |
50 | |
51 | if ( isset( $headers['set-cookie'] ) ) { |
52 | $cacheControl = isset( $headers['cache-control'] ) |
53 | ? implode( ', ', $headers['cache-control'] ) |
54 | : ''; |
55 | |
56 | if ( !preg_match( '/(?:^|,)\s*(?:private|no-cache|no-store)\s*(?:$|,)/i', |
57 | $cacheControl ) |
58 | ) { |
59 | header( 'Expires: Thu, 01 Jan 1970 00:00:00 GMT' ); |
60 | header( 'Cache-Control: private, max-age=0, s-maxage=0' ); |
61 | \MediaWiki\Logger\LoggerFactory::getInstance( 'cache-cookies' )->warning( |
62 | 'Cookies set on {url} with Cache-Control "{cache-control}"', [ |
63 | 'url' => WebRequest::getGlobalRequestURL(), |
64 | 'set-cookie' => self::sanitizeSetCookie( $headers['set-cookie'] ), |
65 | 'cache-control' => $cacheControl ?: '<not set>', |
66 | ] |
67 | ); |
68 | } |
69 | } |
70 | |
71 | $telemetryHeaders = Telemetry::getInstance()->getRequestHeaders(); |
72 | // Set the request ID/trace prams on the response, so edge infrastructure can log it. |
73 | // FIXME this is not an ideal place to do it, but the most reliable for now. |
74 | foreach ( $telemetryHeaders as $header => $value ) { |
75 | if ( !isset( $headers[strtolower( $header )] ) ) { |
76 | header( "$header: $value" ); |
77 | } |
78 | } |
79 | |
80 | // Save a backtrace for logging in case it turns out that headers were sent prematurely |
81 | self::$headersSentException = new RuntimeException( 'Headers already sent from this point' ); |
82 | } |
83 | |
84 | /** |
85 | * Log a warning message if headers have already been sent. This can be |
86 | * called before flushing the output. |
87 | * |
88 | * @since 1.29 |
89 | */ |
90 | public static function warnIfHeadersSent() { |
91 | if ( headers_sent() && !self::$messageSent ) { |
92 | self::$messageSent = true; |
93 | \MWDebug::warning( 'Headers already sent, should send headers earlier than ' . |
94 | wfGetCaller( 3 ) ); |
95 | $logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'headers-sent' ); |
96 | $logger->error( 'Warning: headers were already sent from the location below', [ |
97 | 'exception' => self::$headersSentException, |
98 | 'detection-trace' => new RuntimeException( 'Detected here' ), |
99 | ] ); |
100 | } |
101 | } |
102 | |
103 | /** |
104 | * Sanitize Set-Cookie headers for logging. |
105 | * @param array $values List of header values. |
106 | * @return string |
107 | */ |
108 | public static function sanitizeSetCookie( array $values ) { |
109 | $sanitizedValues = []; |
110 | foreach ( $values as $value ) { |
111 | // Set-Cookie header format: <cookie-name>=<cookie-value>; <non-sensitive attributes> |
112 | $parts = explode( ';', $value ); |
113 | [ $name, $value ] = explode( '=', $parts[0], 2 ); |
114 | if ( strlen( $value ) > 8 ) { |
115 | $value = substr( $value, 0, 8 ) . '...'; |
116 | $parts[0] = "$name=$value"; |
117 | } |
118 | $sanitizedValues[] = implode( ';', $parts ); |
119 | } |
120 | return implode( "\n", $sanitizedValues ); |
121 | } |
122 | } |
123 | |
124 | /** @deprecated class alias since 1.40 */ |
125 | class_alias( HeaderCallback::class, 'MediaWiki\\HeaderCallback' ); |