MediaWiki REL1_39
WebResponse.php
Go to the documentation of this file.
1<?php
25
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\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.
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 ...
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)
getHeader( $key)
Get a response header.
statusHeader( $code)
Output an HTTP status code header.
static disableForPostSend()
Disable setters for post-send processing.
header( $string, $replace=true, $http_response_code=null)
Output an HTTP header, wrapper for PHP's header()
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...
$header