MediaWiki master
WebResponse.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Request;
10
11use LogicException;
16use RuntimeException;
19use Wikimedia\Timestamp\ConvertibleTimestamp;
20
27
31 protected static $setCookies = [];
32
34 protected $disableForPostSend = false;
35
45 public function disableForPostSend() {
46 $this->disableForPostSend = true;
47 }
48
55 public function header( $string, $replace = true, $http_response_code = null ) {
56 if ( $this->disableForPostSend ) {
57 LoggerFactory::getInstance( 'header' )->warning( 'ignored post-send header {header}', [
58 'header' => $string,
59 'replace' => $replace,
60 'http_response_code' => $http_response_code,
61 'exception' => new RuntimeException( 'Ignored post-send header' ),
62 ] );
63 return;
64 }
65
66 \MediaWiki\Request\HeaderCallback::warnIfHeadersSent();
67 if ( $http_response_code ) {
68 header( $string, $replace, $http_response_code );
69 } else {
70 header( $string, $replace );
71 }
72 }
73
79 public function getStatusCode() {
80 return http_response_code();
81 }
82
89 public function getHeader( $key ) {
90 foreach ( headers_list() as $header ) {
91 [ $name, $val ] = explode( ':', $header, 2 );
92 if ( !strcasecmp( $name, $key ) ) {
93 return trim( $val );
94 }
95 }
96 return null;
97 }
98
104 public function statusHeader( $code ) {
105 if ( $this->disableForPostSend ) {
106 LoggerFactory::getInstance( 'header' )->warning( 'ignored post-send status header {code}', [
107 'code' => $code,
108 'exception' => new RuntimeException( 'Ignored post-send status header' ),
109 ] );
110 return;
111 }
112
113 HttpStatus::header( $code );
114 }
115
121 public function headersSent() {
122 return headers_sent();
123 }
124
145 public function setCookie( $name, $value, $expire = 0, $options = [] ) {
146 $logger = LoggerFactory::getInstance( 'cookie' );
147 $services = MediaWikiServices::getInstance();
148 $mainConfig = $services->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 $options = array_filter( $options, static fn ( $a ) => $a !== null ) + [
156 'prefix' => $cookiePrefix,
157 'domain' => $cookieDomain,
158 'path' => $cookiePath,
159 'secure' => $cookieSecure,
160 'httpOnly' => $cookieHttpOnly,
161 'raw' => false,
162 'sameSite' => '',
163 ];
164
165 if ( $expire === null ) {
166 $expire = 0; // Session cookie
167 } elseif ( $expire == 0 && $cookieExpiration != 0 ) {
168 $expire = ConvertibleTimestamp::time() + $cookieExpiration;
169 }
170
171 if ( $this->disableForPostSend ) {
172 $prefixedName = $options['prefix'] . $name;
173 $logger->warning( 'ignored post-send cookie {cookie}', [
174 'cookie' => $prefixedName,
175 'data' => [
176 'name' => $prefixedName,
177 'value' => (string)$value,
178 'expire' => (int)$expire,
179 'path' => (string)$options['path'],
180 'domain' => (string)$options['domain'],
181 'secure' => (bool)$options['secure'],
182 'httpOnly' => (bool)$options['httpOnly'],
183 'sameSite' => (string)$options['sameSite']
184 ],
185 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
186 ] );
187 return;
188 }
189
190 $hookRunner = new HookRunner( $services->getHookContainer() );
191 if ( !$hookRunner->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
192 return;
193 }
194
195 // Note: Don't try to move this earlier to reuse it for $this->disableForPostSend,
196 // we need to use the altered values from the hook here. (T198525)
197 $prefixedName = $options['prefix'] . $name;
198 $value = (string)$value;
199 $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
200 $setOptions = [
201 'expires' => (int)$expire,
202 'path' => (string)$options['path'],
203 'domain' => (string)$options['domain'],
204 'secure' => (bool)$options['secure'],
205 'httponly' => (bool)$options['httpOnly'],
206 'samesite' => (string)$options['sameSite'],
207 ];
208
209 // Per RFC 6265, key is name + domain + path
210 $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
211
212 // If this cookie name was in the request, fake an entry in
213 // self::$setCookies for it so the deleting check works right.
214 if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
215 self::$setCookies[$key] = [];
216 }
217
218 // PHP deletes if value is the empty string; also, a past expiry is deleting
219 $deleting = ( $value === ''
220 || ( $setOptions['expires'] > 0
221 && $setOptions['expires'] <= ConvertibleTimestamp::time()
222 )
223 );
224
225 $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
226 implode( '", "', array_map( 'strval', $setOptions ) ) . '"';
227 $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ];
228
229 if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
230 $logger->debug( "already deleted $logDesc" );
231 return;
232 } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
233 self::$setCookies[$key] === $optionsForDeduplication
234 ) {
235 $logger->debug( "already set $logDesc" );
236 return;
237 }
238
239 $logger->info( $logDesc );
240 $this->actuallySetCookie( $func, $prefixedName, $value, $setOptions );
241 self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
242 }
243
253 public function clearCookie( $name, $options = [] ) {
254 $this->setCookie( $name, '', ConvertibleTimestamp::time() - ExpirationAwareness::TTL_YEAR, $options );
255 }
256
263 public function hasCookies() {
264 return (bool)self::$setCookies;
265 }
266
267 protected function actuallySetCookie( string $func, string $prefixedName, string $value, array $setOptions ): void {
268 if ( $func === 'setrawcookie' ) {
269 setrawcookie( $prefixedName, $value, $setOptions );
270 } else {
271 setcookie( $prefixedName, $value, $setOptions );
272 }
273 }
274
278 public static function resetCookieCache(): void {
279 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
280 throw new LogicException( __METHOD__ . ' should not be called outside tests' );
281 }
282 self::$setCookies = [];
283 }
284}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Create PSR-3 logger objects.
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.