MediaWiki master
HeaderCallback.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Request;
4
8use RuntimeException;
9
15 private static $headersSentException;
17 private static $messageSent = false;
18
26 public static function register() {
27 // T261260 load the WebRequest class, which will be needed in callback().
28 // Autoloading seems unreliable in header callbacks, and in the case of a web
29 // request (ie. in all cases where the request might be performance-sensitive)
30 // it will have to be loaded at some point anyway.
31 // This can be removed once we require PHP 8.0+.
32 class_exists( WebRequest::class );
33 class_exists( Telemetry::class );
34
35 header_register_callback( [ __CLASS__, 'callback' ] );
36 }
37
43 public static function callback() {
44 // Prevent caching of responses with cookies (T127993)
45 $headers = [];
46 foreach ( headers_list() as $header ) {
47 $header = explode( ':', $header, 2 );
48
49 // Note: The code below (currently) does not care about value-less headers
50 if ( isset( $header[1] ) ) {
51 $headers[ strtolower( trim( $header[0] ) ) ][] = trim( $header[1] );
52 }
53 }
54
55 if ( isset( $headers['set-cookie'] ) ) {
56 $cacheControl = isset( $headers['cache-control'] )
57 ? implode( ', ', $headers['cache-control'] )
58 : '';
59
60 if ( !preg_match( '/(?:^|,)\s*(?:private|no-cache|no-store)\s*(?:$|,)/i',
61 $cacheControl )
62 ) {
63 header( 'Expires: Thu, 01 Jan 1970 00:00:00 GMT' );
64 header( 'Cache-Control: private, max-age=0, s-maxage=0' );
65 LoggerFactory::getInstance( 'cache-cookies' )->warning(
66 'Cookies set on {url} with Cache-Control "{cache-control}"', [
68 'set-cookie' => self::sanitizeSetCookie( $headers['set-cookie'] ),
69 'cache-control' => $cacheControl ?: '<not set>',
70 ]
71 );
72 }
73 }
74
75 $telemetryHeaders = Telemetry::getInstance()->getRequestHeaders();
76 // Set the request ID/trace prams on the response, so edge infrastructure can log it.
77 // FIXME this is not an ideal place to do it, but the most reliable for now.
78 foreach ( $telemetryHeaders as $header => $value ) {
79 if ( !isset( $headers[strtolower( $header )] ) ) {
80 header( "$header: $value" );
81 }
82 }
83
84 // Save a backtrace for logging in case it turns out that headers were sent prematurely
85 self::$headersSentException = new RuntimeException( 'Headers already sent from this point' );
86 }
87
94 public static function warnIfHeadersSent() {
95 if ( !self::$messageSent && headers_sent( $filename, $line ) ) {
96 self::$messageSent = true;
97 MWDebug::warning( 'Headers already sent, should send headers earlier than ' .
98 wfGetCaller( 3 ) );
99 $logger = LoggerFactory::getInstance( 'headers-sent' );
100 $logger->error( 'Warning: headers were already sent (output started at ' . $filename . ':' . $line . ')', [
101 'exception' => self::$headersSentException,
102 'detection-trace' => new RuntimeException( 'Detected here' ),
103 ] );
104 }
105 }
106
112 public static function sanitizeSetCookie( array $values ) {
113 $sanitizedValues = [];
114 foreach ( $values as $value ) {
115 // Set-Cookie header format: <cookie-name>=<cookie-value>; <non-sensitive attributes>
116 $parts = explode( ';', $value );
117 [ $name, $value ] = explode( '=', $parts[0], 2 );
118 if ( strlen( $value ) > 8 ) {
119 $value = substr( $value, 0, 8 ) . '...';
120 $parts[0] = "$name=$value";
121 }
122 $sanitizedValues[] = implode( ';', $parts );
123 }
124 return implode( "\n", $sanitizedValues );
125 }
126}
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
Debug toolbar.
Definition MWDebug.php:49
Service for handling telemetry data.
Definition Telemetry.php:29
Create PSR-3 logger objects.
static warnIfHeadersSent()
Log a warning message if headers have already been sent.
static callback()
The callback, which is called by the transport.
static sanitizeSetCookie(array $values)
Sanitize Set-Cookie headers for logging.
static getGlobalRequestURL()
Return the path and query string portion of the main request URI.