Remove old HTTP request implementations
Since 1.34 setting non-default HTTP engine has been deprecated. It's time to remove the old implementations. Only Guzzle is now available. Change-Id: I978b75827e69db02cbc027fe0b89a028adfc6820
This commit is contained in:
parent
1c009126c5
commit
928f707731
13 changed files with 14 additions and 765 deletions
|
|
@ -209,6 +209,9 @@ because of Phabricator reports.
|
|||
- ::changeableByGroup,
|
||||
- ::changeableGroups,
|
||||
- ::isAllowUsertalk
|
||||
* Http::$httpEngine, deprecated since 1.34, has been removed. The only available
|
||||
HTTP engine is now Guzzle. CurlHttpRequest and PhpHttpRequest classes were
|
||||
removed.
|
||||
* …
|
||||
|
||||
=== Deprecations in 1.38 ===
|
||||
|
|
|
|||
|
|
@ -315,7 +315,6 @@ $wgAutoloadLocalClasses = [
|
|||
'CssContent' => __DIR__ . '/includes/content/CssContent.php',
|
||||
'CssContentHandler' => __DIR__ . '/includes/content/CssContentHandler.php',
|
||||
'CsvStatsOutput' => __DIR__ . '/maintenance/language/StatOutputs.php',
|
||||
'CurlHttpRequest' => __DIR__ . '/includes/http/CurlHttpRequest.php',
|
||||
'CustomUppercaseCollation' => __DIR__ . '/includes/collation/CustomUppercaseCollation.php',
|
||||
'DBAccessBase' => __DIR__ . '/includes/dao/DBAccessBase.php',
|
||||
'DBAccessError' => __DIR__ . '/includes/libs/rdbms/exception/DBAccessError.php',
|
||||
|
|
@ -1225,7 +1224,6 @@ $wgAutoloadLocalClasses = [
|
|||
'Pbkdf2Password' => __DIR__ . '/includes/password/Pbkdf2Password.php',
|
||||
'PerRowAugmentor' => __DIR__ . '/includes/search/PerRowAugmentor.php',
|
||||
'PermissionsError' => __DIR__ . '/includes/exception/PermissionsError.php',
|
||||
'PhpHttpRequest' => __DIR__ . '/includes/http/PhpHttpRequest.php',
|
||||
'Pingback' => __DIR__ . '/includes/Pingback.php',
|
||||
'PoolCounter' => __DIR__ . '/includes/poolcounter/PoolCounter.php',
|
||||
'PoolCounterNull' => __DIR__ . '/includes/poolcounter/PoolCounterNull.php',
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
<?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
|
||||
*/
|
||||
|
||||
/**
|
||||
* MWHttpRequest implemented using internal curl compiled into PHP
|
||||
*/
|
||||
class CurlHttpRequest extends MWHttpRequest {
|
||||
public const SUPPORTS_FILE_POSTS = true;
|
||||
|
||||
protected $curlOptions = [];
|
||||
protected $headerText = "";
|
||||
|
||||
/**
|
||||
* @internal Use HttpRequestFactory
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( !function_exists( 'curl_init' ) ) {
|
||||
throw new RuntimeException(
|
||||
__METHOD__ . ': curl (https://www.php.net/curl) is not installed' );
|
||||
}
|
||||
|
||||
parent::__construct( ...func_get_args() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $fh
|
||||
* @param string $content
|
||||
* @return int
|
||||
*/
|
||||
protected function readHeader( $fh, $content ) {
|
||||
$this->headerText .= $content;
|
||||
return strlen( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MWHttpRequest::execute
|
||||
*
|
||||
* @throws MWException
|
||||
* @return Status
|
||||
*/
|
||||
public function execute() {
|
||||
$this->prepare();
|
||||
|
||||
if ( !$this->status->isOK() ) {
|
||||
return Status::wrap( $this->status ); // TODO B/C; move this to callers
|
||||
}
|
||||
|
||||
$this->curlOptions[CURLOPT_PROXY] = $this->proxy;
|
||||
$this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
|
||||
$this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000;
|
||||
$this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
|
||||
$this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
|
||||
$this->curlOptions[CURLOPT_HEADERFUNCTION] = [ $this, "readHeader" ];
|
||||
$this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
|
||||
$this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
|
||||
|
||||
$this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
|
||||
|
||||
$this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0;
|
||||
$this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
|
||||
|
||||
if ( $this->caInfo ) {
|
||||
$this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
|
||||
}
|
||||
|
||||
if ( $this->headersOnly ) {
|
||||
$this->curlOptions[CURLOPT_NOBODY] = true;
|
||||
$this->curlOptions[CURLOPT_HEADER] = true;
|
||||
} elseif ( $this->method == 'POST' ) {
|
||||
$this->curlOptions[CURLOPT_POST] = true;
|
||||
$postData = $this->postData;
|
||||
// Don't interpret POST parameters starting with '@' as file uploads, because this
|
||||
// makes it impossible to POST plain values starting with '@' (and causes security
|
||||
// issues potentially exposing the contents of local files).
|
||||
$this->curlOptions[CURLOPT_SAFE_UPLOAD] = true;
|
||||
$this->curlOptions[CURLOPT_POSTFIELDS] = $postData;
|
||||
|
||||
// Suppress 'Expect: 100-continue' header, as some servers
|
||||
// will reject it with a 417 and Curl won't auto retry
|
||||
// with HTTP 1.0 fallback
|
||||
$this->reqHeaders['Expect'] = '';
|
||||
} else {
|
||||
$this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
|
||||
}
|
||||
|
||||
$this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
|
||||
|
||||
$curlHandle = curl_init( $this->url );
|
||||
|
||||
if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
|
||||
$this->status->fatal( 'http-internal-error' );
|
||||
throw new InvalidArgumentException( "Error setting curl options." );
|
||||
}
|
||||
|
||||
if ( $this->followRedirects && $this->canFollowRedirects() ) {
|
||||
Wikimedia\suppressWarnings();
|
||||
if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
|
||||
$this->logger->debug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " .
|
||||
"Probably open_basedir is set." );
|
||||
// Continue the processing. If it were in curl_setopt_array,
|
||||
// processing would have halted on its entry
|
||||
}
|
||||
Wikimedia\restoreWarnings();
|
||||
}
|
||||
|
||||
if ( $this->profiler ) {
|
||||
$profileSection = $this->profiler->scopedProfileIn(
|
||||
__METHOD__ . '-' . $this->profileName
|
||||
);
|
||||
}
|
||||
|
||||
$curlRes = curl_exec( $curlHandle );
|
||||
if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) {
|
||||
$this->status->fatal( 'http-timed-out', $this->url );
|
||||
} elseif ( $curlRes === false ) {
|
||||
$this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
|
||||
} else {
|
||||
$this->headerList = explode( "\r\n", $this->headerText );
|
||||
}
|
||||
|
||||
curl_close( $curlHandle );
|
||||
|
||||
if ( $this->profiler ) {
|
||||
$this->profiler->scopedProfileOut( $profileSection );
|
||||
}
|
||||
|
||||
$this->parseHeader();
|
||||
$this->setStatus();
|
||||
|
||||
return Status::wrap( $this->status ); // TODO B/C; move this to callers
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function canFollowRedirects() {
|
||||
$curlVersionInfo = curl_version();
|
||||
if ( $curlVersionInfo['version_number'] < 0x071304 ) {
|
||||
$this->logger->debug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037" );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,15 +27,14 @@ use Psr\Http\Message\RequestInterface;
|
|||
/**
|
||||
* MWHttpRequest implemented using the Guzzle library
|
||||
*
|
||||
* Differences from the CurlHttpRequest implementation:
|
||||
* 1) a new 'sink' option is available as an alternative to callbacks. See:
|
||||
* http://docs.guzzlephp.org/en/stable/request-options.html#sink)
|
||||
* The 'callback' option remains available as well. If both 'sink' and 'callback' are
|
||||
* specified, 'sink' is used.
|
||||
* 2) callers may set a custom handler via the 'handler' option.
|
||||
* If this is not set, Guzzle will use curl (if available) or PHP streams (otherwise)
|
||||
* 3) setting either sslVerifyHost or sslVerifyCert will enable both. Guzzle does not allow
|
||||
* them to be set separately.
|
||||
* @note a new 'sink' option is available as an alternative to callbacks.
|
||||
* See: http://docs.guzzlephp.org/en/stable/request-options.html#sink)
|
||||
* The 'callback' option remains available as well. If both 'sink' and 'callback' are
|
||||
* specified, 'sink' is used.
|
||||
* @note Callers may set a custom handler via the 'handler' option.
|
||||
* If this is not set, Guzzle will use curl (if available) or PHP streams (otherwise)
|
||||
* @note Setting either sslVerifyHost or sslVerifyCert will enable both.
|
||||
* Guzzle does not allow them to be set separately.
|
||||
*
|
||||
* @since 1.33
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ use MediaWiki\MediaWikiServices;
|
|||
* @ingroup HTTP
|
||||
*/
|
||||
class Http {
|
||||
/** @deprecated since 1.34, just use the default engine */
|
||||
public static $httpEngine = null;
|
||||
|
||||
/**
|
||||
* Perform an HTTP request
|
||||
*
|
||||
|
|
|
|||
|
|
@ -19,18 +19,14 @@
|
|||
*/
|
||||
namespace MediaWiki\Http;
|
||||
|
||||
use CurlHttpRequest;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttpRequest;
|
||||
use Http;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MultiHttpClient;
|
||||
use MWHttpRequest;
|
||||
use PhpHttpRequest;
|
||||
use Profiler;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Status;
|
||||
|
||||
/**
|
||||
|
|
@ -90,15 +86,10 @@ class HttpRequestFactory {
|
|||
* @phpcs:ignore Generic.Files.LineLength
|
||||
* @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:string|array,proxy?:?string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:?string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:\WebRequest|array{ip:string,userAgent:string}} $options
|
||||
* @param string $caller The method making this request, for profiling
|
||||
* @throws RuntimeException
|
||||
* @return MWHttpRequest
|
||||
* @see MWHttpRequest::__construct
|
||||
*/
|
||||
public function create( $url, array $options = [], $caller = __METHOD__ ) {
|
||||
if ( !Http::$httpEngine ) {
|
||||
Http::$httpEngine = 'guzzle';
|
||||
}
|
||||
|
||||
if ( !isset( $options['logger'] ) ) {
|
||||
$options['logger'] = $this->logger;
|
||||
}
|
||||
|
|
@ -115,16 +106,7 @@ class HttpRequestFactory {
|
|||
$this->options->get( 'HTTPMaxConnectTimeout' )
|
||||
);
|
||||
|
||||
switch ( Http::$httpEngine ) {
|
||||
case 'guzzle':
|
||||
return new GuzzleHttpRequest( $url, $options, $caller, Profiler::instance() );
|
||||
case 'curl':
|
||||
return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
|
||||
case 'php':
|
||||
return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
|
||||
default:
|
||||
throw new RuntimeException( __METHOD__ . ': The requested engine is not valid.' );
|
||||
}
|
||||
return new GuzzleHttpRequest( $url, $options, $caller, Profiler::instance() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,243 +0,0 @@
|
|||
<?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
|
||||
*/
|
||||
|
||||
class PhpHttpRequest extends MWHttpRequest {
|
||||
|
||||
private $fopenErrors = [];
|
||||
|
||||
/**
|
||||
* @internal Use HttpRequestFactory
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
|
||||
throw new RuntimeException( __METHOD__ . ': allow_url_fopen needs to be enabled for ' .
|
||||
'pure PHP http requests to work. If possible, curl should be used instead. See ' .
|
||||
'https://www.php.net/curl.'
|
||||
);
|
||||
}
|
||||
|
||||
parent::__construct( ...func_get_args() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
protected function urlToTcp( $url ) {
|
||||
$parsedUrl = parse_url( $url );
|
||||
|
||||
return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with a 'capath' or 'cafile' key
|
||||
* that is suitable to be merged into the 'ssl' sub-array of
|
||||
* a stream context options array.
|
||||
* Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
|
||||
* default CA bundle if PHP supports that, or searches a few standard locations.
|
||||
* @return array
|
||||
* @throws DomainException
|
||||
*/
|
||||
protected function getCertOptions() {
|
||||
$certOptions = [];
|
||||
$certLocations = [];
|
||||
if ( $this->caInfo ) {
|
||||
$certLocations = [ 'manual' => $this->caInfo ];
|
||||
}
|
||||
|
||||
foreach ( $certLocations as $key => $cert ) {
|
||||
if ( is_dir( $cert ) ) {
|
||||
$certOptions['capath'] = $cert;
|
||||
break;
|
||||
} elseif ( is_file( $cert ) ) {
|
||||
$certOptions['cafile'] = $cert;
|
||||
break;
|
||||
} elseif ( $key === 'manual' ) {
|
||||
// fail more loudly if a cert path was manually configured and it is not valid
|
||||
throw new DomainException( "Invalid CA info passed: $cert" );
|
||||
}
|
||||
}
|
||||
|
||||
return $certOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error handler for dealing with fopen() errors.
|
||||
* fopen() tends to fire multiple errors in succession, and the last one
|
||||
* is completely useless (something like "fopen: failed to open stream")
|
||||
* so normal methods of handling errors programmatically
|
||||
* like get_last_error() don't work.
|
||||
* @internal
|
||||
* @param int $errno
|
||||
* @param string $errstr
|
||||
*/
|
||||
public function errorHandler( $errno, $errstr ) {
|
||||
$n = count( $this->fopenErrors ) + 1;
|
||||
$this->fopenErrors += [ "errno$n" => $errno, "errstr$n" => $errstr ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MWHttpRequest::execute
|
||||
*
|
||||
* @return Status
|
||||
*/
|
||||
public function execute() {
|
||||
$this->prepare();
|
||||
|
||||
if ( is_array( $this->postData ) ) {
|
||||
$this->postData = wfArrayToCgi( $this->postData );
|
||||
}
|
||||
|
||||
if ( $this->parsedUrl['scheme'] != 'http'
|
||||
&& $this->parsedUrl['scheme'] != 'https' ) {
|
||||
$this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
|
||||
}
|
||||
|
||||
$this->reqHeaders['Accept'] = "*/*";
|
||||
$this->reqHeaders['Connection'] = 'Close';
|
||||
if ( $this->method == 'POST' ) {
|
||||
// Required for HTTP 1.0 POSTs
|
||||
$this->reqHeaders['Content-Length'] = strlen( $this->postData );
|
||||
if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
|
||||
$this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded";
|
||||
}
|
||||
}
|
||||
|
||||
// Set up PHP stream context
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => $this->method,
|
||||
'header' => implode( "\r\n", $this->getHeaderList() ),
|
||||
'protocol_version' => '1.1',
|
||||
'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0,
|
||||
'ignore_errors' => true,
|
||||
'timeout' => $this->timeout,
|
||||
// Curl options in case curlwrappers are installed
|
||||
'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0,
|
||||
'curl_verify_ssl_peer' => $this->sslVerifyCert,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => $this->sslVerifyCert,
|
||||
'SNI_enabled' => true,
|
||||
'ciphers' => 'HIGH:!SSLv2:!SSLv3:-ADH:-kDH:-kECDH:-DSS',
|
||||
'disable_compression' => true,
|
||||
],
|
||||
];
|
||||
|
||||
if ( $this->proxy ) {
|
||||
$options['http']['proxy'] = $this->urlToTcp( $this->proxy );
|
||||
$options['http']['request_fulluri'] = true;
|
||||
}
|
||||
|
||||
if ( $this->postData ) {
|
||||
$options['http']['content'] = $this->postData;
|
||||
}
|
||||
|
||||
if ( $this->sslVerifyHost ) {
|
||||
$options['ssl']['peer_name'] = $this->parsedUrl['host'];
|
||||
}
|
||||
|
||||
$options['ssl'] += $this->getCertOptions();
|
||||
|
||||
$context = stream_context_create( $options );
|
||||
|
||||
$this->headerList = [];
|
||||
$reqCount = 0;
|
||||
$url = $this->url;
|
||||
|
||||
$result = [];
|
||||
|
||||
if ( $this->profiler ) {
|
||||
$profileSection = $this->profiler->scopedProfileIn(
|
||||
__METHOD__ . '-' . $this->profileName
|
||||
);
|
||||
}
|
||||
do {
|
||||
$reqCount++;
|
||||
$this->fopenErrors = [];
|
||||
set_error_handler( [ $this, 'errorHandler' ] );
|
||||
$fh = fopen( $url, "r", false, $context );
|
||||
restore_error_handler();
|
||||
|
||||
if ( !$fh ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$result = stream_get_meta_data( $fh );
|
||||
$this->headerList = $result['wrapper_data'];
|
||||
$this->parseHeader();
|
||||
|
||||
if ( !$this->followRedirects ) {
|
||||
break;
|
||||
}
|
||||
|
||||
# Handle manual redirection
|
||||
if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
|
||||
break;
|
||||
}
|
||||
# Check security of URL
|
||||
$url = $this->getResponseHeader( "Location" );
|
||||
|
||||
if ( !Http::isValidURI( $url ) ) {
|
||||
$this->logger->debug( __METHOD__ . ": insecure redirection" );
|
||||
break;
|
||||
}
|
||||
} while ( true );
|
||||
if ( $this->profiler ) {
|
||||
$this->profiler->scopedProfileOut( $profileSection );
|
||||
}
|
||||
|
||||
$this->setStatus();
|
||||
|
||||
if ( $fh === false ) {
|
||||
if ( $this->fopenErrors ) {
|
||||
$this->logger->warning( __CLASS__
|
||||
. ': error opening connection: {errstr1}', $this->fopenErrors );
|
||||
}
|
||||
$this->status->fatal( 'http-request-error' );
|
||||
return Status::wrap( $this->status ); // TODO B/C; move this to callers
|
||||
}
|
||||
|
||||
if ( $result['timed_out'] ) {
|
||||
$this->status->fatal( 'http-timed-out', $this->url );
|
||||
return Status::wrap( $this->status ); // TODO B/C; move this to callers
|
||||
}
|
||||
|
||||
// If everything went OK, or we received some error code
|
||||
// get the response body content.
|
||||
if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) {
|
||||
while ( !feof( $fh ) ) {
|
||||
$buf = fread( $fh, 8192 );
|
||||
|
||||
if ( $buf === false ) {
|
||||
$this->status->fatal( 'http-read-error' );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $buf !== '' ) {
|
||||
call_user_func( $this->callback, $fh, $buf );
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose( $fh );
|
||||
|
||||
return Status::wrap( $this->status ); // TODO B/C; move this to callers
|
||||
}
|
||||
}
|
||||
|
|
@ -510,8 +510,7 @@ class MultiHttpClient implements LoggerAwareInterface {
|
|||
* Execute a set of HTTP(S) requests sequentially.
|
||||
*
|
||||
* @see MultiHttpClient::runMulti()
|
||||
* @todo Remove dependency on MediaWikiServices: use a separate HTTP client
|
||||
* library or copy code from PhpHttpRequest
|
||||
* @todo Remove dependency on MediaWikiServices: rewrite using Guzzle T202352
|
||||
* @param array $reqs Map of HTTP request arrays
|
||||
* @phpcs:ignore Generic.Files.LineLength
|
||||
* @phan-param array<int,array{url:string,query:array,method:string,body:string,proxy?:?string,headers?:string[]}> $reqs
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ $wgAutoloadClasses += [
|
|||
# tests/common
|
||||
'TestSetup' => "$testDir/common/TestSetup.php",
|
||||
|
||||
# tests/integration
|
||||
'MWHttpRequestTestCase' => "$testDir/integration/includes/http/MWHttpRequestTestCase.php",
|
||||
|
||||
# tests/exception
|
||||
'TestThrowerDummy' => "$testDir/phpunit/data/exception/TestThrowerDummy.php",
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group large
|
||||
* @covers CurlHttpRequest
|
||||
*/
|
||||
class CurlHttpRequestTest extends MWHttpRequestTestCase {
|
||||
protected static $httpEngine = 'curl';
|
||||
}
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Http\HttpRequestFactory;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
|
||||
abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
|
||||
protected static $httpEngine;
|
||||
protected $oldHttpEngine;
|
||||
|
||||
/** @var HttpRequestFactory */
|
||||
private $factory;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->oldHttpEngine = Http::$httpEngine;
|
||||
Http::$httpEngine = static::$httpEngine;
|
||||
|
||||
$this->factory = MediaWikiServices::getInstance()->getHttpRequestFactory();
|
||||
|
||||
try {
|
||||
$request = $factory->create( 'null:' );
|
||||
} catch ( RuntimeException $e ) {
|
||||
$this->markTestSkipped( static::$httpEngine . ' engine not supported' );
|
||||
}
|
||||
|
||||
if ( static::$httpEngine === 'php' ) {
|
||||
$this->assertInstanceOf( PhpHttpRequest::class, $request );
|
||||
} else {
|
||||
$this->assertInstanceOf( CurlHttpRequest::class, $request );
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
Http::$httpEngine = $this->oldHttpEngine;
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testIsRedirect() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/get' );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertFalse( $request->isRedirect() );
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/redirect/1' );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertTrue( $request->isRedirect() );
|
||||
}
|
||||
|
||||
public function testgetFinalUrl() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/redirect/3' );
|
||||
if ( !$request->canFollowRedirects() ) {
|
||||
$this->markTestSkipped( 'cannot follow redirects' );
|
||||
}
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
|
||||
=> true ] );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
|
||||
$this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
|
||||
=> true ] );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
|
||||
$this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
|
||||
|
||||
if ( static::$httpEngine === 'curl' ) {
|
||||
$this->markTestIncomplete( 'maxRedirects seems to be ignored by CurlHttpRequest' );
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
|
||||
=> true, 'maxRedirects' => 1 ] );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
|
||||
}
|
||||
|
||||
public function testSetCookie() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/cookies' );
|
||||
$request->setCookie( 'foo', 'bar' );
|
||||
$request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
|
||||
}
|
||||
|
||||
public function testSetCookieJar() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/cookies' );
|
||||
$cookieJar = new CookieJar();
|
||||
$cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
|
||||
$cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
|
||||
$request->setCookieJar( $cookieJar );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/cookies/set?foo=bar' );
|
||||
$cookieJar = new CookieJar();
|
||||
$request->setCookieJar( $cookieJar );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertHasCookie( 'foo', 'bar', $request->getCookieJar() );
|
||||
|
||||
$this->markTestIncomplete( 'CookieJar does not handle deletion' );
|
||||
|
||||
// $request = $this->factory->create( 'http://httpbin.org/cookies/delete?foo' );
|
||||
// $cookieJar = new CookieJar();
|
||||
// $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
|
||||
// $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] );
|
||||
// $request->setCookieJar( $cookieJar );
|
||||
// $status = $request->execute();
|
||||
// $this->assertTrue( $status->isGood() );
|
||||
// $this->assertNotHasCookie( 'foo', $request->getCookieJar() );
|
||||
// $this->assertHasCookie( 'foo2', 'bar2', $request->getCookieJar() );
|
||||
}
|
||||
|
||||
public function testGetResponseHeaders() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/response-headers?Foo=bar' );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER );
|
||||
$this->assertArrayHasKey( 'foo', $headers );
|
||||
$this->assertSame( 'bar', $request->getResponseHeader( 'Foo' ) );
|
||||
}
|
||||
|
||||
public function testSetHeader() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/headers' );
|
||||
$request->setHeader( 'Foo', 'bar' );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertResponseFieldValue( [ 'headers', 'Foo' ], 'bar', $request );
|
||||
}
|
||||
|
||||
public function testGetStatus() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/status/418' );
|
||||
$status = $request->execute();
|
||||
$this->assertFalse( $status->isOK() );
|
||||
$this->assertSame( 418, $request->getStatus() );
|
||||
}
|
||||
|
||||
public function testSetUserAgent() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/user-agent' );
|
||||
$request->setUserAgent( 'foo' );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertResponseFieldValue( 'user-agent', 'foo', $request );
|
||||
}
|
||||
|
||||
public function testSetData() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/post', [ 'method' => 'POST' ] );
|
||||
$request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertResponseFieldValue( 'form', [ 'foo' => 'bar', 'foo2' => 'bar2' ], $request );
|
||||
}
|
||||
|
||||
public function testSetCallback() {
|
||||
if ( static::$httpEngine === 'php' ) {
|
||||
$this->markTestIncomplete( 'PhpHttpRequest does not use setCallback()' );
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/ip' );
|
||||
$data = '';
|
||||
$request->setCallback( static function ( $fh, $content ) use ( &$data ) {
|
||||
$data .= $content;
|
||||
return strlen( $content );
|
||||
} );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$data = json_decode( $data, true );
|
||||
$this->assertIsArray( $data );
|
||||
$this->assertArrayHasKey( 'origin', $data );
|
||||
}
|
||||
|
||||
public function testBasicAuthentication() {
|
||||
$request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [
|
||||
'username' => 'user',
|
||||
'password' => 'pass',
|
||||
] );
|
||||
$status = $request->execute();
|
||||
$this->assertTrue( $status->isGood() );
|
||||
$this->assertResponseFieldValue( 'authenticated', true, $request );
|
||||
|
||||
$request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [
|
||||
'username' => 'user',
|
||||
'password' => 'wrongpass',
|
||||
] );
|
||||
$status = $request->execute();
|
||||
$this->assertFalse( $status->isOK() );
|
||||
$this->assertSame( 401, $request->getStatus() );
|
||||
}
|
||||
|
||||
public function testFactoryDefaults() {
|
||||
$request = $this->factory->create( 'http://acme.test' );
|
||||
$this->assertInstanceOf( MWHttpRequest::class, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the request was successful, returned valid JSON and the given field of that
|
||||
* JSON data is as expected.
|
||||
* @param string|string[] $key Path to the data in the response object
|
||||
* @param mixed $expectedValue
|
||||
* @param MWHttpRequest $response
|
||||
*/
|
||||
protected function assertResponseFieldValue( $key, $expectedValue, MWHttpRequest $response ) {
|
||||
$this->assertSame( 200, $response->getStatus(), 'response status is not 200' );
|
||||
$data = json_decode( $response->getContent(), true );
|
||||
$this->assertIsArray( $data, 'response is not JSON' );
|
||||
$keyPath = '';
|
||||
foreach ( (array)$key as $keySegment ) {
|
||||
$keyPath .= ( $keyPath ? '.' : '' ) . $keySegment;
|
||||
$this->assertArrayHasKey( $keySegment, $data, $keyPath . ' not found' );
|
||||
$data = $data[$keySegment];
|
||||
}
|
||||
$this->assertSame( $expectedValue, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cookie jar has the given cookie with the given value.
|
||||
* @param string $expectedName Cookie name
|
||||
* @param string $expectedValue Cookie value
|
||||
* @param CookieJar $cookieJar
|
||||
*/
|
||||
protected function assertHasCookie( $expectedName, $expectedValue, CookieJar $cookieJar ) {
|
||||
$cookieJar = TestingAccessWrapper::newFromObject( $cookieJar );
|
||||
$cookies = array_change_key_case( $cookieJar->cookie, CASE_LOWER );
|
||||
$this->assertArrayHasKey( strtolower( $expectedName ), $cookies );
|
||||
$cookie = TestingAccessWrapper::newFromObject(
|
||||
$cookies[strtolower( $expectedName )] );
|
||||
$this->assertSame( $expectedValue, $cookie->value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the cookie jar does not have the given cookie.
|
||||
* @param string $name Cookie name
|
||||
* @param CookieJar $cookieJar
|
||||
*/
|
||||
protected function assertNotHasCookie( $name, CookieJar $cookieJar ) {
|
||||
$cookieJar = TestingAccessWrapper::newFromObject( $cookieJar );
|
||||
$this->assertArrayNotHasKey( strtolower( $name ),
|
||||
array_change_key_case( $cookieJar->cookie, CASE_LOWER ) );
|
||||
}
|
||||
|
||||
public static function provideRelativeRedirects() {
|
||||
return [
|
||||
[
|
||||
'location' => [ 'http://newsite/file.ext', '/newfile.ext' ],
|
||||
'final' => 'http://newsite/newfile.ext',
|
||||
'Relative file path Location: interpreted as full URL'
|
||||
],
|
||||
[
|
||||
'location' => [ 'https://oldsite/file.ext' ],
|
||||
'final' => 'https://oldsite/file.ext',
|
||||
'Location to the HTTPS version of the site'
|
||||
],
|
||||
[
|
||||
'location' => [
|
||||
'/anotherfile.ext',
|
||||
'http://anotherfile/hoster.ext',
|
||||
'https://anotherfile/hoster.ext'
|
||||
],
|
||||
'final' => 'https://anotherfile/hoster.ext',
|
||||
'Relative file path Location: should keep the latest host and scheme!'
|
||||
],
|
||||
[
|
||||
'location' => [ '/anotherfile.ext' ],
|
||||
'final' => 'http://oldsite/anotherfile.ext',
|
||||
'Relative Location without domain '
|
||||
],
|
||||
[
|
||||
'location' => null,
|
||||
'final' => 'http://oldsite/file.ext',
|
||||
'No Location (no redirect) '
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRelativeRedirects
|
||||
* @covers MWHttpRequest::getFinalUrl
|
||||
*/
|
||||
public function testRelativeRedirections( $location, $final, $message = null ) {
|
||||
$h = $this->factory->create( 'http://oldsite/file.ext', [], __METHOD__ );
|
||||
$h = TestingAccessWrapper::newFromObject( $h );
|
||||
|
||||
// Forge a Location header
|
||||
$h->respHeaders['location'] = $location;
|
||||
|
||||
// Verify it correctly fixes the Location
|
||||
$this->assertEquals( $final, $h->getFinalUrl(), $message );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group large
|
||||
* @covers PhpHttpRequest
|
||||
*/
|
||||
class PhpHttpRequestTest extends MWHttpRequestTestCase {
|
||||
protected static $httpEngine = 'php';
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ class MultiHttpClientTest extends MediaWikiIntegrationTestCase {
|
|||
'timeout' => 1,
|
||||
'connectTimeout' => 1
|
||||
];
|
||||
$httpRequest = $this->getMockBuilder( PhpHttpRequest::class )
|
||||
$httpRequest = $this->getMockBuilder( MWHttpRequest::class )
|
||||
->setConstructorArgs( [ '', $options ] )
|
||||
->getMock();
|
||||
$httpRequest->method( 'execute' )
|
||||
|
|
|
|||
Loading…
Reference in a new issue