entrypoint: Isolate entry points from PHP runtime for testing
1) Introduce EntryPointEnvironment which wraps functions that interact with the PHP runtime, so they can be mocked for testing. 2) Allow server info fields to be overwritten in FauxRequest. 3) Make MediaWikiEntryPoint use WebResponse to set headers Bug: T354216 Change-Id: Ic21950c956de5d2b5a7dd66a1e2de58f807cfd9f
This commit is contained in:
parent
0ce2d4e778
commit
24d0aee05e
18 changed files with 962 additions and 241 deletions
|
|
@ -171,6 +171,7 @@ because of Phabricator reports.
|
|||
$settings parameter and pass it on when calling the parent implementation.
|
||||
MaintenanceRunner will always provide this parameter when calling
|
||||
finalSetup().
|
||||
* WebResponse::disableForPostSend() is no longer static.
|
||||
* MediaWiki's virtualrest internal library has been removed in favor of the
|
||||
HTTP library like: Guzzle, MultiHttpClient or MwHttpRequest.
|
||||
* Several deprecated methods have been removed from the Content interface,
|
||||
|
|
|
|||
|
|
@ -1124,6 +1124,7 @@ $wgAutoloadLocalClasses = [
|
|||
'MediaWiki\\Edit\\SelserContext' => __DIR__ . '/includes/edit/SelserContext.php',
|
||||
'MediaWiki\\Edit\\SimpleParsoidOutputStash' => __DIR__ . '/includes/edit/SimpleParsoidOutputStash.php',
|
||||
'MediaWiki\\Emptiable' => __DIR__ . '/includes/libs/Emptiable.php',
|
||||
'MediaWiki\\EntryPointEnvironment' => __DIR__ . '/includes/EntryPointEnvironment.php',
|
||||
'MediaWiki\\Export\\WikiExporterFactory' => __DIR__ . '/includes/export/WikiExporterFactory.php',
|
||||
'MediaWiki\\ExtensionInfo' => __DIR__ . '/includes/utils/ExtensionInfo.php',
|
||||
'MediaWiki\\ExternalLinks\\ExternalLinksLookup' => __DIR__ . '/includes/ExternalLinks/ExternalLinksLookup.php',
|
||||
|
|
|
|||
120
includes/EntryPointEnvironment.php
Normal file
120
includes/EntryPointEnvironment.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki;
|
||||
|
||||
/**
|
||||
* Utility class wrapping PHP runtime state.
|
||||
*
|
||||
* @internal For use by MediaWikiEntryPoint subclasses.
|
||||
* Should be revised before wider use.
|
||||
*/
|
||||
class EntryPointEnvironment {
|
||||
|
||||
public function isCli(): bool {
|
||||
return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
|
||||
}
|
||||
|
||||
/**
|
||||
* @see fastcgi_finish_request
|
||||
*/
|
||||
public function hasFastCgi(): bool {
|
||||
return function_exists( 'fastcgi_finish_request' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @see fastcgi_finish_request
|
||||
*/
|
||||
public function fastCgiFinishRequest(): bool {
|
||||
if ( $this->hasFastCgi() ) {
|
||||
return fastcgi_finish_request();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getServerInfo( string $key, $default = null ) {
|
||||
return $_SERVER[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function exit( int $code = 0 ) {
|
||||
exit( $code );
|
||||
}
|
||||
|
||||
public function disableModDeflate(): void {
|
||||
if ( function_exists( 'apache_setenv' ) ) {
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@apache_setenv(
|
||||
'no-gzip',
|
||||
'1'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a PHP runtime error
|
||||
*
|
||||
* @see trigger_error
|
||||
*/
|
||||
public function triggerError( string $message, int $level = E_USER_NOTICE ): bool {
|
||||
return trigger_error( $message, $level );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an environment variable.
|
||||
*
|
||||
* @see getenv
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return array|false|string
|
||||
*/
|
||||
public function getEnv( string $name ) {
|
||||
return getenv( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an ini option.
|
||||
*
|
||||
* @see ini_get
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function getIni( string $name ) {
|
||||
return ini_get( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function setIniOption( string $name, $value ) {
|
||||
return ini_set( $name, $value );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
use MediaWiki\EntryPointEnvironment;
|
||||
use MediaWiki\MediaWikiEntryPoint;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
|
||||
|
|
@ -31,10 +32,14 @@ use MediaWiki\MediaWikiServices;
|
|||
*/
|
||||
class MediaWiki extends MediaWikiEntryPoint {
|
||||
|
||||
public function __construct( IContextSource $context = null ) {
|
||||
public function __construct(
|
||||
?IContextSource $context = null,
|
||||
?EntryPointEnvironment $environment = null
|
||||
) {
|
||||
$context ??= RequestContext::getMain();
|
||||
$environment ??= new EntryPointEnvironment();
|
||||
|
||||
parent::__construct( $context, MediaWikiServices::getInstance() );
|
||||
parent::__construct( $context, $environment, MediaWikiServices::getInstance() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
namespace MediaWiki;
|
||||
|
||||
use Exception;
|
||||
use HttpStatus;
|
||||
use IBufferingStatsdDataFactory;
|
||||
use IContextSource;
|
||||
use JobQueueGroup;
|
||||
|
|
@ -78,6 +79,9 @@ abstract class MediaWikiEntryPoint {
|
|||
|
||||
private IContextSource $context;
|
||||
private Config $config;
|
||||
private int $baseOutputBufferLevel;
|
||||
|
||||
private bool $postSendMode = false;
|
||||
|
||||
/** @var int Class DEFER_* constant; how non-blocking post-response tasks should run */
|
||||
private int $postSendStrategy;
|
||||
|
|
@ -93,23 +97,30 @@ abstract class MediaWikiEntryPoint {
|
|||
|
||||
private bool $preparedForOutput = false;
|
||||
|
||||
protected EntryPointEnvironment $environment;
|
||||
|
||||
private MediaWikiServices $mediaWikiServices;
|
||||
|
||||
/**
|
||||
* @param IContextSource $context
|
||||
* @param EntryPointEnvironment $environment
|
||||
* @param MediaWikiServices $mediaWikiServices
|
||||
*/
|
||||
public function __construct(
|
||||
IContextSource $context,
|
||||
EntryPointEnvironment $environment,
|
||||
MediaWikiServices $mediaWikiServices
|
||||
) {
|
||||
$this->context = $context;
|
||||
$this->mediaWikiServices = $mediaWikiServices;
|
||||
$this->config = $this->context->getConfig();
|
||||
$this->environment = $environment;
|
||||
$this->mediaWikiServices = $mediaWikiServices;
|
||||
|
||||
if ( MW_ENTRY_POINT === 'cli' ) {
|
||||
$this->baseOutputBufferLevel = 0;
|
||||
|
||||
if ( $environment->isCli() ) {
|
||||
$this->postSendStrategy = self::DEFER_CLI_MODE;
|
||||
} elseif ( function_exists( 'fastcgi_finish_request' ) ) {
|
||||
} elseif ( $environment->hasFastCgi() ) {
|
||||
$this->postSendStrategy = self::DEFER_FASTCGI_FINISH_REQUEST;
|
||||
} else {
|
||||
$this->postSendStrategy = self::DEFER_SET_LENGTH_AND_FLUSH;
|
||||
|
|
@ -137,6 +148,11 @@ abstract class MediaWikiEntryPoint {
|
|||
*/
|
||||
protected function doSetup() {
|
||||
// no-op
|
||||
// TODO: move ob_start( [ MediaWiki\Output\OutputHandler::class, 'handle' ] ) here
|
||||
// TODO: move HeaderCallback::register() here
|
||||
// TODO: move SessionManager::getGlobalSession() here (from Setup.php)
|
||||
// TODO: move AuthManager::autoCreateUser here (from Setup.php)
|
||||
// TODO: move pingback here (from Setup.php)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -318,7 +334,7 @@ abstract class MediaWikiEntryPoint {
|
|||
);
|
||||
$now = time();
|
||||
|
||||
$allowHeaders = !( $output->isDisabled() || headers_sent() );
|
||||
$allowHeaders = !( $output->isDisabled() || $this->inPostSendMode() );
|
||||
|
||||
if ( $cpIndex > 0 ) {
|
||||
if ( $allowHeaders ) {
|
||||
|
|
@ -408,6 +424,25 @@ abstract class MediaWikiEntryPoint {
|
|||
return 'external';
|
||||
}
|
||||
|
||||
/**
|
||||
* If the request URL matches a given base path, extract the path part of
|
||||
* the request URL after that base, and decode escape sequences in it.
|
||||
*
|
||||
* If the request URL does not match, false is returned.
|
||||
*
|
||||
* @internal Should be protected, made public for backwards
|
||||
* compatibility code in WebRequest.
|
||||
* @param string $basePath
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function getRequestPathSuffix( $basePath ) {
|
||||
return WebRequest::getRequestPathSuffix(
|
||||
$basePath,
|
||||
$this->getRequestURL()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the response to be sent to the client and then
|
||||
* does work that can be done *after* the
|
||||
|
|
@ -435,23 +470,18 @@ abstract class MediaWikiEntryPoint {
|
|||
// Defer everything else if possible...
|
||||
if ( $this->postSendStrategy === self::DEFER_FASTCGI_FINISH_REQUEST ) {
|
||||
// Flush the output to the client, continue processing, and avoid further output
|
||||
fastcgi_finish_request();
|
||||
$this->fastCgiFinishRequest();
|
||||
} elseif ( $this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH ) {
|
||||
// Flush the output to the client, continue processing, and avoid further output
|
||||
if ( ob_get_level() ) {
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@ob_end_flush();
|
||||
}
|
||||
// Flush the web server output buffer to the client/proxy if possible
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@flush();
|
||||
$this->flushOutputBuffer();
|
||||
}
|
||||
|
||||
// Since the headers and output where already flushed, disable WebResponse setters
|
||||
// during post-send processing to warnings and unexpected behavior (T191537)
|
||||
WebResponse::disableForPostSend();
|
||||
$this->enterPostSendMode();
|
||||
|
||||
// Run post-send updates while preventing further output...
|
||||
ob_start( static function () {
|
||||
$this->startOutputBuffer( static function () {
|
||||
return ''; // do not output uncaught exceptions
|
||||
} );
|
||||
try {
|
||||
|
|
@ -462,11 +492,14 @@ abstract class MediaWikiEntryPoint {
|
|||
MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
|
||||
);
|
||||
}
|
||||
$length = ob_get_length();
|
||||
$length = $this->getOutputBufferLength();
|
||||
if ( $length > 0 ) {
|
||||
trigger_error( __METHOD__ . ": suppressed $length byte(s)", E_USER_NOTICE );
|
||||
$this->triggerError(
|
||||
__METHOD__ . ": suppressed $length byte(s)",
|
||||
E_USER_NOTICE
|
||||
);
|
||||
}
|
||||
ob_end_clean();
|
||||
$this->discardOutputBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -517,7 +550,7 @@ abstract class MediaWikiEntryPoint {
|
|||
/**
|
||||
* Print a response body to the current buffer (if there is one) or the server (otherwise)
|
||||
*
|
||||
* This method should be called after commitMainTransaction() and before postOutputShutdown()
|
||||
* This method should be called after commitMainTransaction() and before doPostOutputShutdown()
|
||||
*
|
||||
* Any accompanying Content-Type header is assumed to have already been set
|
||||
*
|
||||
|
|
@ -525,11 +558,11 @@ abstract class MediaWikiEntryPoint {
|
|||
*/
|
||||
protected function outputResponsePayload( $content ) {
|
||||
// Append any visible profiling data in a manner appropriate for the Content-Type
|
||||
ob_start();
|
||||
$this->startOutputBuffer();
|
||||
try {
|
||||
Profiler::instance()->logDataPageOutputOnly();
|
||||
} finally {
|
||||
$content .= ob_get_clean();
|
||||
$content .= $this->drainOutputBuffer();
|
||||
}
|
||||
|
||||
// By default, usually one output buffer is active now, either the internal PHP buffer
|
||||
|
|
@ -542,33 +575,44 @@ abstract class MediaWikiEntryPoint {
|
|||
|
||||
// Disable mod_deflate compression since it interferes with the output buffer set
|
||||
// by MW_SETUP_CALLBACK and can also cause the client to wait on deferred updates
|
||||
if ( function_exists( 'apache_setenv' ) ) {
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@apache_setenv( 'no-gzip', '1' );
|
||||
$this->disableModDeflate();
|
||||
|
||||
if ( $this->inPostSendMode() ) {
|
||||
// Output already sent. This may happen for actions or special pages
|
||||
// that generate raw output and disable OutputPage. In that case,
|
||||
// we should just exit, but we should log an error if $content
|
||||
// was not empty.
|
||||
if ( $content !== '' ) {
|
||||
$length = strlen( $content );
|
||||
$this->triggerError(
|
||||
__METHOD__ . ": discarded $length byte(s) of output",
|
||||
E_USER_NOTICE
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
// "Content-Length" is used to prevent clients from waiting on deferred updates
|
||||
$this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH &&
|
||||
// The HTTP response code clearly allows for a meaningful body
|
||||
in_array( http_response_code(), [ 200, 404 ], true ) &&
|
||||
in_array( $this->getStatusCode(), [ 200, 404 ], true ) &&
|
||||
// The queue of (post-send) deferred updates is non-empty
|
||||
DeferredUpdates::pendingUpdatesCount() &&
|
||||
// Any buffered output is not spread out across multiple output buffers
|
||||
ob_get_level() <= 1 &&
|
||||
// It is not too late to set additional HTTP headers
|
||||
!headers_sent()
|
||||
$this->getOutputBufferLevel() <= 1
|
||||
) {
|
||||
$response = $this->context->getRequest()->response();
|
||||
|
||||
$obStatus = ob_get_status();
|
||||
$obStatus = $this->getOutputBufferStatus();
|
||||
if ( !isset( $obStatus['name'] ) ) {
|
||||
// No output buffer is active
|
||||
$response->header( 'Content-Length: ' . strlen( $content ) );
|
||||
} elseif ( $obStatus['name'] === 'default output handler' ) {
|
||||
// Internal PHP "output_buffering" output buffer (note that the internal PHP
|
||||
// "zlib.output_compression" output buffer is named "zlib output compression")
|
||||
$response->header( 'Content-Length: ' . ( ob_get_length() + strlen( $content ) ) );
|
||||
$response->header( 'Content-Length: ' .
|
||||
( $this->getOutputBufferLength() + strlen( $content ) ) );
|
||||
}
|
||||
|
||||
// The MW_SETUP_CALLBACK output buffer ("MediaWiki\OutputHandler::handle") sets
|
||||
|
|
@ -580,7 +624,7 @@ abstract class MediaWikiEntryPoint {
|
|||
// has been read (informed by any "Content-Length" header). This prevents the client
|
||||
// from waiting on deferred updates.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection
|
||||
if ( ( $_SERVER['SERVER_PROTOCOL'] ?? '' ) === 'HTTP/1.1' ) {
|
||||
if ( $this->getServerInfo( 'SERVER_PROTOCOL' ) === 'HTTP/1.1' ) {
|
||||
$response->header( 'Connection: close' );
|
||||
}
|
||||
}
|
||||
|
|
@ -588,7 +632,7 @@ abstract class MediaWikiEntryPoint {
|
|||
// Print the content *after* adjusting HTTP headers and disabling mod_deflate since
|
||||
// calling "print" will send the output to the client if there is no output buffer or
|
||||
// if the output buffer chunk size is reached
|
||||
print $content;
|
||||
$this->print( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -837,4 +881,317 @@ abstract class MediaWikiEntryPoint {
|
|||
protected function getConfig( string $key ) {
|
||||
return $this->config->get( $key );
|
||||
}
|
||||
|
||||
protected function isCli(): bool {
|
||||
return $this->environment->isCli();
|
||||
}
|
||||
|
||||
protected function hasFastCgi(): bool {
|
||||
return $this->environment->hasFastCgi();
|
||||
}
|
||||
|
||||
protected function getServerInfo( string $key, $default = null ) {
|
||||
return $this->environment->getServerInfo( $key, $default );
|
||||
}
|
||||
|
||||
protected function print( $data ) {
|
||||
if ( $this->inPostSendMode() ) {
|
||||
throw new RuntimeException( 'Output already sent!' );
|
||||
}
|
||||
|
||||
print $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
protected function exit( int $code = 0 ) {
|
||||
$this->environment->exit( $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new output buffer level.
|
||||
*
|
||||
* @param ?callable $callback
|
||||
*
|
||||
* @see ob_start
|
||||
*/
|
||||
protected function startOutputBuffer( ?callable $callback = null ): void {
|
||||
ob_start( $callback );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the current output buffer and resets the buffer
|
||||
* (but does not end it).
|
||||
*
|
||||
* @see ob_get_clean
|
||||
* @return false|string
|
||||
*/
|
||||
protected function drainOutputBuffer() {
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the base buffer level.
|
||||
* The base buffer level controls whether flushOutputBuffer() actually sends
|
||||
* data to the client, or just commits the data to a parent buffer.
|
||||
*
|
||||
* Per default, the base buffer level is initialized to 0.
|
||||
* Tests will have to establish a different base level, in order to be able
|
||||
* to capture output.
|
||||
*
|
||||
* @see captureOutput();
|
||||
*
|
||||
* @param int $offset The base level relative to the current level.
|
||||
* Set to 0 if the current level should not be flushed by
|
||||
* flushOutputBuffer(). Set to -1 if the current level should
|
||||
* be flushed.
|
||||
*/
|
||||
public function establishOutputBufferLevel( int $offset = 0 ): void {
|
||||
$level = ob_get_level() + $offset;
|
||||
|
||||
if ( $level < 0 ) {
|
||||
throw new RuntimeException(
|
||||
'Cannot set the output buffer base level to a negative number'
|
||||
);
|
||||
}
|
||||
|
||||
$this->baseOutputBufferLevel = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output buffer level, taking into account the base buffer
|
||||
* level.
|
||||
*
|
||||
* @see ob_get_level
|
||||
*/
|
||||
protected function getOutputBufferLevel(): int {
|
||||
return max( 0, ob_get_level() - $this->baseOutputBufferLevel );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the current output buffer, appending its content to the parent
|
||||
* buffer.
|
||||
* @see ob_end_flush
|
||||
*/
|
||||
protected function commitOutputBuffer(): bool {
|
||||
if ( $this->inPostSendMode() ) {
|
||||
throw new RuntimeException( 'Output already sent!' );
|
||||
}
|
||||
|
||||
$level = $this->getOutputBufferLevel();
|
||||
if ( $level === 0 ) {
|
||||
return false;
|
||||
} else {
|
||||
//phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
return @ob_end_flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits all output buffers down to the base buffer level,
|
||||
* then ends the base level buffer and returns its contents.
|
||||
*
|
||||
* Intended for testing, to allow the generated output to be examined.
|
||||
* Needs establishOutputBufferLevel() to be called before run().
|
||||
* getOutputBufferLevel() will return 0 after this method returns.
|
||||
*
|
||||
* @see establishOutputBufferLevel();
|
||||
* @see ob_end_clean
|
||||
*/
|
||||
public function captureOutput(): string {
|
||||
if ( !$this->inPostSendMode() ) {
|
||||
throw new RuntimeException( 'Output not yet sent!' );
|
||||
}
|
||||
|
||||
$this->flushOutputBuffer();
|
||||
return $this->drainOutputBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits all output buffers down to the base buffer level.
|
||||
* getOutputBufferLevel() will return 0 after this method returns.
|
||||
* If the base buffer level is 0, this sends data to the client.
|
||||
*
|
||||
* @see ob_end_flush
|
||||
* @see flush
|
||||
*/
|
||||
protected function flushOutputBuffer(): void {
|
||||
if ( $this->inPostSendMode() && $this->getOutputBufferLevel() ) {
|
||||
throw new RuntimeException( 'Output already sent!' );
|
||||
}
|
||||
|
||||
while ( $this->getOutputBufferLevel() ) {
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@ob_end_flush();
|
||||
}
|
||||
|
||||
// If the true buffer level is 0, flush the system buffer as well,
|
||||
// so we actually send data to the client.
|
||||
if ( ob_get_level() === 0 ) {
|
||||
// Flush the web server output buffer to the client/proxy if possible
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards all buffered output, down to the base buffer level.
|
||||
*/
|
||||
protected function discardAllOutput() {
|
||||
while ( $this->getOutputBufferLevel() ) {
|
||||
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
|
||||
@ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ob_get_length
|
||||
* @return false|int
|
||||
*/
|
||||
protected function getOutputBufferLength() {
|
||||
return ob_get_length();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ob_get_status
|
||||
*/
|
||||
protected function getOutputBufferStatus(): array {
|
||||
return ob_get_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ob_end_clean
|
||||
*/
|
||||
protected function discardOutputBuffer(): bool {
|
||||
return ob_end_clean();
|
||||
}
|
||||
|
||||
protected function disableModDeflate(): void {
|
||||
$this->environment->disableModDeflate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http_response_code
|
||||
* @return int|bool
|
||||
*/
|
||||
protected function getStatusCode() {
|
||||
return $this->getResponse()->getStatusCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see headers_sent
|
||||
*/
|
||||
protected function inPostSendMode(): bool {
|
||||
return $this->postSendMode || $this->getResponse()->headersSent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a PHP runtime error
|
||||
*
|
||||
* @see trigger_error
|
||||
*/
|
||||
protected function triggerError( string $message, int $level = E_USER_NOTICE ): bool {
|
||||
return $this->environment->triggerError( $message, $level );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an environment variable.
|
||||
*
|
||||
* @see getenv
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return array|false|string
|
||||
*/
|
||||
protected function getEnv( string $name ) {
|
||||
return $this->environment->getEnv( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an ini option.
|
||||
*
|
||||
* @see ini_get
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
protected function getIni( string $name ) {
|
||||
return $this->environment->getIni( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
protected function setIniOption( string $name, $value ) {
|
||||
return $this->environment->setIniOption( $name, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* @see header() function
|
||||
*/
|
||||
protected function header( string $header, bool $replace = true, int $status = 0 ): void {
|
||||
$this->getResponse()->header( $header, $replace, $status );
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HttpStatus
|
||||
*/
|
||||
protected function status( int $code ): void {
|
||||
$this->header( HttpStatus::getHeader( $code ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls fastcgi_finish_request if possible. Reasons for not calling
|
||||
* fastcgi_finish_request include the fastcgi extension not being loaded
|
||||
* and the base buffer level being different from 0.
|
||||
*
|
||||
* @see fastcgi_finish_request
|
||||
* @return bool true if fastcgi_finish_request was called and successful.
|
||||
*/
|
||||
protected function fastCgiFinishRequest(): bool {
|
||||
if ( !$this->inPostSendMode() ) {
|
||||
$this->flushOutputBuffer();
|
||||
}
|
||||
|
||||
// Don't mess with fastcgi on CLI mode.
|
||||
if ( $this->isCli() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only mess with fastcgi if we really have no buffers left.
|
||||
if ( ob_get_level() > 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->environment->fastCgiFinishRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current request's path and query string (not a full URL),
|
||||
* like PHP's built-in $_SERVER['REQUEST_URI'].
|
||||
*
|
||||
* @see WebRequest::getRequestURL()
|
||||
* @see WebRequest::getGlobalRequestURL()
|
||||
*/
|
||||
protected function getRequestURL(): string {
|
||||
// Despite the name, this just returns the path and query string
|
||||
return $this->getRequest()->getRequestURL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all output to the client.
|
||||
*/
|
||||
protected function enterPostSendMode() {
|
||||
$this->postSendMode = true;
|
||||
|
||||
$this->getResponse()->disableForPostSend();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,12 @@ use MWException;
|
|||
* @ingroup HTTP
|
||||
*/
|
||||
class FauxRequest extends WebRequest {
|
||||
private $wasPosted;
|
||||
private $requestUrl;
|
||||
protected $cookies = [];
|
||||
/** @var array */
|
||||
private $uploadData = [];
|
||||
private bool $wasPosted;
|
||||
private ?string $requestUrl = null;
|
||||
private array $serverInfo;
|
||||
|
||||
protected array $cookies = [];
|
||||
private array $uploadData = [];
|
||||
|
||||
/**
|
||||
* @stable to call
|
||||
|
|
@ -60,6 +61,7 @@ class FauxRequest extends WebRequest {
|
|||
$session = null, $protocol = 'http'
|
||||
) {
|
||||
$this->requestTime = microtime( true );
|
||||
$this->serverInfo = $_SERVER;
|
||||
|
||||
$this->data = $data;
|
||||
$this->wasPosted = $wasPosted;
|
||||
|
|
@ -77,6 +79,14 @@ class FauxRequest extends WebRequest {
|
|||
$this->protocol = $protocol;
|
||||
}
|
||||
|
||||
public function response(): FauxResponse {
|
||||
/* Lazy initialization of response object for this request */
|
||||
if ( !$this->response ) {
|
||||
$this->response = new FauxResponse();
|
||||
}
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the header list
|
||||
*/
|
||||
|
|
@ -105,6 +115,10 @@ class FauxRequest extends WebRequest {
|
|||
}
|
||||
}
|
||||
|
||||
public function getQueryValuesOnly() {
|
||||
return $this->getQueryValues();
|
||||
}
|
||||
|
||||
public function getMethod() {
|
||||
return $this->wasPosted ? 'POST' : 'GET';
|
||||
}
|
||||
|
|
@ -202,10 +216,35 @@ class FauxRequest extends WebRequest {
|
|||
|
||||
/**
|
||||
* @param string $url
|
||||
*
|
||||
* @since 1.25
|
||||
*/
|
||||
public function setRequestURL( $url ) {
|
||||
public function setRequestURL( string $url ) {
|
||||
$this->requestUrl = $url;
|
||||
|
||||
if ( preg_match( '@^(.*)://@', $url, $m ) ) {
|
||||
$this->protocol = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.42
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRequestURL(): bool {
|
||||
return $this->requestUrl !== null;
|
||||
}
|
||||
|
||||
protected function getServerInfo( $name, $default = null ): ?string {
|
||||
return $this->serverInfo[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $_SERVER
|
||||
* @param array $info
|
||||
*/
|
||||
public function setServerInfo( array $info ) {
|
||||
$this->serverInfo = $info;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ class FauxResponse extends WebResponse {
|
|||
* @param null|int $http_response_code Forces the HTTP response code to the specified value.
|
||||
*/
|
||||
public function header( $string, $replace = true, $http_response_code = null ) {
|
||||
if ( $this->disableForPostSend ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( str_starts_with( $string, 'HTTP/' ) ) {
|
||||
$parts = explode( ' ', $string, 3 );
|
||||
$this->code = intval( $parts[1] );
|
||||
|
|
@ -118,6 +122,10 @@ class FauxResponse extends WebResponse {
|
|||
* @param array $options Ignored in this faux subclass.
|
||||
*/
|
||||
public function setCookie( $name, $value, $expire = 0, $options = [] ) {
|
||||
if ( $this->disableForPostSend ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cookieConfig = $this->getCookieConfig();
|
||||
$cookiePath = $cookieConfig->get( MainConfigNames::CookiePath );
|
||||
$cookiePrefix = $cookieConfig->get( MainConfigNames::CookiePrefix );
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class WebRequest {
|
|||
* Lazy-init response object
|
||||
* @var WebResponse
|
||||
*/
|
||||
private $response;
|
||||
protected ?WebResponse $response = null;
|
||||
|
||||
/**
|
||||
* Cached client IP address
|
||||
|
|
@ -129,6 +129,23 @@ class WebRequest {
|
|||
$this->queryAndPathParams = $this->queryParams = $_GET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an entry from the $_SERVER array.
|
||||
* This exists mainly to allow us to inject fake values for testing.
|
||||
*
|
||||
* @param string $name A well known key for $_SERVER,
|
||||
* see <https://www.php.net/manual/en/reserved.variables.server.php>.
|
||||
* Only fields that contain string values are supported,
|
||||
* so 'argv' and 'argc' are not safe to use.
|
||||
* @param ?string $default The value to return if no value is known for the
|
||||
* key $name.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
protected function getServerInfo( string $name, ?string $default = null ): ?string {
|
||||
return isset( $_SERVER[$name] ) ? (string)$_SERVER[$name] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract relevant query arguments from the http request uri's path
|
||||
* to be merged with the normal php provided query arguments.
|
||||
|
|
@ -149,13 +166,13 @@ class WebRequest {
|
|||
* @return string[] Any query arguments found in path matches.
|
||||
* @throws FatalError If invalid routes are configured (T48998)
|
||||
*/
|
||||
protected static function getPathInfo( $want = 'all' ) {
|
||||
protected function getPathInfo( $want = 'all' ) {
|
||||
// PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
|
||||
// And also by Apache 2.x, double slashes are converted to single slashes.
|
||||
// So we will use REQUEST_URI if possible.
|
||||
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
|
||||
$url = $this->getServerInfo( 'REQUEST_URI' );
|
||||
if ( $url !== null ) {
|
||||
// Slurp out the path portion to examine...
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
if ( !preg_match( '!^https?://!', $url ) ) {
|
||||
$url = 'http://unused' . $url;
|
||||
}
|
||||
|
|
@ -208,14 +225,16 @@ class WebRequest {
|
|||
global $wgUsePathInfo;
|
||||
$matches = [];
|
||||
if ( $wgUsePathInfo ) {
|
||||
if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
|
||||
$origPathInfo = $this->getServerInfo( 'ORIG_PATH_INFO' ) ?? '';
|
||||
$pathInfo = $this->getServerInfo( 'PATH_INFO' ) ?? '';
|
||||
if ( $origPathInfo !== '' ) {
|
||||
// Mangled PATH_INFO
|
||||
// https://bugs.php.net/bug.php?id=31892
|
||||
// Also reported when ini_get('cgi.fix_pathinfo')==false
|
||||
$matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
|
||||
} elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
|
||||
$matches['title'] = substr( $origPathInfo, 1 );
|
||||
} elseif ( $pathInfo !== '' ) {
|
||||
// Regular old PATH_INFO yay
|
||||
$matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
|
||||
$matches['title'] = substr( $pathInfo, 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -232,11 +251,13 @@ class WebRequest {
|
|||
* @since 1.35
|
||||
* @param string $basePath The base URL path. Trailing slashes will be
|
||||
* stripped.
|
||||
* @param ?string $requestUrl The request URL to examine. If not given, the
|
||||
* URL returned by getGlobalRequestURL() will be used.
|
||||
* @return string|false
|
||||
*/
|
||||
public static function getRequestPathSuffix( $basePath ) {
|
||||
public static function getRequestPathSuffix( string $basePath, ?string $requestUrl ) {
|
||||
$basePath = rtrim( $basePath, '/' ) . '/';
|
||||
$requestUrl = self::getGlobalRequestURL();
|
||||
$requestUrl ??= self::getGlobalRequestURL();
|
||||
$qpos = strpos( $requestUrl, '?' );
|
||||
if ( $qpos !== false ) {
|
||||
$requestPath = substr( $requestUrl, 0, $qpos );
|
||||
|
|
@ -375,7 +396,7 @@ class WebRequest {
|
|||
* available variant URLs.
|
||||
*/
|
||||
public function interpolateTitle() {
|
||||
$matches = self::getPathInfo( 'title' );
|
||||
$matches = $this->getPathInfo( 'title' );
|
||||
foreach ( $matches as $key => $val ) {
|
||||
$this->data[$key] = $this->queryAndPathParams[$key] = $val;
|
||||
}
|
||||
|
|
@ -770,7 +791,7 @@ class WebRequest {
|
|||
* @return-taint tainted
|
||||
*/
|
||||
public function getRawQueryString() {
|
||||
return $_SERVER['QUERY_STRING'];
|
||||
return $this->getServerInfo( 'QUERY_STRING' ) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -807,7 +828,7 @@ class WebRequest {
|
|||
* @return string
|
||||
*/
|
||||
public function getMethod() {
|
||||
return $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
return $this->getServerInfo( 'REQUEST_METHOD' ) ?: 'GET';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1105,14 +1126,11 @@ class WebRequest {
|
|||
/**
|
||||
* Return a handle to WebResponse style object, for setting cookies,
|
||||
* headers and other stuff, for Request being worked on.
|
||||
*
|
||||
* @return WebResponse
|
||||
*/
|
||||
public function response() {
|
||||
public function response(): WebResponse {
|
||||
/* Lazy initialization of response object for this request */
|
||||
if ( !is_object( $this->response ) ) {
|
||||
$class = ( $this instanceof FauxRequest ) ? FauxResponse::class : WebResponse::class;
|
||||
$this->response = new $class();
|
||||
if ( !$this->response ) {
|
||||
$this->response = new WebResponse();
|
||||
}
|
||||
return $this->response;
|
||||
}
|
||||
|
|
@ -1253,11 +1271,11 @@ class WebRequest {
|
|||
* @return string|null
|
||||
*/
|
||||
protected function getRawIP() {
|
||||
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
$remoteAddr = $this->getServerInfo( 'REMOTE_ADDR' );
|
||||
if ( !$remoteAddr ) {
|
||||
return null;
|
||||
}
|
||||
if ( is_array( $remoteAddr ) || str_contains( $remoteAddr, ',' ) ) {
|
||||
if ( str_contains( $remoteAddr, ',' ) ) {
|
||||
throw new MWException( 'Remote IP must not contain multiple values' );
|
||||
}
|
||||
|
||||
|
|
@ -1391,11 +1409,11 @@ class WebRequest {
|
|||
* @since 1.28
|
||||
*/
|
||||
public function hasSafeMethod() {
|
||||
if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) {
|
||||
if ( $this->getServerInfo( 'REQUEST_METHOD' ) === null ) {
|
||||
return false; // CLI mode
|
||||
}
|
||||
|
||||
return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
|
||||
return in_array( $this->getServerInfo( 'REQUEST_METHOD' ), [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class WebResponse {
|
|||
protected static $setCookies = [];
|
||||
|
||||
/** @var bool Used to disable setters before running jobs post-request (T191537) */
|
||||
protected static $disableForPostSend = false;
|
||||
protected $disableForPostSend = false;
|
||||
|
||||
/**
|
||||
* Disable setters for post-send processing
|
||||
|
|
@ -50,10 +50,10 @@ class WebResponse {
|
|||
* self::statusHeader() will log a warning and return without
|
||||
* setting cookies or headers.
|
||||
*
|
||||
* @since 1.32
|
||||
* @since 1.32 (non-static since 1.42)
|
||||
*/
|
||||
public static function disableForPostSend() {
|
||||
self::$disableForPostSend = true;
|
||||
public function disableForPostSend() {
|
||||
$this->disableForPostSend = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,7 +63,7 @@ class WebResponse {
|
|||
* @param null|int $http_response_code Forces the HTTP response code to the specified value.
|
||||
*/
|
||||
public function header( $string, $replace = true, $http_response_code = null ) {
|
||||
if ( self::$disableForPostSend ) {
|
||||
if ( $this->disableForPostSend ) {
|
||||
wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
|
||||
'header' => $string,
|
||||
'replace' => $replace,
|
||||
|
|
@ -81,6 +81,15 @@ class WebResponse {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http_response_code
|
||||
* @return int|bool
|
||||
* @since 1.42
|
||||
*/
|
||||
public function getStatusCode() {
|
||||
return http_response_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a response header
|
||||
* @param string $key The name of the header to get (case insensitive).
|
||||
|
|
@ -103,7 +112,7 @@ class WebResponse {
|
|||
* @param int $code Status code
|
||||
*/
|
||||
public function statusHeader( $code ) {
|
||||
if ( self::$disableForPostSend ) {
|
||||
if ( $this->disableForPostSend ) {
|
||||
wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
|
||||
'code' => $code,
|
||||
'exception' => new RuntimeException( 'Ignored post-send status header' ),
|
||||
|
|
@ -182,7 +191,7 @@ class WebResponse {
|
|||
$expire = time() + $cookieExpiration;
|
||||
}
|
||||
|
||||
if ( self::$disableForPostSend ) {
|
||||
if ( $this->disableForPostSend ) {
|
||||
$prefixedName = $options['prefix'] . $name;
|
||||
wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
|
||||
'cookie' => $prefixedName,
|
||||
|
|
@ -206,7 +215,7 @@ class WebResponse {
|
|||
return;
|
||||
}
|
||||
|
||||
// Note: Don't try to move this earlier to reuse it for self::$disableForPostSend,
|
||||
// Note: Don't try to move this earlier to reuse it for $this->disableForPostSend,
|
||||
// we need to use the altered values from the hook here. (T198525)
|
||||
$prefixedName = $options['prefix'] . $name;
|
||||
$value = (string)$value;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use HttpError;
|
|||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\MainConfigNames;
|
||||
use MediaWiki\MediaWikiEntryPoint;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Output\OutputPage;
|
||||
use MediaWiki\Permissions\PermissionStatus;
|
||||
use MediaWiki\Profiler\ProfilingContext;
|
||||
|
|
@ -41,13 +40,6 @@ use Wikimedia\Rdbms\DBConnectionError;
|
|||
*/
|
||||
class ActionEntryPoint extends MediaWikiEntryPoint {
|
||||
|
||||
public function __construct(
|
||||
$context,
|
||||
MediaWikiServices $mediaWikiServices
|
||||
) {
|
||||
parent::__construct( $context, $mediaWikiServices );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwritten to narrow the return type to RequestContext
|
||||
* @return RequestContext
|
||||
|
|
@ -82,8 +74,8 @@ class ActionEntryPoint extends MediaWikiEntryPoint {
|
|||
$cache = new HTMLFileCache( $context->getTitle(), $action );
|
||||
if ( $cache->isCached() ) {
|
||||
$cache->loadFromFileCache( $context, HTMLFileCache::MODE_OUTAGE );
|
||||
print MWExceptionRenderer::getHTML( $e );
|
||||
exit;
|
||||
$this->print( MWExceptionRenderer::getHTML( $e ) );
|
||||
$this->exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,30 @@ class HttpStatus {
|
|||
return $statusMessage[$code] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an HTTP status code header
|
||||
*
|
||||
* @since 1.42
|
||||
* @param int $code Status code
|
||||
* @return string
|
||||
*/
|
||||
public static function getHeader( $code ): string {
|
||||
static $version = null;
|
||||
$message = self::getMessage( $code );
|
||||
if ( $message === null ) {
|
||||
throw new InvalidArgumentException( "Unknown HTTP status code $code" );
|
||||
}
|
||||
|
||||
if ( $version === null ) {
|
||||
$version = isset( $_SERVER['SERVER_PROTOCOL'] ) &&
|
||||
$_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0' ?
|
||||
'1.0' :
|
||||
'1.1';
|
||||
}
|
||||
|
||||
return "HTTP/$version $code $message";
|
||||
}
|
||||
|
||||
/**
|
||||
* Output an HTTP status code header
|
||||
*
|
||||
|
|
@ -94,22 +118,13 @@ class HttpStatus {
|
|||
* @param int $code Status code
|
||||
*/
|
||||
public static function header( $code ) {
|
||||
static $version = null;
|
||||
$message = self::getMessage( $code );
|
||||
if ( $message === null ) {
|
||||
trigger_error( "Unknown HTTP status code $code", E_USER_WARNING );
|
||||
return;
|
||||
}
|
||||
|
||||
\MediaWiki\Request\HeaderCallback::warnIfHeadersSent();
|
||||
if ( $version === null ) {
|
||||
$version = isset( $_SERVER['SERVER_PROTOCOL'] ) &&
|
||||
$_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0' ?
|
||||
'1.0' :
|
||||
'1.1';
|
||||
}
|
||||
|
||||
header( "HTTP/$version $code $message" );
|
||||
try {
|
||||
header( self::getHeader( $code ) );
|
||||
} catch ( InvalidArgumentException $ex ) {
|
||||
trigger_error( "Unknown HTTP status code $code", E_USER_WARNING );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
*/
|
||||
|
||||
use MediaWiki\Actions\ActionEntryPoint;
|
||||
use MediaWiki\EntryPointEnvironment;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
|
||||
define( 'MW_ENTRY_POINT', 'index' );
|
||||
|
|
@ -50,6 +51,7 @@ require __DIR__ . '/includes/WebStart.php';
|
|||
// Create the entry point object and call run() to handle the request.
|
||||
( new ActionEntryPoint(
|
||||
RequestContext::getMain(),
|
||||
new EntryPointEnvironment(),
|
||||
// TODO: Maybe create a light-weight services container here instead.
|
||||
MediaWikiServices::getInstance()
|
||||
) )->run();
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ $wgAutoloadClasses += [
|
|||
'NullGuzzleClient' => "$testDir/phpunit/mocks/NullGuzzleClient.php",
|
||||
'NullHttpRequestFactory' => "$testDir/phpunit/mocks/NullHttpRequestFactory.php",
|
||||
'NullMultiHttpClient' => "$testDir/phpunit/mocks/NullMultiHttpClient.php",
|
||||
'MediaWiki\Tests\MockEnvironment' => "$testDir/phpunit/mocks/MockEnvironment.php",
|
||||
|
||||
# tests/phpunit/unit/includes
|
||||
'Wikimedia\\Reflection\\GhostFieldTestClass' => "$testDir/phpunit/mocks/GhostFieldTestClass.php",
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Deferred\DeferredUpdates;
|
||||
use MediaWiki\MainConfigNames;
|
||||
use MediaWiki\Request\WebResponse;
|
||||
|
||||
/**
|
||||
* @group Database
|
||||
*/
|
||||
class MediaWikiTest extends MediaWikiIntegrationTestCase {
|
||||
private $oldServer, $oldGet, $oldPost;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->overrideConfigValues( [
|
||||
MainConfigNames::Server => 'http://example.org',
|
||||
MainConfigNames::ScriptPath => '/w',
|
||||
MainConfigNames::Script => '/w/index.php',
|
||||
MainConfigNames::ArticlePath => '/wiki/$1',
|
||||
MainConfigNames::ActionPaths => [],
|
||||
MainConfigNames::LanguageCode => 'en',
|
||||
] );
|
||||
|
||||
// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
|
||||
$this->oldServer = $_SERVER;
|
||||
$this->oldGet = $_GET;
|
||||
$this->oldPost = $_POST;
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
$_SERVER = $this->oldServer;
|
||||
$_GET = $this->oldGet;
|
||||
$_POST = $this->oldPost;
|
||||
// The MediaWiki class writes to $wgTitle. Revert any writes done in this test to make
|
||||
// sure that they don't leak into other tests (T341951)
|
||||
$GLOBALS['wgTitle'] = null;
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a post-send job can not set cookies (T191537).
|
||||
* @coversNothing
|
||||
*/
|
||||
public function testPostSendJobDoesNotSetCookie() {
|
||||
// Prevent updates from running
|
||||
$cleanup = DeferredUpdates::preventOpportunisticUpdates();
|
||||
|
||||
$response = new WebResponse;
|
||||
|
||||
// A job that attempts to set a cookie
|
||||
$jobHasRun = false;
|
||||
DeferredUpdates::addCallableUpdate( static function () use ( $response, &$jobHasRun ) {
|
||||
$jobHasRun = true;
|
||||
$response->setCookie( 'JobCookie', 'yes' );
|
||||
$response->header( 'Foo: baz' );
|
||||
} );
|
||||
|
||||
$hookWasRun = false;
|
||||
$this->setTemporaryHook( 'WebResponseSetCookie', static function () use ( &$hookWasRun ) {
|
||||
$hookWasRun = true;
|
||||
return true;
|
||||
} );
|
||||
|
||||
$logger = new TestLogger();
|
||||
$logger->setCollect( true );
|
||||
$this->setLogger( 'cookie', $logger );
|
||||
$this->setLogger( 'header', $logger );
|
||||
|
||||
$mw = new MediaWiki();
|
||||
$mw->doPostOutputShutdown();
|
||||
// restInPeace() might have been registered to a callback of
|
||||
// register_postsend_function() and thus can not be triggered from
|
||||
// PHPUnit.
|
||||
if ( $jobHasRun === false ) {
|
||||
$mw->restInPeace();
|
||||
}
|
||||
|
||||
$this->assertTrue( $jobHasRun, 'post-send job has run' );
|
||||
$this->assertFalse( $hookWasRun,
|
||||
'post-send job must not trigger WebResponseSetCookie hook' );
|
||||
$this->assertEquals(
|
||||
[
|
||||
[ 'info', 'ignored post-send cookie {cookie}' ],
|
||||
[ 'info', 'ignored post-send header {header}' ],
|
||||
],
|
||||
$logger->getBuffer()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -659,4 +659,34 @@ class WebRequestTest extends MediaWikiIntegrationTestCase {
|
|||
[ self::INTERNAL_SERVER . '/w/index.php?action=history&title=Title', $cdnUrls, /* matchOrder= */ true, false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRequestPathSuffix
|
||||
*
|
||||
* @param string $basePath
|
||||
* @param string $requestUrl
|
||||
* @param string|false $expected
|
||||
*/
|
||||
public function testRequestPathSuffix( string $basePath, string $requestUrl, $expected ) {
|
||||
$suffix = WebRequest::getRequestPathSuffix( $basePath, $requestUrl );
|
||||
$this->assertSame( $expected, $suffix );
|
||||
}
|
||||
|
||||
public static function provideRequestPathSuffix() {
|
||||
yield [
|
||||
'/w/index.php',
|
||||
'/w/index.php/Hello',
|
||||
'Hello'
|
||||
];
|
||||
yield [
|
||||
'/w/index.php',
|
||||
'/w/index.php/Hello?x=y',
|
||||
'Hello'
|
||||
];
|
||||
yield [
|
||||
'/wiki/',
|
||||
'/w/index.php/Hello?x=y',
|
||||
false
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
54
tests/phpunit/includes/Request/WebResponseTest.php
Normal file
54
tests/phpunit/includes/Request/WebResponseTest.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Request\WebResponse;
|
||||
|
||||
/**
|
||||
* @covers MediaWiki\Request\WebResponse
|
||||
*
|
||||
* @group WebRequest
|
||||
*/
|
||||
class WebResponseTest extends MediaWikiIntegrationTestCase {
|
||||
|
||||
/**
|
||||
* Test that no cookies get set post-send.
|
||||
*/
|
||||
public function testDisableForPostSend() {
|
||||
$response = new WebResponse;
|
||||
$response->disableForPostSend();
|
||||
|
||||
$hookWasRun = false;
|
||||
$this->setTemporaryHook( 'WebResponseSetCookie', static function () use ( &$hookWasRun ) {
|
||||
$hookWasRun = true;
|
||||
return true;
|
||||
} );
|
||||
|
||||
$logger = new TestLogger();
|
||||
$logger->setCollect( true );
|
||||
$this->setLogger( 'cookie', $logger );
|
||||
$this->setLogger( 'header', $logger );
|
||||
|
||||
$response->setCookie( 'TetsCookie', 'foobar' );
|
||||
$response->header( 'TestHeader', 'foobar' );
|
||||
|
||||
$this->assertFalse( $hookWasRun, 'The WebResponseSetCookie hook should not run' );
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
[ 'info', 'ignored post-send cookie {cookie}' ],
|
||||
[ 'info', 'ignored post-send header {header}' ],
|
||||
],
|
||||
$logger->getBuffer()
|
||||
);
|
||||
}
|
||||
|
||||
public function testStatusCode() {
|
||||
$response = new WebResponse();
|
||||
|
||||
$response->statusHeader( 404 );
|
||||
$this->assertSame( 404, $response->getStatusCode() );
|
||||
|
||||
$response->header( 'Test', true, 415 );
|
||||
$this->assertSame( 415, $response->getStatusCode() );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,14 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Action;
|
||||
|
||||
use BadTitleError;
|
||||
use DeferredUpdates;
|
||||
use DeferredUpdatesScopeStack;
|
||||
use MediaWiki\Actions\ActionEntryPoint;
|
||||
use MediaWiki\Deferred\DeferredUpdatesScopeMediaWikiStack;
|
||||
use MediaWiki\MainConfigNames;
|
||||
use MediaWiki\Request\FauxRequest;
|
||||
use MediaWiki\Request\FauxResponse;
|
||||
use MediaWiki\Request\WebRequest;
|
||||
use MediaWiki\Request\WebResponse;
|
||||
use MediaWiki\SpecialPage\SpecialPage;
|
||||
use MediaWiki\Tests\MockEnvironment;
|
||||
use MediaWiki\Title\MalformedTitleException;
|
||||
use MediaWiki\Title\Title;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use ReflectionMethod;
|
||||
use RequestContext;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
use WikiPage;
|
||||
|
||||
// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
|
||||
|
||||
|
|
@ -17,10 +29,6 @@ use Wikimedia\TestingAccessWrapper;
|
|||
* @covers MediaWiki\Actions\ActionEntryPoint
|
||||
*/
|
||||
class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
||||
private ?array $oldServer;
|
||||
private ?array $oldGet;
|
||||
private ?array $oldPost;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
|
|
@ -32,34 +40,41 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
MainConfigNames::ActionPaths => [],
|
||||
MainConfigNames::LanguageCode => 'en',
|
||||
] );
|
||||
|
||||
// phpcs:disable ActionEntryPoint.Usage.SuperGlobalsUsage.SuperGlobals
|
||||
$this->oldServer = $_SERVER;
|
||||
$this->oldGet = $_GET;
|
||||
$this->oldPost = $_POST;
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
$_SERVER = $this->oldServer;
|
||||
$_GET = $this->oldGet;
|
||||
$_POST = $this->oldPost;
|
||||
// The ActionEntryPoint class writes to $wgTitle. Revert any writes done in this test to make
|
||||
// sure that they don't leak into other tests (T341951)
|
||||
$GLOBALS['wgTitle'] = null;
|
||||
|
||||
// Restore a scope stack that will run updates immediately
|
||||
DeferredUpdates::setScopeStack( new DeferredUpdatesScopeMediaWikiStack() );
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MockEnvironment|WebRequest|array|null $environment
|
||||
* @param RequestContext|null $context
|
||||
*
|
||||
* @return ActionEntryPoint
|
||||
*/
|
||||
private function getEntryPoint(): ActionEntryPoint {
|
||||
return new ActionEntryPoint(
|
||||
RequestContext::getMain(),
|
||||
private function getEntryPoint( $environment = null, RequestContext $context = null ) {
|
||||
if ( !$environment ) {
|
||||
$environment = new MockEnvironment();
|
||||
}
|
||||
|
||||
if ( is_array( $environment ) ) {
|
||||
$environment = new FauxRequest( $environment );
|
||||
}
|
||||
|
||||
if ( $environment instanceof WebRequest ) {
|
||||
$environment = new MockEnvironment( $environment );
|
||||
}
|
||||
|
||||
$entryPoint = new ActionEntryPoint(
|
||||
$context ?? $environment->makeFauxContext(),
|
||||
$environment,
|
||||
$this->getServiceContainer()
|
||||
);
|
||||
$entryPoint->establishOutputBufferLevel();
|
||||
|
||||
return $entryPoint;
|
||||
}
|
||||
|
||||
public static function provideTryNormaliseRedirect() {
|
||||
|
|
@ -186,22 +201,16 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
// Set SERVER because interpolateTitle() doesn't use getRequestURL(),
|
||||
// whereas tryNormaliseRedirect does(). Also, using WebRequest allows
|
||||
// us to test some quirks in that class.
|
||||
$_SERVER['REQUEST_URI'] = $url;
|
||||
$_POST = [];
|
||||
$_GET = $query;
|
||||
$req = new WebRequest;
|
||||
|
||||
// This adds a virtual 'title' query parameter. Normally called from Setup.php
|
||||
$req->interpolateTitle();
|
||||
$environment = new MockEnvironment();
|
||||
$environment->setRequestInfo( $url, $query );
|
||||
|
||||
$titleObj = Title::newFromText( $title );
|
||||
|
||||
// Set global context since some involved code paths don't yet have context
|
||||
$context = RequestContext::getMain();
|
||||
$context->setRequest( $req );
|
||||
$context = $environment->makeFauxContext();
|
||||
$context->setTitle( $titleObj );
|
||||
|
||||
$mw = new ActionEntryPoint( $context, $this->getServiceContainer() );
|
||||
$mw = $this->getEntryPoint( $environment, $context );
|
||||
|
||||
$method = new ReflectionMethod( $mw, 'tryNormaliseRedirect' );
|
||||
$method->setAccessible( true );
|
||||
|
|
@ -286,7 +295,7 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
$req = new FauxRequest( $query );
|
||||
$mw = $this->getEntryPoint();
|
||||
$mw = $this->getEntryPoint( $req );
|
||||
|
||||
$method = new ReflectionMethod( $mw, 'parseTitle' );
|
||||
$method->setAccessible( true );
|
||||
|
|
@ -339,7 +348,10 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
// opportunistic updates.
|
||||
DeferredUpdates::setScopeStack( new DeferredUpdatesScopeStack() );
|
||||
|
||||
$response = new WebResponse;
|
||||
$mw = TestingAccessWrapper::newFromObject( $this->getEntryPoint() );
|
||||
|
||||
/** @var FauxResponse $response */
|
||||
$response = $mw->getResponse();
|
||||
|
||||
// A update that attempts to set a cookie
|
||||
$jobHasRun = false;
|
||||
|
|
@ -349,19 +361,8 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
$response->header( 'Foo: baz' );
|
||||
} );
|
||||
|
||||
$hookWasRun = false;
|
||||
$this->setTemporaryHook( 'WebResponseSetCookie', static function () use ( &$hookWasRun ) {
|
||||
$hookWasRun = true;
|
||||
return true;
|
||||
} );
|
||||
|
||||
$logger = new TestLogger();
|
||||
$logger->setCollect( true );
|
||||
$this->setLogger( 'cookie', $logger );
|
||||
$this->setLogger( 'header', $logger );
|
||||
|
||||
$mw = TestingAccessWrapper::newFromObject( $this->getEntryPoint() );
|
||||
$mw->doPostOutputShutdown();
|
||||
|
||||
// restInPeace() might have been registered to a callback of
|
||||
// register_postsend_function() and thus can not be triggered from
|
||||
// PHPUnit.
|
||||
|
|
@ -370,15 +371,8 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
$this->assertTrue( $jobHasRun, 'post-send job has run' );
|
||||
$this->assertFalse( $hookWasRun,
|
||||
'post-send job must not trigger WebResponseSetCookie hook' );
|
||||
$this->assertEquals(
|
||||
[
|
||||
[ 'info', 'ignored post-send cookie {cookie}' ],
|
||||
[ 'info', 'ignored post-send header {header}' ],
|
||||
],
|
||||
$logger->getBuffer()
|
||||
);
|
||||
$this->assertNull( $response->getCookie( 'JobCookie' ) );
|
||||
$this->assertNull( $response->getHeader( 'Foo' ) );
|
||||
}
|
||||
|
||||
public function testInvalidRedirectingOnSpecialPageWithPersonallyIdentifiableTarget() {
|
||||
|
|
@ -388,17 +382,30 @@ class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
|
|||
$req = new FauxRequest( [
|
||||
'title' => $specialTitle->getPrefixedDbKey(),
|
||||
] );
|
||||
$req->setRequestURL( $specialTitle->getFullUrl() );
|
||||
$req->setRequestURL( $specialTitle->getLinkURL() );
|
||||
|
||||
$context = new RequestContext();
|
||||
$context->setRequest( $req );
|
||||
$env = new MockEnvironment( $req );
|
||||
$context = $env->makeFauxContext();
|
||||
$context->setTitle( $specialTitle );
|
||||
|
||||
$mw = TestingAccessWrapper::newFromObject( new ActionEntryPoint( $context, $this->getServiceContainer() ) );
|
||||
$mw = TestingAccessWrapper::newFromObject( $this->getEntryPoint( $env, $context ) );
|
||||
|
||||
$this->expectException( BadTitleError::class );
|
||||
$this->expectExceptionMessage( 'The requested page title contains invalid characters: "<".' );
|
||||
$mw->performRequest();
|
||||
}
|
||||
|
||||
public function testView() {
|
||||
$page = $this->getExistingTestPage();
|
||||
|
||||
$request = new FauxRequest( [ 'title' => $page->getTitle()->getPrefixedDBkey() ] );
|
||||
$env = new MockEnvironment( $request );
|
||||
|
||||
$entryPoint = $this->getEntryPoint( $env );
|
||||
$entryPoint->run();
|
||||
|
||||
$expected = '<title>(pagetitle: ' . $page->getTitle()->getPrefixedText();
|
||||
Assert::assertStringContainsString( $expected, $entryPoint->captureOutput() );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
152
tests/phpunit/mocks/MockEnvironment.php
Normal file
152
tests/phpunit/mocks/MockEnvironment.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests;
|
||||
|
||||
use Exception;
|
||||
use HashConfig;
|
||||
use MediaWiki\EntryPointEnvironment;
|
||||
use MediaWiki\Request\FauxRequest;
|
||||
use MediaWiki\Request\FauxResponse;
|
||||
use MultiConfig;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use RequestContext;
|
||||
|
||||
/**
|
||||
* @internal For testing MediaWikiEntryPoint subclasses.
|
||||
* Should be revised before wider use.
|
||||
*/
|
||||
class MockEnvironment extends EntryPointEnvironment {
|
||||
|
||||
public const MOCK_REQUEST_URL = '/just/a/test';
|
||||
|
||||
private ?FauxRequest $request = null;
|
||||
|
||||
private array $serverInfo = [];
|
||||
|
||||
public function __construct( ?FauxRequest $request = null ) {
|
||||
if ( $request ) {
|
||||
if ( !$request->hasRequestURL() ) {
|
||||
$request->setRequestURL( self::MOCK_REQUEST_URL );
|
||||
}
|
||||
|
||||
// Note that setRequestInfo() will reset $this->request to null
|
||||
$this->setRequestInfo(
|
||||
$request->getRequestURL(),
|
||||
$request->getQueryValuesOnly(),
|
||||
$request->getMethod()
|
||||
);
|
||||
}
|
||||
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function setRequestInfo( string $requestUrl, $params = '', $method = 'GET' ) {
|
||||
$this->request = null;
|
||||
|
||||
$this->setServerInfo(
|
||||
'REQUEST_URI',
|
||||
$requestUrl
|
||||
);
|
||||
$this->setServerInfo(
|
||||
'REQUEST_METHOD',
|
||||
$method
|
||||
);
|
||||
$this->setServerInfo(
|
||||
'QUERY_STRING',
|
||||
is_string( $params ) ? $params : wfArrayToCgi( $params )
|
||||
);
|
||||
}
|
||||
|
||||
public function getFauxRequest(): FauxRequest {
|
||||
if ( !$this->request ) {
|
||||
$data = wfCgiToArray( $this->getServerInfo( 'QUERY_STRING', '' ) );
|
||||
$wasPosted = $this->getServerInfo( 'REQUEST_METHOD', 'GET' ) === 'POST';
|
||||
$requestUrl = $this->getServerInfo( 'REQUEST_URI' ) ?? self::MOCK_REQUEST_URL;
|
||||
|
||||
$request = new FauxRequest( $data, $wasPosted );
|
||||
$request->setServerInfo( $this->serverInfo );
|
||||
$request->setRequestURL( $requestUrl );
|
||||
|
||||
// This adds a virtual 'title' query parameter. Normally called from Setup.php
|
||||
$request->interpolateTitle();
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getFauxResponse(): FauxResponse {
|
||||
return $this->getFauxRequest()->response();
|
||||
}
|
||||
|
||||
public function makeFauxContext( array $config = [] ): RequestContext {
|
||||
$context = new RequestContext();
|
||||
$context->setRequest( $this->getFauxRequest() );
|
||||
$context->setLanguage( 'qqx' );
|
||||
$context->setConfig( new MultiConfig( [
|
||||
new HashConfig( $config ),
|
||||
$context->getConfig()
|
||||
] ) );
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
public function isCli(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasFastCgi(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function fastCgiFinishRequest(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setServerInfo( string $key, $value ) {
|
||||
$this->serverInfo[$key] = $value;
|
||||
}
|
||||
|
||||
public function getServerInfo( string $key, $default = null ) {
|
||||
return $this->serverInfo[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function exit( int $code = 0 ) {
|
||||
throw new Exception( $code );
|
||||
}
|
||||
|
||||
public function disableModDeflate(): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public function getEnv( string $name ) {
|
||||
// Implement when needed.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getIni( string $name ) {
|
||||
// Implement when needed.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setIniOption( string $name, $value ) {
|
||||
// Implement when needed.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function assertStatusCode( int $expected, $message = null ) {
|
||||
$message ??= "HTTP status";
|
||||
Assert::assertSame( $expected, $this->getFauxResponse()->getStatusCode(), $message );
|
||||
}
|
||||
|
||||
public function assertHeaderValue( ?string $expected, string $name, $message = null ) {
|
||||
$message ??= "$name header";
|
||||
Assert::assertSame( $expected, $this->getFauxResponse()->getHeader( $name ), $message );
|
||||
}
|
||||
|
||||
public function assertCookieValue( ?string $expected, string $name, $message = null ) {
|
||||
$message ??= "$name header";
|
||||
Assert::assertSame( $expected, $this->getFauxResponse()->getCookie( $name ), $message );
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue