MediaWiki master
WebResponse.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Request;
10
11use LogicException;
15use RuntimeException;
18use Wikimedia\Timestamp\ConvertibleTimestamp;
19
26
30 protected static $setCookies = [];
31
33 protected $disableForPostSend = false;
34
44 public function disableForPostSend() {
45 $this->disableForPostSend = true;
46 }
47
54 public function header( $string, $replace = true, $http_response_code = null ) {
55 if ( $this->disableForPostSend ) {
56 wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
57 'header' => $string,
58 'replace' => $replace,
59 'http_response_code' => $http_response_code,
60 'exception' => new RuntimeException( 'Ignored post-send header' ),
61 ] );
62 return;
63 }
64
65 \MediaWiki\Request\HeaderCallback::warnIfHeadersSent();
66 if ( $http_response_code ) {
67 header( $string, $replace, $http_response_code );
68 } else {
69 header( $string, $replace );
70 }
71 }
72
78 public function getStatusCode() {
79 return http_response_code();
80 }
81
88 public function getHeader( $key ) {
89 foreach ( headers_list() as $header ) {
90 [ $name, $val ] = explode( ':', $header, 2 );
91 if ( !strcasecmp( $name, $key ) ) {
92 return trim( $val );
93 }
94 }
95 return null;
96 }
97
103 public function statusHeader( $code ) {
104 if ( $this->disableForPostSend ) {
105 wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
106 'code' => $code,
107 'exception' => new RuntimeException( 'Ignored post-send status header' ),
108 ] );
109 return;
110 }
111
112 HttpStatus::header( $code );
113 }
114
120 public function headersSent() {
121 return headers_sent();
122 }
123
144 public function setCookie( $name, $value, $expire = 0, $options = [] ) {
145 $services = MediaWikiServices::getInstance();
146 $mainConfig = $services->getMainConfig();
147 $cookiePath = $mainConfig->get( MainConfigNames::CookiePath );
148 $cookiePrefix = $mainConfig->get( MainConfigNames::CookiePrefix );
149 $cookieDomain = $mainConfig->get( MainConfigNames::CookieDomain );
150 $cookieSecure = $mainConfig->get( MainConfigNames::CookieSecure );
151 $cookieExpiration = $mainConfig->get( MainConfigNames::CookieExpiration );
152 $cookieHttpOnly = $mainConfig->get( MainConfigNames::CookieHttpOnly );
153 $options = array_filter( $options, static fn ( $a ) => $a !== null ) + [
154 'prefix' => $cookiePrefix,
155 'domain' => $cookieDomain,
156 'path' => $cookiePath,
157 'secure' => $cookieSecure,
158 'httpOnly' => $cookieHttpOnly,
159 'raw' => false,
160 'sameSite' => '',
161 ];
162
163 if ( $expire === null ) {
164 $expire = 0; // Session cookie
165 } elseif ( $expire == 0 && $cookieExpiration != 0 ) {
166 $expire = ConvertibleTimestamp::time() + $cookieExpiration;
167 }
168
169 if ( $this->disableForPostSend ) {
170 $prefixedName = $options['prefix'] . $name;
171 wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
172 'cookie' => $prefixedName,
173 'data' => [
174 'name' => $prefixedName,
175 'value' => (string)$value,
176 'expire' => (int)$expire,
177 'path' => (string)$options['path'],
178 'domain' => (string)$options['domain'],
179 'secure' => (bool)$options['secure'],
180 'httpOnly' => (bool)$options['httpOnly'],
181 'sameSite' => (string)$options['sameSite']
182 ],
183 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
184 ] );
185 return;
186 }
187
188 $hookRunner = new HookRunner( $services->getHookContainer() );
189 if ( !$hookRunner->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
190 return;
191 }
192
193 // Note: Don't try to move this earlier to reuse it for $this->disableForPostSend,
194 // we need to use the altered values from the hook here. (T198525)
195 $prefixedName = $options['prefix'] . $name;
196 $value = (string)$value;
197 $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
198 $setOptions = [
199 'expires' => (int)$expire,
200 'path' => (string)$options['path'],
201 'domain' => (string)$options['domain'],
202 'secure' => (bool)$options['secure'],
203 'httponly' => (bool)$options['httpOnly'],
204 'samesite' => (string)$options['sameSite'],
205 ];
206
207 // Per RFC 6265, key is name + domain + path
208 $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
209
210 // If this cookie name was in the request, fake an entry in
211 // self::$setCookies for it so the deleting check works right.
212 if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
213 self::$setCookies[$key] = [];
214 }
215
216 // PHP deletes if value is the empty string; also, a past expiry is deleting
217 $deleting = ( $value === ''
218 || ( $setOptions['expires'] > 0
219 && $setOptions['expires'] <= ConvertibleTimestamp::time()
220 )
221 );
222
223 $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
224 implode( '", "', array_map( 'strval', $setOptions ) ) . '"';
225 $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ];
226
227 if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
228 wfDebugLog( 'cookie', "already deleted $logDesc" );
229 return;
230 } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
231 self::$setCookies[$key] === $optionsForDeduplication
232 ) {
233 wfDebugLog( 'cookie', "already set $logDesc" );
234 return;
235 }
236
237 wfDebugLog( 'cookie', $logDesc );
238 $this->actuallySetCookie( $func, $prefixedName, $value, $setOptions );
239 self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
240 }
241
251 public function clearCookie( $name, $options = [] ) {
252 $this->setCookie( $name, '', ConvertibleTimestamp::time() - ExpirationAwareness::TTL_YEAR, $options );
253 }
254
261 public function hasCookies() {
262 return (bool)self::$setCookies;
263 }
264
265 protected function actuallySetCookie( string $func, string $prefixedName, string $value, array $setOptions ): void {
266 if ( $func === 'setrawcookie' ) {
267 setrawcookie( $prefixedName, $value, $setOptions );
268 } else {
269 setcookie( $prefixedName, $value, $setOptions );
270 }
271 }
272
276 public static function resetCookieCache(): void {
277 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
278 throw new LogicException( __METHOD__ . ' should not be called outside tests' );
279 }
280 self::$setCookies = [];
281 }
282}
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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.
actuallySetCookie(string $func, string $prefixedName, string $value, array $setOptions)
bool $disableForPostSend
Used to disable setters before running jobs post-request (T191537)
Generic interface providing Time-To-Live constants for expirable object storage.