MediaWiki REL1_40
WebResponse.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Request;
24
25use Hooks;
26use 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 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
149 $cookiePath = $mainConfig->get( MainConfigNames::CookiePath );
150 $cookiePrefix = $mainConfig->get( MainConfigNames::CookiePrefix );
151 $cookieDomain = $mainConfig->get( MainConfigNames::CookieDomain );
152 $cookieSecure = $mainConfig->get( MainConfigNames::CookieSecure );
153 $cookieExpiration = $mainConfig->get( MainConfigNames::CookieExpiration );
154 $cookieHttpOnly = $mainConfig->get( MainConfigNames::CookieHttpOnly );
155 $useSameSiteLegacyCookies = $mainConfig->get( MainConfigNames::UseSameSiteLegacyCookies );
156 $options = array_filter( $options, static function ( $a ) {
157 return $a !== null;
158 } ) + [
159 'prefix' => $cookiePrefix,
160 'domain' => $cookieDomain,
161 'path' => $cookiePath,
162 'secure' => $cookieSecure,
163 'httpOnly' => $cookieHttpOnly,
164 'raw' => false,
165 'sameSite' => '',
166 'sameSiteLegacy' => $useSameSiteLegacyCookies
167 ];
168
169 if ( strcasecmp( $options['sameSite'], 'none' ) === 0
170 && !empty( $options['sameSiteLegacy'] )
171 ) {
172 $legacyOptions = $options;
173 $legacyOptions['sameSiteLegacy'] = false;
174 $legacyOptions['sameSite'] = '';
175 $this->setCookie( "ss0-$name", $value, $expire, $legacyOptions );
176 }
177
178 if ( $expire === null ) {
179 $expire = 0; // Session cookie
180 } elseif ( $expire == 0 && $cookieExpiration != 0 ) {
181 $expire = time() + $cookieExpiration;
182 }
183
184 if ( self::$disableForPostSend ) {
185 $prefixedName = $options['prefix'] . $name;
186 wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
187 'cookie' => $prefixedName,
188 'data' => [
189 'name' => $prefixedName,
190 'value' => (string)$value,
191 'expire' => (int)$expire,
192 'path' => (string)$options['path'],
193 'domain' => (string)$options['domain'],
194 'secure' => (bool)$options['secure'],
195 'httpOnly' => (bool)$options['httpOnly'],
196 'sameSite' => (string)$options['sameSite']
197 ],
198 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
199 ] );
200 return;
201 }
202
203 if ( !Hooks::runner()->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
204 return;
205 }
206
207 // Note: Don't try to move this earlier to reuse it for self::$disableForPostSend,
208 // we need to use the altered values from the hook here. (T198525)
209 $prefixedName = $options['prefix'] . $name;
210 $value = (string)$value;
211 $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
212 $setOptions = [
213 'expires' => (int)$expire,
214 'path' => (string)$options['path'],
215 'domain' => (string)$options['domain'],
216 'secure' => (bool)$options['secure'],
217 'httponly' => (bool)$options['httpOnly'],
218 'samesite' => (string)$options['sameSite'],
219 ];
220
221 // Per RFC 6265, key is name + domain + path
222 $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
223
224 // If this cookie name was in the request, fake an entry in
225 // self::$setCookies for it so the deleting check works right.
226 if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
227 self::$setCookies[$key] = [];
228 }
229
230 // PHP deletes if value is the empty string; also, a past expiry is deleting
231 $deleting = ( $value === '' || $setOptions['expires'] > 0 && $setOptions['expires'] <= time() );
232
233 $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
234 implode( '", "', array_map( 'strval', $setOptions ) ) . '"';
235 $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ];
236
237 if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
238 wfDebugLog( 'cookie', "already deleted $logDesc" );
239 return;
240 } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
241 self::$setCookies[$key] === $optionsForDeduplication
242 ) {
243 wfDebugLog( 'cookie', "already set $logDesc" );
244 return;
245 }
246
247 wfDebugLog( 'cookie', $logDesc );
248 if ( $func === 'setrawcookie' ) {
249 setrawcookie( $prefixedName, $value, $setOptions );
250 } else {
251 setcookie( $prefixedName, $value, $setOptions );
252 }
253 self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
254 }
255
265 public function clearCookie( $name, $options = [] ) {
266 $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
267 }
268
275 public function hasCookies() {
276 return (bool)self::$setCookies;
277 }
278}
279
280class_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.
Hooks class.
Definition Hooks.php:38
static header( $code)
Output an HTTP status code header.
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