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  $this->guzzleOptions['headers'] = $this->reqHeaders;
161 
162  // Create Middleware to use cookies from $this->getCookieJar(),
163  // which is in MediaWiki CookieJar format, not in Guzzle-specific CookieJar format.
164  // Note: received cookies (from HTTP response) don't need to be handled here,
165  // they will be added back into the CookieJar by MWHttpRequest::parseCookies().
166  $stack = HandlerStack::create( $this->handler );
167 
168  // @phan-suppress-next-line PhanUndeclaredFunctionInCallable
169  $stack->remove( 'cookies' );
170 
171  $mwCookieJar = $this->getCookieJar();
172  $stack->push( Middleware::mapRequest(
173  function ( RequestInterface $request ) use ( $mwCookieJar ) {
174  $uri = $request->getUri();
175  $cookieHeader = $mwCookieJar->serializeToHttpRequest(
176  $uri->getPath() ?: '/',
177  $uri->getHost()
178  );
179  if ( !$cookieHeader ) {
180  return $request;
181  }
182 
183  return $request->withHeader( 'Cookie', $cookieHeader );
184  }
185  ), 'cookies' );
186 
187  $this->guzzleOptions['handler'] = $stack;
188 
189  if ( $this->sink ) {
190  $this->guzzleOptions['sink'] = $this->sink;
191  }
192 
193  if ( $this->caInfo ) {
194  $this->guzzleOptions['verify'] = $this->caInfo;
195  } elseif ( !$this->sslVerifyHost && !$this->sslVerifyCert ) {
196  $this->guzzleOptions['verify'] = false;
197  }
198 
199  try {
200  $client = new Client( $this->guzzleOptions );
201  $request = new Request( $this->method, $this->url );
202  $response = $client->send( $request );
203  $this->headerList = $response->getHeaders();
204 
205  $this->respVersion = $response->getProtocolVersion();
206  $this->respStatus = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
207  } catch ( GuzzleHttp\Exception\ConnectException $e ) {
208  // ConnectException is thrown for several reasons besides generic "timeout":
209  // Connection refused
210  // couldn't connect to host
211  // connection attempt failed
212  // Could not resolve IPv4 address for host
213  // Could not resolve IPv6 address for host
214  if ( $this->usingCurl() ) {
215  $handlerContext = $e->getHandlerContext();
216  if ( $handlerContext['errno'] == CURLE_OPERATION_TIMEOUTED ) {
217  $this->status->fatal( 'http-timed-out', $this->url );
218  } else {
219  $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
220  }
221  } else {
222  $this->status->fatal( 'http-request-error' );
223  }
224  } catch ( GuzzleHttp\Exception\RequestException $e ) {
225  if ( $this->usingCurl() ) {
226  $handlerContext = $e->getHandlerContext();
227  $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
228  } else {
229  // Non-ideal, but the only way to identify connection timeout vs other conditions
230  $needle = 'Connection timed out';
231  if ( strpos( $e->getMessage(), $needle ) !== false ) {
232  $this->status->fatal( 'http-timed-out', $this->url );
233  } else {
234  $this->status->fatal( 'http-request-error' );
235  }
236  }
237  } catch ( GuzzleHttp\Exception\GuzzleException $e ) {
238  // @phan-suppress-previous-line PhanRedefinedClassReference False positive
239  $this->status->fatal( 'http-internal-error' );
240  }
241 
242  if ( $this->profiler ) {
243  $profileSection = $this->profiler->scopedProfileIn(
244  __METHOD__ . '-' . $this->profileName
245  );
246  }
247 
248  if ( $this->profiler ) {
249  $this->profiler->scopedProfileOut( $profileSection );
250  }
251 
252  $this->parseHeader();
253  $this->setStatus();
254 
255  return Status::wrap( $this->status ); // TODO B/C; move this to callers
256  }
257 
258  protected function prepare() {
259  $this->doSetCallback( $this->callback );
260  parent::prepare();
261  }
262 
266  protected function usingCurl() {
267  return ( $this->handler && is_a( $this->handler, 'GuzzleHttp\Handler\CurlHandler' ) ) ||
268  ( !$this->handler && extension_loaded( 'curl' ) );
269  }
270 
275  protected function parseHeader() {
276  // Failure without (valid) headers gets a response status of zero
277  if ( !$this->status->isOK() ) {
278  $this->respStatus = '0 Error';
279  }
280 
281  foreach ( $this->headerList as $name => $values ) {
282  $this->respHeaders[strtolower( $name )] = $values;
283  }
284 
285  $this->parseCookies();
286  }
287 }
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:442
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:85
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:275
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:574
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:258
Profiler
Profiler base class that defines the interface and some shared functionality.
Definition: Profiler.php:33
MWHttpRequest\getCookieJar
getCookieJar()
Returns the cookie jar in use.
Definition: MWHttpRequest.php:542
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\$reqHeaders
array $reqHeaders
Definition: MWHttpRequest.php:51
MWHttpRequest\$caInfo
$caInfo
Definition: MWHttpRequest.php:48
MWCallbackStream
Callback-aware stream.
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:266
MWHttpRequest\$url
$url
Definition: MWHttpRequest.php:52