MediaWiki  master
WebResponse.php
Go to the documentation of this file.
1 <?php
24 
30 class WebResponse {
31 
35  protected static $setCookies = [];
36 
38  protected static $disableForPostSend = false;
39 
49  public static function disableForPostSend() {
50  self::$disableForPostSend = true;
51  }
52 
59  public function header( $string, $replace = true, $http_response_code = null ) {
60  if ( self::$disableForPostSend ) {
61  wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
62  'header' => $string,
63  'replace' => $replace,
64  'http_response_code' => $http_response_code,
65  'exception' => new RuntimeException( 'Ignored post-send header' ),
66  ] );
67  return;
68  }
69 
71  if ( $http_response_code ) {
72  header( $string, $replace, $http_response_code );
73  } else {
74  header( $string, $replace );
75  }
76  }
77 
84  public function getHeader( $key ) {
85  foreach ( headers_list() as $header ) {
86  list( $name, $val ) = explode( ':', $header, 2 );
87  if ( !strcasecmp( $name, $key ) ) {
88  return trim( $val );
89  }
90  }
91  return null;
92  }
93 
99  public function statusHeader( $code ) {
100  if ( self::$disableForPostSend ) {
101  wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
102  'code' => $code,
103  'exception' => new RuntimeException( 'Ignored post-send status header' ),
104  ] );
105  return;
106  }
107 
108  HttpStatus::header( $code );
109  }
110 
116  public function headersSent() {
117  return headers_sent();
118  }
119 
141  public function setCookie( $name, $value, $expire = 0, $options = [] ) {
145 
146  $options = array_filter( $options, function ( $a ) {
147  return $a !== null;
148  } ) + [
149  'prefix' => $wgCookiePrefix,
150  'domain' => $wgCookieDomain,
151  'path' => $wgCookiePath,
152  'secure' => $wgCookieSecure,
153  'httpOnly' => $wgCookieHttpOnly,
154  'raw' => false,
155  'sameSite' => '',
156  'sameSiteLegacy' => $wgUseSameSiteLegacyCookies
157  ];
158 
159  if ( strcasecmp( $options['sameSite'], 'none' ) === 0
160  && !empty( $options['sameSiteLegacy'] )
161  ) {
162  $legacyOptions = $options;
163  $legacyOptions['sameSiteLegacy'] = false;
164  $legacyOptions['sameSite'] = '';
165  $this->setCookie( "ss0-$name", $value, $expire, $legacyOptions );
166  }
167 
168  if ( $expire === null ) {
169  $expire = 0; // Session cookie
170  } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) {
171  $expire = time() + $wgCookieExpiration;
172  }
173 
174  if ( self::$disableForPostSend ) {
175  $prefixedName = $options['prefix'] . $name;
176  wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
177  'cookie' => $prefixedName,
178  'data' => [
179  'name' => $prefixedName,
180  'value' => (string)$value,
181  'expire' => (int)$expire,
182  'path' => (string)$options['path'],
183  'domain' => (string)$options['domain'],
184  'secure' => (bool)$options['secure'],
185  'httpOnly' => (bool)$options['httpOnly'],
186  'sameSite' => (string)$options['sameSite']
187  ],
188  'exception' => new RuntimeException( 'Ignored post-send cookie' ),
189  ] );
190  return;
191  }
192 
193  if ( !Hooks::runner()->onWebResponseSetCookie( $name, $value, $expire, $options ) ) {
194  return;
195  }
196 
197  // Note: Don't try to move this earlier to reuse it for self::$disableForPostSend,
198  // we need to use the altered values from the hook here. (T198525)
199  $prefixedName = $options['prefix'] . $name;
200  $value = (string)$value;
201  $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
202  $setOptions = [
203  'expires' => (int)$expire,
204  'path' => (string)$options['path'],
205  'domain' => (string)$options['domain'],
206  'secure' => (bool)$options['secure'],
207  'httponly' => (bool)$options['httpOnly'],
208  'samesite' => (string)$options['sameSite'],
209  ];
210 
211  // Per RFC 6265, key is name + domain + path
212  $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}";
213 
214  // If this cookie name was in the request, fake an entry in
215  // self::$setCookies for it so the deleting check works right.
216  if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) {
217  self::$setCookies[$key] = [];
218  }
219 
220  // PHP deletes if value is the empty string; also, a past expiry is deleting
221  $deleting = ( $value === '' || $setOptions['expires'] > 0 && $setOptions['expires'] <= time() );
222 
223  $logDesc = "$func: \"$prefixedName\", \"$value\", \"" .
224  implode( '", "', $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  if ( $func === 'setrawcookie' ) {
239  SetCookieCompat::setrawcookie( $prefixedName, $value, $setOptions );
240  } else {
241  SetCookieCompat::setcookie( $prefixedName, $value, $setOptions );
242  }
243  self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication;
244  }
245 
255  public function clearCookie( $name, $options = [] ) {
256  $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
257  }
258 
265  public function hasCookies() {
266  return (bool)self::$setCookies;
267  }
268 }
WebResponse\$setCookies
static array $setCookies
Used to record set cookies, because PHP's setcookie() will happily send an identical Set-Cookie to th...
Definition: WebResponse.php:35
$wgCookiePath
$wgCookiePath
Set this variable if you want to restrict cookies to a certain path within the domain specified by $w...
Definition: DefaultSettings.php:6456
WebResponse\getHeader
getHeader( $key)
Get a response header.
Definition: WebResponse.php:84
Wikimedia\Http\SetCookieCompat
Definition: SetCookieCompat.php:9
WebResponse\$disableForPostSend
static bool $disableForPostSend
Used to disable setters before running jobs post-request (T191537)
Definition: WebResponse.php:38
WebResponse\header
header( $string, $replace=true, $http_response_code=null)
Output an HTTP header, wrapper for PHP's header()
Definition: WebResponse.php:59
WebResponse\setCookie
setCookie( $name, $value, $expire=0, $options=[])
Set the browser cookie.
Definition: WebResponse.php:141
$wgUseSameSiteLegacyCookies
bool $wgUseSameSiteLegacyCookies
If true, when a cross-site cookie with SameSite=None is sent, a legacy cookie with an "ss0" prefix wi...
Definition: DefaultSettings.php:6512
WebResponse\hasCookies
hasCookies()
Checks whether this request is performing cookie operations.
Definition: WebResponse.php:265
WebResponse\statusHeader
statusHeader( $code)
Output an HTTP status code header.
Definition: WebResponse.php:99
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:988
WebResponse\clearCookie
clearCookie( $name, $options=[])
Unset a browser cookie.
Definition: WebResponse.php:255
WebResponse\headersSent
headersSent()
Test if headers have been sent.
Definition: WebResponse.php:116
$wgCookieHttpOnly
$wgCookieHttpOnly
Set authentication cookies to HttpOnly to prevent access by JavaScript, in browsers that support this...
Definition: DefaultSettings.php:6490
WebResponse\disableForPostSend
static disableForPostSend()
Disable setters for post-send processing.
Definition: WebResponse.php:49
$wgCookieExpiration
$wgCookieExpiration
Default cookie lifetime, in seconds.
Definition: DefaultSettings.php:6436
$header
$header
Definition: updateCredits.php:41
$wgCookieDomain
$wgCookieDomain
Set to set an explicit domain on the login cookies eg, "justthis.domain.org" or "....
Definition: DefaultSettings.php:6450
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
HttpStatus\header
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
$wgCookieSecure
$wgCookieSecure
Whether the "secure" flag should be set on the cookie.
Definition: DefaultSettings.php:6468
WebResponse
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
Definition: WebResponse.php:30
$wgCookiePrefix
$wgCookiePrefix
Cookies generated by MediaWiki have names starting with this prefix.
Definition: DefaultSettings.php:6483
MediaWiki\HeaderCallback\warnIfHeadersSent
static warnIfHeadersSent()
Log a warning message if headers have already been sent.
Definition: HeaderCallback.php:76