MediaWiki 1.41.2
WebResponse.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Request;
24
25use HttpStatus;
29use RuntimeException;
30
37
41 protected static $setCookies = [];
42
44 protected static $disableForPostSend = false;
45
55 public static function disableForPostSend() {
56 self::$disableForPostSend = true;
57 }
58
65 public function header( $string, $replace = true, $http_response_code = null ) {
66 if ( self::$disableForPostSend ) {
67 wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
68 'header' => $string,
69 'replace' => $replace,
70 'http_response_code' => $http_response_code,
71 'exception' => new RuntimeException( 'Ignored post-send header' ),
72 ] );
73 return;
74 }
75
76 \MediaWiki\Request\HeaderCallback::warnIfHeadersSent();
77 if ( $http_response_code ) {
78 header( $string, $replace, $http_response_code );
79 } else {
80 header( $string, $replace );
81 }
82 }
83
90 public function getHeader( $key ) {
91 foreach ( headers_list() as $header ) {
92 [ $name, $val ] = explode( ':', $header, 2 );
93 if ( !strcasecmp( $name, $key ) ) {
94 return trim( $val );
95 }
96 }
97 return null;
98 }
99
105 public function statusHeader( $code ) {
106 if ( self::$disableForPostSend ) {
107 wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
108 'code' => $code,
109 'exception' => new RuntimeException( 'Ignored post-send status header' ),
110 ] );
111 return;
112 }
113
114 HttpStatus::header( $code );
115 }
116
122 public function headersSent() {
123 return headers_sent();
124 }
125
147 public function setCookie( $name, $value, $expire = 0, $options = [] ) {
148 $services = MediaWikiServices::getInstance();
149 $mainConfig = $services->getMainConfig();
150 $cookiePath = $mainConfig->get( MainConfigNames::CookiePath );
151 $cookiePrefix = $mainConfig->get( MainConfigNames::CookiePrefix );
152 $cookieDomain = $mainConfig->get( MainConfigNames::CookieDomain );
153 $cookieSecure = $mainConfig->get( MainConfigNames::CookieSecure );
154 $cookieExpiration = $mainConfig->get( MainConfigNames::CookieExpiration );
155 $cookieHttpOnly = $mainConfig->get( MainConfigNames::CookieHttpOnly );
156 $useSameSiteLegacyCookies = $mainConfig->get( MainConfigNames::UseSameSiteLegacyCookies );
157 $options = array_filter( $options, static function ( $a ) {
158 return $a !== null;
159 } ) + [
160 'prefix' => $cookiePrefix,
161 'domain' => $cookieDomain,
162 'path' => $cookiePath,
163 'secure' => $cookieSecure,
164 'httpOnly' => $cookieHttpOnly,
165 'raw' => false,
166 'sameSite' => '',
167 'sameSiteLegacy' => $useSameSiteLegacyCookies
168 ];
169
170 if ( strcasecmp( $options['sameSite'], 'none' ) === 0
171 && !empty( $options['sameSiteLegacy'] )
172 ) {
173 $legacyOptions = $options;
174 $legacyOptions['sameSiteLegacy'] = false;
175 $legacyOptions['sameSite'] = '';
176 $this->setCookie( "ss0-$name", $value, $expire, $legacyOptions );
177 }
178
179 if ( $expire === null ) {
180 $expire = 0; // Session cookie
181 } elseif ( $expire == 0 && $cookieExpiration != 0 ) {
182 $expire = time() + $cookieExpiration;
183 }
184
185 if ( self::$disableForPostSend ) {
186 $prefixedName = $options['prefix'] . $name;
187 wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
188 'cookie' => $prefixedName,
189 'data' => [
190 'name' => $prefixedName,
191 'value' => (string)$value,
192 'expire' => (int)$expire,
193 'path' => (string)$options['path'],
194 'domain' => (string)$options['domain'],
195 'secure' => (bool)$options['secure'],
196 'httpOnly' => (bool)$options['httpOnly'],
197 'sameSite' => (string)$options['sameSite']
198 ],
199 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
200 ] );
201 return;
202 }
203
204 $hookRunner = new HookRunner( $services->getHookContainer() );
205 if ( !$hookRunner->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
206 return;
207 }
208
209 // Note: Don't try to move this earlier to reuse it for self::$disableForPostSend,
210 // we need to use the altered values from the hook here. (T198525)
211 $prefixedName = $options['prefix'] . $name;
212 $value = (string)$value;
213 $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
214 $setOptions = [
215 'expires' => (int)$expire,
216 'path' => (string)$options['path'],
217 'domain' => (string)$options['domain'],
218 'secure' => (bool)$options['secure'],
219 'httponly' => (bool)$options['httpOnly'],
220 'samesite' => (string)$options['sameSite'],
221 ];
222
223 // Per RFC 6265, key is name + domain + path
224 $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
225
226 // If this cookie name was in the request, fake an entry in
227 // self::$setCookies for it so the deleting check works right.
228 if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
229 self::$setCookies[$key] = [];
230 }
231
232 // PHP deletes if value is the empty string; also, a past expiry is deleting
233 $deleting = ( $value === '' || $setOptions['expires'] > 0 && $setOptions['expires'] <= time() );
234
235 $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
236 implode( '", "', array_map( 'strval', $setOptions ) ) . '"';
237 $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ];
238
239 if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
240 wfDebugLog( 'cookie', "already deleted $logDesc" );
241 return;
242 } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
243 self::$setCookies[$key] === $optionsForDeduplication
244 ) {
245 wfDebugLog( 'cookie', "already set $logDesc" );
246 return;
247 }
248
249 wfDebugLog( 'cookie', $logDesc );
250 if ( $func === 'setrawcookie' ) {
251 setrawcookie( $prefixedName, $value, $setOptions );
252 } else {
253 setcookie( $prefixedName, $value, $setOptions );
254 }
255 self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
256 }
257
267 public function clearCookie( $name, $options = [] ) {
268 $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
269 }
270
277 public function hasCookies() {
278 return (bool)self::$setCookies;
279 }
280}
281
285class_alias( WebResponse::class, 'WebResponse' );
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
static header( $code)
Output an HTTP status code header.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
const CookieExpiration
Name constant for the CookieExpiration setting, for use with Config::get()
const CookieDomain
Name constant for the CookieDomain setting, for use with Config::get()
const CookiePath
Name constant for the CookiePath setting, for use with Config::get()
const UseSameSiteLegacyCookies
Name constant for the UseSameSiteLegacyCookies setting, for use with Config::get()
const CookieSecure
Name constant for the CookieSecure setting, for use with Config::get()
const CookiePrefix
Name constant for the CookiePrefix setting, for use with Config::get()
const CookieHttpOnly
Name constant for the CookieHttpOnly setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
header( $string, $replace=true, $http_response_code=null)
Output an HTTP header, wrapper for PHP's header()
headersSent()
Test if headers have been sent.
setCookie( $name, $value, $expire=0, $options=[])
Set the browser cookie.
getHeader( $key)
Get a response header.
statusHeader( $code)
Output an HTTP status code header.
hasCookies()
Checks whether this request is performing cookie operations.
static array $setCookies
Used to record set cookies, because PHP's setcookie() will happily send an identical Set-Cookie to th...
clearCookie( $name, $options=[])
Unset a browser cookie.
static bool $disableForPostSend
Used to disable setters before running jobs post-request (T191537)
static disableForPostSend()
Disable setters for post-send processing.
$header