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