MediaWiki  master
WebResponse.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Request;
24 
25 use HttpStatus;
29 use RuntimeException;
30 
36 class WebResponse {
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 
285 class_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.
Definition: HttpStatus.php:96
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
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 ...
Definition: WebResponse.php:36
static disableForPostSend()
Disable setters for post-send processing.
Definition: WebResponse.php:55
statusHeader( $code)
Output an HTTP status code header.
hasCookies()
Checks whether this request is performing cookie operations.
getHeader( $key)
Get a response header.
Definition: WebResponse.php:90
setCookie( $name, $value, $expire=0, $options=[])
Set the browser cookie.
headersSent()
Test if headers have been sent.
static array $setCookies
Used to record set cookies, because PHP's setcookie() will happily send an identical Set-Cookie to th...
Definition: WebResponse.php:41
static bool $disableForPostSend
Used to disable setters before running jobs post-request (T191537)
Definition: WebResponse.php:44
header( $string, $replace=true, $http_response_code=null)
Output an HTTP header, wrapper for PHP's header()
Definition: WebResponse.php:65
clearCookie( $name, $options=[])
Unset a browser cookie.
$header