MediaWiki  master
GuzzleHttpRequest.php
Go to the documentation of this file.
1 <?php
21 use GuzzleHttp\Client;
22 use GuzzleHttp\HandlerStack;
23 use GuzzleHttp\Middleware;
24 use GuzzleHttp\Psr7\Request;
25 use Psr\Http\Message\RequestInterface;
26 
43  public const SUPPORTS_FILE_POSTS = true;
44 
45  protected $handler = null;
46  protected $sink = null;
48  protected $guzzleOptions = [ 'http_errors' => false ];
49 
59  public function __construct(
60  $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
61  ) {
62  parent::__construct( $url, $options, $caller, $profiler );
63 
64  if ( isset( $options['handler'] ) ) {
65  $this->handler = $options['handler'];
66  }
67  if ( isset( $options['sink'] ) ) {
68  $this->sink = $options['sink'];
69  }
70  }
71 
92  public function setCallback( $callback ) {
93  $this->sink = null;
94  $this->doSetCallback( $callback );
95  }
96 
107  protected function doSetCallback( $callback ) {
108  if ( !$this->sink ) {
109  parent::doSetCallback( $callback );
110  $this->sink = new MWCallbackStream( $this->callback );
111  }
112  }
113 
119  public function execute() {
120  $this->prepare();
121 
122  if ( !$this->status->isOK() ) {
123  return Status::wrap( $this->status ); // TODO B/C; move this to callers
124  }
125 
126  if ( $this->proxy ) {
127  $this->guzzleOptions['proxy'] = $this->proxy;
128  }
129 
130  $this->guzzleOptions['timeout'] = $this->timeout;
131  $this->guzzleOptions['connect_timeout'] = $this->connectTimeout;
132  $this->guzzleOptions['version'] = '1.1';
133 
134  if ( !$this->followRedirects ) {
135  $this->guzzleOptions['allow_redirects'] = false;
136  } else {
137  $this->guzzleOptions['allow_redirects'] = [
138  'max' => $this->maxRedirects
139  ];
140  }
141 
142  if ( $this->method == 'POST' ) {
144  if ( is_array( $postData ) ) {
145  $this->guzzleOptions['form_params'] = $postData;
146  } else {
147  $this->guzzleOptions['body'] = $postData;
148  // mimic CURLOPT_POST option
149  if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
150  $this->reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
151  }
152  }
153 
154  // Suppress 'Expect: 100-continue' header, as some servers
155  // will reject it with a 417 and Curl won't auto retry
156  // with HTTP 1.0 fallback
157  $this->guzzleOptions['expect'] = false;
158  }
159 
160  // Create Middleware to use cookies from $this->getCookieJar(),
161  // which is in MediaWiki CookieJar format, not in Guzzle-specific CookieJar format.
162  // Note: received cookies (from HTTP response) don't need to be handled here,
163  // they will be added back into the CookieJar by MWHttpRequest::parseCookies().
164  $stack = HandlerStack::create( $this->handler );
165 
166  // @phan-suppress-next-line PhanUndeclaredFunctionInCallable
167  $stack->remove( 'cookies' );
168 
169  $mwCookieJar = $this->getCookieJar();
170  $stack->push( Middleware::mapRequest(
171  static function ( RequestInterface $request ) use ( $mwCookieJar ) {
172  $uri = $request->getUri();
173  $cookieHeader = $mwCookieJar->serializeToHttpRequest(
174  $uri->getPath() ?: '/',
175  $uri->getHost()
176  );
177  if ( !$cookieHeader ) {
178  return $request;
179  }
180 
181  return $request->withHeader( 'Cookie', $cookieHeader );
182  }
183  ), 'cookies' );
184 
185  $this->guzzleOptions['handler'] = $stack;
186 
187  if ( $this->sink ) {
188  $this->guzzleOptions['sink'] = $this->sink;
189  }
190 
191  if ( $this->caInfo ) {
192  $this->guzzleOptions['verify'] = $this->caInfo;
193  } elseif ( !$this->sslVerifyHost && !$this->sslVerifyCert ) {
194  $this->guzzleOptions['verify'] = false;
195  }
196 
197  $client = new Client( $this->guzzleOptions );
198  $request = new Request( $this->method, $this->url );
199  foreach ( $this->reqHeaders as $name => $value ) {
200  $request = $request->withHeader( $name, $value );
201  }
202 
203  try {
204  $response = $client->send( $request );
205  $this->headerList = $response->getHeaders();
206 
207  $this->respVersion = $response->getProtocolVersion();
208  $this->respStatus = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
209  } catch ( GuzzleHttp\Exception\ConnectException $e ) {
210  // ConnectException is thrown for several reasons besides generic "timeout":
211  // Connection refused
212  // couldn't connect to host
213  // connection attempt failed
214  // Could not resolve IPv4 address for host
215  // Could not resolve IPv6 address for host
216  if ( $this->usingCurl() ) {
217  $handlerContext = $e->getHandlerContext();
218  if ( $handlerContext['errno'] == CURLE_OPERATION_TIMEOUTED ) {
219  $this->status->fatal( 'http-timed-out', $this->url );
220  } else {
221  $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
222  }
223  } else {
224  $this->status->fatal( 'http-request-error' );
225  }
226  } catch ( GuzzleHttp\Exception\RequestException $e ) {
227  if ( $this->usingCurl() ) {
228  $handlerContext = $e->getHandlerContext();
229  $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
230  } else {
231  // Non-ideal, but the only way to identify connection timeout vs other conditions
232  $needle = 'Connection timed out';
233  if ( strpos( $e->getMessage(), $needle ) !== false ) {
234  $this->status->fatal( 'http-timed-out', $this->url );
235  } else {
236  $this->status->fatal( 'http-request-error' );
237  }
238  }
239  } catch ( GuzzleHttp\Exception\GuzzleException $e ) {
240  $this->status->fatal( 'http-internal-error' );
241  }
242 
243  if ( $this->profiler ) {
244  $profileSection = $this->profiler->scopedProfileIn(
245  __METHOD__ . '-' . $this->profileName
246  );
247  }
248 
249  if ( $this->profiler ) {
250  $this->profiler->scopedProfileOut( $profileSection );
251  }
252 
253  $this->parseHeader();
254  $this->setStatus();
255 
256  return Status::wrap( $this->status ); // TODO B/C; move this to callers
257  }
258 
259  protected function prepare() {
260  $this->doSetCallback( $this->callback );
261  parent::prepare();
262  }
263 
267  protected function usingCurl() {
268  return ( $this->handler && is_a( $this->handler, 'GuzzleHttp\Handler\CurlHandler' ) ) ||
269  ( !$this->handler && extension_loaded( 'curl' ) );
270  }
271 
276  protected function parseHeader() {
277  // Failure without (valid) headers gets a response status of zero
278  if ( !$this->status->isOK() ) {
279  $this->respStatus = '0 Error';
280  }
281 
282  foreach ( $this->headerList as $name => $values ) {
283  $this->respHeaders[strtolower( $name )] = $values;
284  }
285 
286  $this->parseCookies();
287  }
288 }
MWHttpRequest\$callback
callable $callback
Definition: MWHttpRequest.php:55
MWHttpRequest\setStatus
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
Definition: MWHttpRequest.php:441
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:87
MWHttpRequest\$maxRedirects
$maxRedirects
Definition: MWHttpRequest.php:56
GuzzleHttpRequest\$sink
$sink
Definition: GuzzleHttpRequest.php:46
MWHttpRequest\$profiler
Profiler $profiler
Definition: MWHttpRequest.php:77
GuzzleHttpRequest\parseHeader
parseHeader()
Guzzle provides headers as an array.
Definition: GuzzleHttpRequest.php:276
MWHttpRequest\$connectTimeout
$connectTimeout
Definition: MWHttpRequest.php:58
MWHttpRequest\$postData
$postData
Definition: MWHttpRequest.php:43
MWHttpRequest\parseCookies
parseCookies()
Parse the cookies in the response headers and store them in the cookie jar.
Definition: MWHttpRequest.php:573
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
GuzzleHttpRequest
MWHttpRequest implemented using the Guzzle library.
Definition: GuzzleHttpRequest.php:42
GuzzleHttpRequest\prepare
prepare()
Definition: GuzzleHttpRequest.php:259
Profiler
Profiler base class that defines the interface and some shared functionality.
Definition: Profiler.php:36
MWHttpRequest\getCookieJar
getCookieJar()
Returns the cookie jar in use.
Definition: MWHttpRequest.php:541
MWHttpRequest\$timeout
int string $timeout
Definition: MWHttpRequest.php:39
GuzzleHttpRequest\doSetCallback
doSetCallback( $callback)
Worker function for setting callbacks.
Definition: GuzzleHttpRequest.php:107
GuzzleHttpRequest\$handler
$handler
Definition: GuzzleHttpRequest.php:45
GuzzleHttpRequest\execute
execute()
Definition: GuzzleHttpRequest.php:119
MWHttpRequest
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
Definition: MWHttpRequest.php:33
MWHttpRequest\$caInfo
$caInfo
Definition: MWHttpRequest.php:48
MWCallbackStream
Definition: MWCallbackStream.php:35
GuzzleHttpRequest\__construct
__construct( $url, array $options=[], $caller=__METHOD__, Profiler $profiler=null)
Definition: GuzzleHttpRequest.php:59
GuzzleHttpRequest\SUPPORTS_FILE_POSTS
const SUPPORTS_FILE_POSTS
Definition: GuzzleHttpRequest.php:43
GuzzleHttpRequest\setCallback
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
Definition: GuzzleHttpRequest.php:92
MWHttpRequest\$proxy
$proxy
Definition: MWHttpRequest.php:44
GuzzleHttpRequest\$guzzleOptions
array $guzzleOptions
Definition: GuzzleHttpRequest.php:48
GuzzleHttpRequest\usingCurl
usingCurl()
Definition: GuzzleHttpRequest.php:267
MWHttpRequest\$url
$url
Definition: MWHttpRequest.php:52