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 }
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
MWHttpRequest implemented using the Guzzle library.
parseHeader()
Guzzle provides headers as an array.
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
doSetCallback( $callback)
Worker function for setting callbacks.
__construct( $url, array $options=[], $caller=__METHOD__, Profiler $profiler=null)
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
getCookieJar()
Returns the cookie jar in use.
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
parseCookies()
Parse the cookies in the response headers and store them in the cookie jar.
int string $timeout
callable $callback
Profiler $profiler
Profiler base class that defines the interface and some shared functionality.
Definition: Profiler.php:36
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62