$string, 'replace' => $replace, 'http_response_code' => $http_response_code, 'exception' => new RuntimeException( 'Ignored post-send header' ), ] ); return; } \MediaWiki\HeaderCallback::warnIfHeadersSent(); if ( $http_response_code ) { header( $string, $replace, $http_response_code ); } else { header( $string, $replace ); } } /** * Get a response header * @param string $key The name of the header to get (case insensitive). * @return string|null The header value (if set); null otherwise. * @since 1.25 */ public function getHeader( $key ) { foreach ( headers_list() as $header ) { list( $name, $val ) = explode( ':', $header, 2 ); if ( !strcasecmp( $name, $key ) ) { return trim( $val ); } } return null; } /** * Output an HTTP status code header * @since 1.26 * @param int $code Status code */ public function statusHeader( $code ) { if ( self::$disableForPostSend ) { wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [ 'code' => $code, 'exception' => new RuntimeException( 'Ignored post-send status header' ), ] ); return; } HttpStatus::header( $code ); } /** * Test if headers have been sent * @since 1.27 * @return bool */ public function headersSent() { return headers_sent(); } /** * Set the browser cookie * * @param string $name The name of the cookie. * @param string $value The value to be stored in the cookie. * @param int|null $expire Unix timestamp (in seconds) when the cookie should expire. * - 0 (the default) causes it to expire $wgCookieExpiration seconds from now. * - null causes it to be a session cookie. * @param array $options Assoc of additional cookie options: * - prefix: string, name prefix ($wgCookiePrefix) * - domain: string, cookie domain ($wgCookieDomain) * - path: string, cookie path ($wgCookiePath) * - secure: bool, secure attribute ($wgCookieSecure) * - httpOnly: bool, httpOnly attribute ($wgCookieHttpOnly) * - raw: bool, true to suppress encoding of the value * - sameSite: string|null, SameSite attribute. May be "strict", "lax", * "none", or null or "" for no attribute. (default absent) * - sameSiteLegacy: bool|null, If true, SameSite=None cookies will be * also be sent as a legacy cookie with an ss0 prefix * @since 1.22 Replaced $prefix, $domain, and $forceSecure with $options */ public function setCookie( $name, $value, $expire = 0, $options = [] ) { global $wgCookiePath, $wgCookiePrefix, $wgCookieDomain; global $wgCookieSecure, $wgCookieExpiration, $wgCookieHttpOnly; global $wgUseSameSiteLegacyCookies; $options = array_filter( $options, function ( $a ) { return $a !== null; } ) + [ 'prefix' => $wgCookiePrefix, 'domain' => $wgCookieDomain, 'path' => $wgCookiePath, 'secure' => $wgCookieSecure, 'httpOnly' => $wgCookieHttpOnly, 'raw' => false, 'sameSite' => '', 'sameSiteLegacy' => $wgUseSameSiteLegacyCookies ]; if ( strcasecmp( $options['sameSite'], 'none' ) === 0 && !empty( $options['sameSiteLegacy'] ) ) { $legacyOptions = $options; $legacyOptions['sameSiteLegacy'] = false; $legacyOptions['sameSite'] = ''; $this->setCookie( "ss0-$name", $value, $expire, $legacyOptions ); } if ( $expire === null ) { $expire = 0; // Session cookie } elseif ( $expire == 0 && $wgCookieExpiration != 0 ) { $expire = time() + $wgCookieExpiration; } if ( self::$disableForPostSend ) { $prefixedName = $options['prefix'] . $name; wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [ 'cookie' => $prefixedName, 'data' => [ 'name' => $prefixedName, 'value' => (string)$value, 'expire' => (int)$expire, 'path' => (string)$options['path'], 'domain' => (string)$options['domain'], 'secure' => (bool)$options['secure'], 'httpOnly' => (bool)$options['httpOnly'], 'sameSite' => (string)$options['sameSite'] ], 'exception' => new RuntimeException( 'Ignored post-send cookie' ), ] ); return; } if ( !Hooks::runner()->onWebResponseSetCookie( $name, $value, $expire, $options ) ) { return; } // Note: Don't try to move this earlier to reuse it for self::$disableForPostSend, // we need to use the altered values from the hook here. (T198525) $prefixedName = $options['prefix'] . $name; $value = (string)$value; $func = $options['raw'] ? 'setrawcookie' : 'setcookie'; $setOptions = [ 'expires' => (int)$expire, 'path' => (string)$options['path'], 'domain' => (string)$options['domain'], 'secure' => (bool)$options['secure'], 'httponly' => (bool)$options['httpOnly'], 'samesite' => (string)$options['sameSite'], ]; // Per RFC 6265, key is name + domain + path $key = "{$prefixedName}\n{$setOptions['domain']}\n{$setOptions['path']}"; // If this cookie name was in the request, fake an entry in // self::$setCookies for it so the deleting check works right. if ( isset( $_COOKIE[$prefixedName] ) && !array_key_exists( $key, self::$setCookies ) ) { self::$setCookies[$key] = []; } // PHP deletes if value is the empty string; also, a past expiry is deleting $deleting = ( $value === '' || $setOptions['expires'] > 0 && $setOptions['expires'] <= time() ); $logDesc = "$func: \"$prefixedName\", \"$value\", \"" . implode( '", "', $setOptions ) . '"'; $optionsForDeduplication = [ $func, $prefixedName, $value, $setOptions ]; if ( $deleting && !isset( self::$setCookies[$key] ) ) { // isset( null ) is false wfDebugLog( 'cookie', "already deleted $logDesc" ); return; } elseif ( !$deleting && isset( self::$setCookies[$key] ) && self::$setCookies[$key] === $optionsForDeduplication ) { wfDebugLog( 'cookie', "already set $logDesc" ); return; } wfDebugLog( 'cookie', $logDesc ); if ( $func === 'setrawcookie' ) { SetCookieCompat::setrawcookie( $prefixedName, $value, $setOptions ); } else { SetCookieCompat::setcookie( $prefixedName, $value, $setOptions ); } self::$setCookies[$key] = $deleting ? null : $optionsForDeduplication; } /** * Unset a browser cookie. * This sets the cookie with an empty value and an expiry set to a time in the past, * which will cause the browser to remove any cookie with the given name, domain and * path from its cookie store. Options other than these (and prefix) have no effect. * @param string $name Cookie name * @param array $options Cookie options, see {@link setCookie()} * @since 1.27 */ public function clearCookie( $name, $options = [] ) { $this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options ); } /** * Checks whether this request is performing cookie operations * * @return bool * @since 1.27 */ public function hasCookies() { return (bool)self::$setCookies; } }