MediaWiki  master
WebResponse.php
Go to the documentation of this file.
1 <?php
25 
31 class WebResponse {
32 
36  protected static $setCookies = [];
37 
39  protected static $disableForPostSend = false;
40 
50  public static function disableForPostSend() {
51  self::$disableForPostSend = true;
52  }
53 
60  public function header( $string, $replace = true, $http_response_code = null ) {
61  if ( self::$disableForPostSend ) {
62  wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
63  'header' => $string,
64  'replace' => $replace,
65  'http_response_code' => $http_response_code,
66  'exception' => new RuntimeException( 'Ignored post-send header' ),
67  ] );
68  return;
69  }
70 
71  \MediaWiki\Request\HeaderCallback::warnIfHeadersSent();
72  if ( $http_response_code ) {
73  header( $string, $replace, $http_response_code );
74  } else {
75  header( $string, $replace );
76  }
77  }
78 
85  public function getHeader( $key ) {
86  foreach ( headers_list() as $header ) {
87  [ $name, $val ] = explode( ':', $header, 2 );
88  if ( !strcasecmp( $name, $key ) ) {
89  return trim( $val );
90  }
91  }
92  return null;
93  }
94 
100  public function statusHeader( $code ) {
101  if ( self::$disableForPostSend ) {
102  wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
103  'code' => $code,
104  'exception' => new RuntimeException( 'Ignored post-send status header' ),
105  ] );
106  return;
107  }
108 
109  HttpStatus::header( $code );
110  }
111 
117  public function headersSent() {
118  return headers_sent();
119  }
120 
142  public function setCookie( $name, $value, $expire = 0, $options = [] ) {
143  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
144  $cookiePath = $mainConfig->get( MainConfigNames::CookiePath );
145  $cookiePrefix = $mainConfig->get( MainConfigNames::CookiePrefix );
146  $cookieDomain = $mainConfig->get( MainConfigNames::CookieDomain );
147  $cookieSecure = $mainConfig->get( MainConfigNames::CookieSecure );
148  $cookieExpiration = $mainConfig->get( MainConfigNames::CookieExpiration );
149  $cookieHttpOnly = $mainConfig->get( MainConfigNames::CookieHttpOnly );
150  $useSameSiteLegacyCookies = $mainConfig->get( MainConfigNames::UseSameSiteLegacyCookies );
151  $options = array_filter( $options, static function ( $a ) {
152  return $a !== null;
153  } ) + [
154  'prefix' => $cookiePrefix,
155  'domain' => $cookieDomain,
156  'path' => $cookiePath,
157  'secure' => $cookieSecure,
158  'httpOnly' => $cookieHttpOnly,
159  'raw' => false,
160  'sameSite' => '',
161  'sameSiteLegacy' => $useSameSiteLegacyCookies
162  ];
163 
164  if ( strcasecmp( $options['sameSite'], 'none' ) === 0
165  && !empty( $options['sameSiteLegacy'] )
166  ) {
167  $legacyOptions = $options;
168  $legacyOptions['sameSiteLegacy'] = false;
169  $legacyOptions['sameSite'] = '';
170  $this->setCookie( "ss0-$name", $value, $expire, $legacyOptions );
171  }
172 
173  if ( $expire === null ) {
174  $expire = 0; // Session cookie
175  } elseif ( $expire == 0 && $cookieExpiration != 0 ) {
176  $expire = time() + $cookieExpiration;
177  }
178 
179  if ( self::$disableForPostSend ) {
180  $prefixedName = $options['prefix'] . $name;
181  wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
182  'cookie' => $prefixedName,
183  'data' => [
184  'name' => $prefixedName,
185  'value' => (string)$value,
186  'expire' => (int)$expire,
187  'path' => (string)$options['path'],
188  'domain' => (string)$options['domain'],
189  'secure' => (bool)$options['secure'],
190  'httpOnly' => (bool)$options['httpOnly'],
191  'sameSite' => (string)$options['sameSite']
192  ],
193  'exception' => new RuntimeException( 'Ignored post-send cookie' ),
194  ] );
195  return;
196  }
197 
198  if ( !Hooks::runner()->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
199  return;
200  }
201 
202  // Note: Don't try to move this earlier to reuse it for self::$disableForPostSend,
203  // we need to use the altered values from the hook here. (T198525)
204  $prefixedName = $options['prefix'] . $name;
205  $value = (string)$value;
206  $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
207  $setOptions = [
208  'expires' => (int)$expire,
209  'path' => (string)$options['path'],
210  'domain' => (string)$options['domain'],
211  'secure' => (bool)$options['secure'],
212  'httponly' => (bool)$options['httpOnly'],
213  'samesite' => (string)$options['sameSite'],
214  ];
215 
216  // Per RFC 6265, key is name + domain + path
217  $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
218 
219  // If this cookie name was in the request, fake an entry in
220  // self::$setCookies for it so the deleting check works right.
221  if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
222  self::$setCookies[$key] = [];
223  }
224 
225  // PHP deletes if value is the empty string; also, a past expiry is deleting
226  $deleting = ( $value === '' || $setOptions['expires'] > 0 && $setOptions['expires'] <= time() );
227 
228  $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
229  implode( '", "', array_map( 'strval', $setOptions ) ) . '"';
230  $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ];
231 
232  if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false
233  wfDebugLog( 'cookie', "already deleted $logDesc" );
234  return;
235  } elseif ( !$deleting && isset( self::$setCookies[$key] ) &&
236  self::$setCookies[$key] === $optionsForDeduplication
237  ) {
238  wfDebugLog( 'cookie', "already set $logDesc" );
239  return;
240  }
241 
242  wfDebugLog( 'cookie', $logDesc );
243  if ( $func === 'setrawcookie' ) {
244  setrawcookie( $prefixedName, $value, $setOptions );
245  } else {
246  setcookie( $prefixedName, $value, $setOptions );
247  }
248  self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
249  }
250 
260  public function clearCookie( $name, $options = [] ) {
261  $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
262  }
263 
270  public function hasCookies() {
271  return (bool)self::$setCookies;
272  }
273 }
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 runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
Definition: WebResponse.php:31
setCookie( $name, $value, $expire=0, $options=[])
Set the browser cookie.
hasCookies()
Checks whether this request is performing cookie operations.
static bool $disableForPostSend
Used to disable setters before running jobs post-request (T191537)
Definition: WebResponse.php:39
getHeader( $key)
Get a response header.
Definition: WebResponse.php:85
statusHeader( $code)
Output an HTTP status code header.
static disableForPostSend()
Disable setters for post-send processing.
Definition: WebResponse.php:50
header( $string, $replace=true, $http_response_code=null)
Output an HTTP header, wrapper for PHP's header()
Definition: WebResponse.php:60
clearCookie( $name, $options=[])
Unset a 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:36
$header