'MIMEsearchPage' => __DIR__ . '/includes/specials/SpecialMIMEsearch.php',
'MSCompoundFileReader' => __DIR__ . '/includes/libs/mime/MSCompoundFileReader.php',
'MWCallableUpdate' => __DIR__ . '/includes/deferred/MWCallableUpdate.php',
+ 'MWCallbackStream' => __DIR__ . '/includes/http/MWCallbackStream.php',
'MWContentSerializationException' => __DIR__ . '/includes/exception/MWContentSerializationException.php',
'MWCryptHKDF' => __DIR__ . '/includes/utils/MWCryptHKDF.php',
'MWCryptHash' => __DIR__ . '/includes/libs/MWCryptHash.php',
* MWHttpRequest implemented using the Guzzle library
*
* Differences from the CurlHttpRequest implementation:
- * 1) the MWHttpRequest 'callback" option is unsupported. Instead, use the 'sink' option to
- * send a filename/stream (see http://docs.guzzlephp.org/en/stable/request-options.html#sink)
+ * 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
* @throws Exception
*/
public function __construct(
- $url, array $options = [], $caller = __METHOD__, $profiler = null
+ $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
) {
parent::__construct( $url, $options, $caller, $profiler );
}
}
+ /**
+ * Set a read callback to accept data read from the HTTP request.
+ * By default, data is appended to an internal buffer which can be
+ * retrieved through $req->getContent().
+ *
+ * To handle data as it comes in -- especially for large files that
+ * would not fit in memory -- you can instead set your own callback,
+ * in the form function($resource, $buffer) where the first parameter
+ * is the low-level resource being read (implementation specific),
+ * and the second parameter is the data buffer.
+ *
+ * You MUST return the number of bytes handled in the buffer; if fewer
+ * bytes are reported handled than were passed to you, the HTTP fetch
+ * will be aborted.
+ *
+ * This function overrides any 'sink' or 'callback' constructor option.
+ *
+ * @param callable|null $callback
+ * @throws InvalidArgumentException
+ */
+ public function setCallback( $callback ) {
+ $this->sink = null;
+ $this->doSetCallback( $callback );
+ }
+
+ /**
+ * Worker function for setting callbacks. Calls can originate both internally and externally
+ * via setCallback). Defaults to the internal read callback if $callback is null.
+ *
+ * If a sink is already specified, this does nothing. This causes the 'sink' constructor
+ * option to override the 'callback' constructor option.
+ *
+ * @param $callback|null $callback
+ * @throws InvalidArgumentException
+ */
+ protected function doSetCallback( $callback ) {
+ if ( !$this->sink ) {
+ parent::doSetCallback( $callback );
+ $this->sink = new MWCallbackStream( $this->callback );
+ }
+ }
+
/**
* @see MWHttpRequest::execute
*
$request = new Request( $this->method, $this->url );
$response = $client->send( $request );
$this->headerList = $response->getHeaders();
- $this->content = $response->getBody()->getContents();
$this->respVersion = $response->getProtocolVersion();
$this->respStatus = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
-
} catch ( GuzzleHttp\Exception\ConnectException $e ) {
// ConnectException is thrown for several reasons besides generic "timeout":
// Connection refused
return Status::wrap( $this->status ); // TODO B/C; move this to callers
}
+ protected function prepare() {
+ $this->doSetCallback( $this->callback );
+ parent::prepare();
+ }
+
/**
* @return bool
*/
/**
* Guzzle provides headers as an array. Reprocess to match our expectations. Guzzle will
- * have already parsed and removed the status line (in EasyHandle::createResponse)z.
+ * have already parsed and removed the status line (in EasyHandle::createResponse).
*/
protected function parseHeader() {
// Failure without (valid) headers gets a response status of zero
* @param string $caller The method making this request, for profiling
* @return string|bool (bool)false on failure or a string on success
*/
- public static function request( $method, $url, $options = [], $caller = __METHOD__ ) {
+ public static function request( $method, $url, array $options = [], $caller = __METHOD__ ) {
$logger = LoggerFactory::getInstance( 'http' );
$logger->debug( "$method: $url" );
* @param string $caller The method making this request, for profiling
* @return string|bool false on error
*/
- public static function get( $url, $options = [], $caller = __METHOD__ ) {
+ public static function get( $url, array $options = [], $caller = __METHOD__ ) {
$args = func_get_args();
if ( isset( $args[1] ) && ( is_string( $args[1] ) || is_numeric( $args[1] ) ) ) {
// Second was used to be the timeout
* @param string $caller The method making this request, for profiling
* @return string|bool false on error
*/
- public static function post( $url, $options = [], $caller = __METHOD__ ) {
+ public static function post( $url, array $options = [], $caller = __METHOD__ ) {
return self::request( 'POST', $url, $options, $caller );
}
* @param array $options
* @return MultiHttpClient
*/
- public static function createMultiClient( $options = [] ) {
+ public static function createMultiClient( array $options = [] ) {
global $wgHTTPConnectTimeout, $wgHTTPTimeout, $wgHTTPProxy;
return new MultiHttpClient( $options + [
--- /dev/null
+<?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
+ */
+
+use Psr\Http\Message\StreamInterface;
+use GuzzleHttp\Psr7\StreamDecoratorTrait;
+
+/**
+ * Callback-aware stream. Allows using a callback function to receive data in contexts where
+ * a PSR-7 stream is required. This was created so that GuzzleHttpRequest can support our
+ * "callback" option, for backward compatibility. Newer code that uses GuzzleHttpRequest
+ * should consider using the "sink" option instead.
+ *
+ * @private for use by GuzzleHttpRequest only
+ * @since 1.33
+ */
+class MWCallbackStream implements StreamInterface {
+ use StreamDecoratorTrait;
+
+ private $callback;
+
+ public function __construct( callable $cb ) {
+ $this->stream = GuzzleHttp\Psr7\stream_for();
+ $this->callback = $cb;
+ }
+
+ public function write( $string ) {
+ return call_user_func( $this->callback, $this, $string );
+ }
+}
* @throws Exception
*/
public function __construct(
- $url, array $options = [], $caller = __METHOD__, $profiler = null
+ $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
) {
global $wgHTTPTimeout, $wgHTTPConnectTimeout;
* @param array $args
* @todo overload the args param
*/
- public function setData( $args ) {
+ public function setData( array $args ) {
$this->postData = $args;
}
* @throws InvalidArgumentException
*/
public function setCallback( $callback ) {
+ return $this->doSetCallback( $callback );
+ }
+
+ /**
+ * Worker function for setting callbacks. Calls can originate both internally and externally
+ * via setCallback). Defaults to the internal read callback if $callback is null.
+ *
+ * @param $callback|null $callback
+ * @throws InvalidArgumentException
+ */
+ protected function doSetCallback( $callback ) {
if ( is_null( $callback ) ) {
$callback = [ $this, 'read' ];
} elseif ( !is_callable( $callback ) ) {
$this->proxySetup(); // set up any proxy as needed
if ( !$this->callback ) {
- $this->setCallback( null );
+ $this->doSetCallback( null );
}
if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
*
* @param CookieJar $jar
*/
- public function setCookieJar( $jar ) {
+ public function setCookieJar( CookieJar $jar ) {
$this->cookieJar = $jar;
}
* @param string $value
* @param array $attr
*/
- public function setCookie( $name, $value, $attr = [] ) {
+ public function setCookie( $name, $value, array $attr = [] ) {
if ( !$this->cookieJar ) {
$this->cookieJar = new CookieJar;
}
--- /dev/null
+<?php
+
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Psr7\Request;
+
+/**
+ * class for tests of GuzzleHttpRequest
+ *
+ * No actual requests are made herein - all external communications are mocked
+ *
+ * @covers GuzzleHttpRequest
+ * @covers MWHttpRequest
+ */
+class GuzzleHttpRequestTest extends MediaWikiTestCase {
+ /**
+ * Placeholder url to use for various tests. This is never contacted, but we must use
+ * a url of valid format to avoid validation errors.
+ * @var string
+ */
+ protected $exampleUrl = 'http://www.example.test';
+
+ /**
+ * Minimal example body text
+ * @var string
+ */
+ protected $exampleBodyText = 'x';
+
+ /**
+ * For accumulating callback data for testing
+ * @var string
+ */
+ protected $bodyTextReceived = '';
+
+ /**
+ * Callback: process a chunk of the result of a HTTP request
+ *
+ * @param mixed $req
+ * @param string $buffer
+ * @return int Number of bytes handled
+ */
+ public function processHttpDataChunk( $req, $buffer ) {
+ $this->bodyTextReceived .= $buffer;
+ return strlen( $buffer );
+ }
+
+ public function testSuccess() {
+ $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+ 'status' => 200,
+ ], $this->exampleBodyText ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+ $r->execute();
+
+ $this->assertEquals( 200, $r->getStatus() );
+ $this->assertEquals( $this->exampleBodyText, $r->getContent() );
+ }
+
+ public function testSuccessConstructorCallback() {
+ $this->bodyTextReceived = '';
+ $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+ 'status' => 200,
+ ], $this->exampleBodyText ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [
+ 'callback' => [ $this, 'processHttpDataChunk' ],
+ 'handler' => $handler,
+ ] );
+ $r->execute();
+
+ $this->assertEquals( 200, $r->getStatus() );
+ $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
+ }
+
+ public function testSuccessSetCallback() {
+ $this->bodyTextReceived = '';
+ $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+ 'status' => 200,
+ ], $this->exampleBodyText ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [
+ 'handler' => $handler,
+ ] );
+ $r->setCallback( [ $this, 'processHttpDataChunk' ] );
+ $r->execute();
+
+ $this->assertEquals( 200, $r->getStatus() );
+ $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
+ }
+
+ /**
+ * use a callback stream to pipe the mocked response data to our callback function
+ */
+ public function testSuccessSink() {
+ $this->bodyTextReceived = '';
+ $handler = HandlerStack::create( new MockHandler( [ new Response( 200, [
+ 'status' => 200,
+ ], $this->exampleBodyText ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [
+ 'handler' => $handler,
+ 'sink' => new MWCallbackStream( [ $this, 'processHttpDataChunk' ] ),
+ ] );
+ $r->execute();
+
+ $this->assertEquals( 200, $r->getStatus() );
+ $this->assertEquals( $this->exampleBodyText, $this->bodyTextReceived );
+ }
+
+ public function testBadUrl() {
+ $r = new GuzzleHttpRequest( '' );
+ $s = $r->execute();
+ $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+ $this->assertEquals( 0, $r->getStatus() );
+ $this->assertEquals( 'http-invalid-url', $errorMsg );
+ }
+
+ public function testConnectException() {
+ $handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\ConnectException(
+ 'Mock Connection Exception', new Request( 'GET', $this->exampleUrl )
+ ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+ $s = $r->execute();
+ $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+ $this->assertEquals( 0, $r->getStatus() );
+ $this->assertEquals( 'http-request-error', $errorMsg );
+ }
+
+ public function testTimeout() {
+ $handler = HandlerStack::create( new MockHandler( [ new GuzzleHttp\Exception\RequestException(
+ 'Connection timed out', new Request( 'GET', $this->exampleUrl )
+ ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+ $s = $r->execute();
+ $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+ $this->assertEquals( 0, $r->getStatus() );
+ $this->assertEquals( 'http-timed-out', $errorMsg );
+ }
+
+ public function testNotFound() {
+ $handler = HandlerStack::create( new MockHandler( [ new Response( 404, [
+ 'status' => '404',
+ ] ) ] ) );
+ $r = new GuzzleHttpRequest( $this->exampleUrl, [ 'handler' => $handler ] );
+ $s = $r->execute();
+ $errorMsg = $s->getErrorsByType( 'error' )[0]['message'];
+
+ $this->assertEquals( 404, $r->getStatus() );
+ $this->assertEquals( 'http-bad-status', $errorMsg );
+ }
+}
<?php
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Psr7\Response;
-
/**
* @group Http
* @group small
$this->assertTrue( defined( $value ), $value . ' not defined' );
}
-
- /**
- * No actual request is made herein
- */
- public function testGuzzleHttpRequest() {
- $handler = HandlerStack::create( new MockHandler( [ new Response( 200 ) ] ) );
- $r = new GuzzleHttpRequest( 'http://www.example.text', [ 'handler' => $handler ] );
- $r->execute();
- $this->assertEquals( 200, $r->getStatus() );
-
- // @TODO: add failure tests (404s and failure to connect)
- }
}
/**
*/
public static $response;
- public static function get( $url, $options = [], $caller = __METHOD__ ) {
+ public static function get( $url, array $options = [], $caller = __METHOD__ ) {
PHPUnit_Framework_Assert::assertInternalType( 'string', $url );
- PHPUnit_Framework_Assert::assertInternalType( 'array', $options );
PHPUnit_Framework_Assert::assertInternalType( 'string', $caller );
return self::$response;