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 
42  public const SUPPORTS_FILE_POSTS = true;
43 
44  protected $handler = null;
45  protected $sink = null;
47  protected $guzzleOptions = [ 'http_errors' => false ];
48 
58  public function __construct(
59  $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
60  ) {
61  parent::__construct( $url, $options, $caller, $profiler );
62 
63  if ( isset( $options['handler'] ) ) {
64  $this->handler = $options['handler'];
65  }
66  if ( isset( $options['sink'] ) ) {
67  $this->sink = $options['sink'];
68  }
69  }
70 
91  public function setCallback( $callback ) {
92  $this->sink = null;
93  $this->doSetCallback( $callback );
94  }
95 
106  protected function doSetCallback( $callback ) {
107  if ( !$this->sink ) {
108  parent::doSetCallback( $callback );
109  $this->sink = new MWCallbackStream( $this->callback );
110  }
111  }
112 
118  public function execute() {
119  $this->prepare();
120 
121  if ( !$this->status->isOK() ) {
122  return Status::wrap( $this->status ); // TODO B/C; move this to callers
123  }
124 
125  if ( $this->proxy ) {
126  $this->guzzleOptions['proxy'] = $this->proxy;
127  }
128 
129  $this->guzzleOptions['timeout'] = $this->timeout;
130  $this->guzzleOptions['connect_timeout'] = $this->connectTimeout;
131  $this->guzzleOptions['version'] = '1.1';
132 
133  if ( !$this->followRedirects ) {
134  $this->guzzleOptions['allow_redirects'] = false;
135  } else {
136  $this->guzzleOptions['allow_redirects'] = [
137  'max' => $this->maxRedirects
138  ];
139  }
140 
141  if ( $this->method == 'POST' ) {
143  if ( is_array( $postData ) ) {
144  $this->guzzleOptions['form_params'] = $postData;
145  } else {
146  $this->guzzleOptions['body'] = $postData;
147  // mimic CURLOPT_POST option
148  if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
149  $this->reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
150  }
151  }
152 
153  // Suppress 'Expect: 100-continue' header, as some servers
154  // will reject it with a 417 and Curl won't auto retry
155  // with HTTP 1.0 fallback
156  $this->guzzleOptions['expect'] = false;
157  }
158 
159  // Create Middleware to use cookies from $this->getCookieJar(),
160  // which is in MediaWiki CookieJar format, not in Guzzle-specific CookieJar format.
161  // Note: received cookies (from HTTP response) don't need to be handled here,
162  // they will be added back into the CookieJar by MWHttpRequest::parseCookies().
163  $stack = HandlerStack::create( $this->handler );
164 
165  // @phan-suppress-next-line PhanUndeclaredFunctionInCallable
166  $stack->remove( 'cookies' );
167 
168  $mwCookieJar = $this->getCookieJar();
169  $stack->push( Middleware::mapRequest(
170  static function ( RequestInterface $request ) use ( $mwCookieJar ) {
171  $uri = $request->getUri();
172  $cookieHeader = $mwCookieJar->serializeToHttpRequest(
173  $uri->getPath() ?: '/',
174  $uri->getHost()
175  );
176  if ( !$cookieHeader ) {
177  return $request;
178  }
179 
180  return $request->withHeader( 'Cookie', $cookieHeader );
181  }
182  ), 'cookies' );
183 
184  $this->guzzleOptions['handler'] = $stack;
185 
186  if ( $this->sink ) {
187  $this->guzzleOptions['sink'] = $this->sink;
188  }
189 
190  if ( $this->caInfo ) {
191  $this->guzzleOptions['verify'] = $this->caInfo;
192  } elseif ( !$this->sslVerifyHost && !$this->sslVerifyCert ) {
193  $this->guzzleOptions['verify'] = false;
194  }
195 
196  $client = new Client( $this->guzzleOptions );
197  $request = new Request( $this->method, $this->url );
198  foreach ( $this->reqHeaders as $name => $value ) {
199  $request = $request->withHeader( $name, $value );
200  }
201 
202  try {
203  $response = $client->send( $request );
204  $this->headerList = $response->getHeaders();
205 
206  $this->respVersion = $response->getProtocolVersion();
207  $this->respStatus = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
208  } catch ( GuzzleHttp\Exception\ConnectException $e ) {
209  // ConnectException is thrown for several reasons besides generic "timeout":
210  // Connection refused
211  // couldn't connect to host
212  // connection attempt failed
213  // Could not resolve IPv4 address for host
214  // Could not resolve IPv6 address for host
215  if ( $this->usingCurl() ) {
216  $handlerContext = $e->getHandlerContext();
217  if ( $handlerContext['errno'] == CURLE_OPERATION_TIMEOUTED ) {
218  $this->status->fatal( 'http-timed-out', $this->url );
219  } else {
220  $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
221  }
222  } else {
223  $this->status->fatal( 'http-request-error' );
224  }
225  } catch ( GuzzleHttp\Exception\RequestException $e ) {
226  if ( $this->usingCurl() ) {
227  $handlerContext = $e->getHandlerContext();
228  $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
229  } else {
230  // Non-ideal, but the only way to identify connection timeout vs other conditions
231  $needle = 'Connection timed out';
232  if ( strpos( $e->getMessage(), $needle ) !== false ) {
233  $this->status->fatal( 'http-timed-out', $this->url );
234  } else {
235  $this->status->fatal( 'http-request-error' );
236  }
237  }
238  } catch ( GuzzleHttp\Exception\GuzzleException $e ) {
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:479
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:92
MWHttpRequest\$maxRedirects
$maxRedirects
Definition: MWHttpRequest.php:56
GuzzleHttpRequest\$sink
$sink
Definition: GuzzleHttpRequest.php:45
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:611
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:41
GuzzleHttpRequest\prepare
prepare()
Definition: GuzzleHttpRequest.php:258
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:579
MWHttpRequest\$timeout
int string $timeout
Definition: MWHttpRequest.php:39
GuzzleHttpRequest\doSetCallback
doSetCallback( $callback)
Worker function for setting callbacks.
Definition: GuzzleHttpRequest.php:106
GuzzleHttpRequest\$handler
$handler
Definition: GuzzleHttpRequest.php:44
GuzzleHttpRequest\execute
execute()
Definition: GuzzleHttpRequest.php:118
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:36
GuzzleHttpRequest\__construct
__construct( $url, array $options=[], $caller=__METHOD__, Profiler $profiler=null)
Definition: GuzzleHttpRequest.php:58
GuzzleHttpRequest\SUPPORTS_FILE_POSTS
const SUPPORTS_FILE_POSTS
Definition: GuzzleHttpRequest.php:42
GuzzleHttpRequest\setCallback
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
Definition: GuzzleHttpRequest.php:91
MWHttpRequest\$proxy
$proxy
Definition: MWHttpRequest.php:44
GuzzleHttpRequest\$guzzleOptions
array $guzzleOptions
Definition: GuzzleHttpRequest.php:47
GuzzleHttpRequest\usingCurl
usingCurl()
Definition: GuzzleHttpRequest.php:266
MWHttpRequest\$url
$url
Definition: MWHttpRequest.php:52