MediaWiki master
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 $disableForPostSend = false;
45
55 public function disableForPostSend() {
56 $this->disableForPostSend = true;
57 }
58
65 public function header( $string, $replace = true, $http_response_code = null ) {
66 if ( $this->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
89 public function getStatusCode() {
90 return http_response_code();
91 }
92
99 public function getHeader( $key ) {
100 foreach ( headers_list() as $header ) {
101 [ $name, $val ] = explode( ':', $header, 2 );
102 if ( !strcasecmp( $name, $key ) ) {
103 return trim( $val );
104 }
105 }
106 return null;
107 }
108
114 public function statusHeader( $code ) {
115 if ( $this->disableForPostSend ) {
116 wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
117 'code' => $code,
118 'exception' => new RuntimeException( 'Ignored post-send status header' ),
119 ] );
120 return;
121 }
122
123 HttpStatus::header( $code );
124 }
125
131 public function headersSent() {
132 return headers_sent();
133 }
134
155 public function setCookie( $name, $value, $expire = 0, $options = [] ) {
156 $services = MediaWikiServices::getInstance();
157 $mainConfig = $services->getMainConfig();
158 $cookiePath = $mainConfig->get( MainConfigNames::CookiePath );
159 $cookiePrefix = $mainConfig->get( MainConfigNames::CookiePrefix );
160 $cookieDomain = $mainConfig->get( MainConfigNames::CookieDomain );
161 $cookieSecure = $mainConfig->get( MainConfigNames::CookieSecure );
162 $cookieExpiration = $mainConfig->get( MainConfigNames::CookieExpiration );
163 $cookieHttpOnly = $mainConfig->get( MainConfigNames::CookieHttpOnly );
164 $options = array_filter( $options, static function ( $a ) {
165 return $a !== null;
166 } ) + [
167 'prefix' => $cookiePrefix,
168 'domain' => $cookieDomain,
169 'path' => $cookiePath,
170 'secure' => $cookieSecure,
171 'httpOnly' => $cookieHttpOnly,
172 'raw' => false,
173 'sameSite' => '',
174 ];
175
176 if ( $expire === null ) {
177 $expire = 0; // Session cookie
178 } elseif ( $expire == 0 && $cookieExpiration != 0 ) {
179 $expire = time() + $cookieExpiration;
180 }
181
182 if ( $this->disableForPostSend ) {
183 $prefixedName = $options['prefix'] . $name;
184 wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
185 'cookie' => $prefixedName,
186 'data' => [
187 'name' => $prefixedName,
188 'value' => (string)$value,
189 'expire' => (int)$expire,
190 'path' => (string)$options['path'],
191 'domain' => (string)$options['domain'],
192 'secure' => (bool)$options['secure'],
193 'httpOnly' => (bool)$options['httpOnly'],
194 'sameSite' => (string)$options['sameSite']
195 ],
196 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
197 ] );
198 return;
199 }
200
201 $hookRunner = new HookRunner( $services->getHookContainer() );
202 if ( !$hookRunner->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
203 return;
204 }
205
206 // Note: Don't try to move this earlier to reuse it for $this->disableForPostSend,
207 // we need to use the altered values from the hook here. (T198525)
208 $prefixedName = $options['prefix'] . $name;
209 $value = (string)$value;
210 $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
211 $setOptions = [
212 'expires' => (int)$expire,
213 'path' => (string)$options['path'],
214 'domain' => (string)$options['domain'],
215 'secure' => (bool)$options['secure'],
216 'httponly' => (bool)$options['httpOnly'],
217 'samesite' => (string)$options['sameSite'],
218 ];
219
220 // Per RFC 6265, key is name + domain + path
221 $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
222
223 // If this cookie name was in the request, fake an entry in
224 // self::$setCookies for it so the deleting check works right.
225 if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
226 self::$setCookies[$key] = [];
227 }
228
229 // PHP deletes if value is the empty string; also, a past expiry is deleting
230 $deleting = ( $value === '' || ( $setOptions['expires'] > 0 && $setOptions['expires'] <= time() ) );
231
232 $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
233 implode( '", "', array_map( 'strval', $setOptions ) ) . '"';
234 $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ];
235
236 if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
237 wfDebugLog( 'cookie', "already deleted $logDesc" );
238 return;
239 } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
240 self::$setCookies[$key] === $optionsForDeduplication
241 ) {
242 wfDebugLog( 'cookie', "already set $logDesc" );
243 return;
244 }
245
246 wfDebugLog( 'cookie', $logDesc );
247 if ( $func === 'setrawcookie' ) {
248 setrawcookie( $prefixedName, $value, $setOptions );
249 } else {
250 setcookie( $prefixedName, $value, $setOptions );
251 }
252 self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
253 }
254
264 public function clearCookie( $name, $options = [] ) {
265 $this->setCookie( $name, '', time() - 31_536_000 /* 1 year */, $options );
266 }
267
274 public function hasCookies() {
275 return (bool)self::$setCookies;
276 }
277}
278
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.
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 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.
disableForPostSend()
Disable setters for post-send processing.
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.
bool $disableForPostSend
Used to disable setters before running jobs post-request (T191537)
$header