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