MediaWiki 1.40.4
GuzzleHttpRequest.php
Go to the documentation of this file.
1<?php
21use GuzzleHttp\Client;
22use GuzzleHttp\HandlerStack;
23use GuzzleHttp\MessageFormatter;
24use GuzzleHttp\Middleware;
25use GuzzleHttp\Psr7\Request;
26use Psr\Http\Message\RequestInterface;
27use Psr\Log\NullLogger;
28
44 public const SUPPORTS_FILE_POSTS = true;
45
46 protected $handler = null;
47 protected $sink = null;
49 protected $guzzleOptions = [ 'http_errors' => false ];
50
60 public function __construct(
61 $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
62 ) {
63 parent::__construct( $url, $options, $caller, $profiler );
64
65 if ( isset( $options['handler'] ) ) {
66 $this->handler = $options['handler'];
67 }
68 if ( isset( $options['sink'] ) ) {
69 $this->sink = $options['sink'];
70 }
71 }
72
93 public function setCallback( $callback ) {
94 $this->sink = null;
95 $this->doSetCallback( $callback );
96 }
97
108 protected function doSetCallback( $callback ) {
109 if ( !$this->sink ) {
110 parent::doSetCallback( $callback );
111 $this->sink = new MWCallbackStream( $this->callback );
112 }
113 }
114
120 public function execute() {
121 $this->prepare();
122
123 if ( !$this->status->isOK() ) {
124 return Status::wrap( $this->status ); // TODO B/C; move this to callers
125 }
126
127 if ( $this->proxy ) {
128 $this->guzzleOptions['proxy'] = $this->proxy;
129 }
130
131 $this->guzzleOptions['timeout'] = $this->timeout;
132 $this->guzzleOptions['connect_timeout'] = $this->connectTimeout;
133 $this->guzzleOptions['version'] = '1.1';
134
135 if ( !$this->followRedirects ) {
136 $this->guzzleOptions['allow_redirects'] = false;
137 } else {
138 $this->guzzleOptions['allow_redirects'] = [
139 'max' => $this->maxRedirects
140 ];
141 }
142
143 if ( $this->method == 'POST' ) {
144 $postData = $this->postData;
145 if ( is_array( $postData ) ) {
146 $this->guzzleOptions['form_params'] = $postData;
147 } else {
148 $this->guzzleOptions['body'] = $postData;
149 // mimic CURLOPT_POST option
150 if ( !isset( $this->reqHeaders['Content-Type'] ) ) {
151 $this->reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
152 }
153 }
154
155 // Suppress 'Expect: 100-continue' header, as some servers
156 // will reject it with a 417 and Curl won't auto retry
157 // with HTTP 1.0 fallback
158 $this->guzzleOptions['expect'] = false;
159 }
160
161 $stack = HandlerStack::create( $this->handler );
162
163 // Create Middleware to use cookies from $this->getCookieJar(),
164 // which is in MediaWiki CookieJar format, not in Guzzle-specific CookieJar format.
165 // Note: received cookies (from HTTP response) don't need to be handled here,
166 // they will be added back into the CookieJar by MWHttpRequest::parseCookies().
167 // @phan-suppress-next-line PhanUndeclaredFunctionInCallable
168 $stack->remove( 'cookies' );
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 if ( !$this->logger instanceof NullLogger ) {
186 $stack->push( Middleware::log( $this->logger, new MessageFormatter(
187 // TODO {error} will be 'NULL' on success which is unfortunate, but
188 // doesn't seem fixable without a custom formatter. Same for using
189 // PSR-3 variable replacement instead of raw strings.
190 '[{ts}] {method} {uri} HTTP/{version} - {code} {error}'
191 ) ), 'logger' );
192 }
193
194 $this->guzzleOptions['handler'] = $stack;
195
196 if ( $this->sink ) {
197 $this->guzzleOptions['sink'] = $this->sink;
198 }
199
200 if ( $this->caInfo ) {
201 $this->guzzleOptions['verify'] = $this->caInfo;
202 } elseif ( !$this->sslVerifyHost && !$this->sslVerifyCert ) {
203 $this->guzzleOptions['verify'] = false;
204 }
205
206 $client = new Client( $this->guzzleOptions );
207 $request = new Request( $this->method, $this->url );
208 foreach ( $this->reqHeaders as $name => $value ) {
209 $request = $request->withHeader( $name, $value );
210 }
211
212 try {
213 $response = $client->send( $request );
214 $this->headerList = $response->getHeaders();
215
216 $this->respVersion = $response->getProtocolVersion();
217 $this->respStatus = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
218 } catch ( GuzzleHttp\Exception\ConnectException $e ) {
219 // ConnectException is thrown for several reasons besides generic "timeout":
220 // Connection refused
221 // couldn't connect to host
222 // connection attempt failed
223 // Could not resolve IPv4 address for host
224 // Could not resolve IPv6 address for host
225 if ( $this->usingCurl() ) {
226 $handlerContext = $e->getHandlerContext();
227 if ( $handlerContext['errno'] == CURLE_OPERATION_TIMEOUTED ) {
228 $this->status->fatal( 'http-timed-out', $this->url );
229 } else {
230 $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
231 }
232 } else {
233 $this->status->fatal( 'http-request-error' );
234 }
235 } catch ( GuzzleHttp\Exception\RequestException $e ) {
236 if ( $this->usingCurl() ) {
237 $handlerContext = $e->getHandlerContext();
238 $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
239 } else {
240 // Non-ideal, but the only way to identify connection timeout vs other conditions
241 $needle = 'Connection timed out';
242 if ( strpos( $e->getMessage(), $needle ) !== false ) {
243 $this->status->fatal( 'http-timed-out', $this->url );
244 } else {
245 $this->status->fatal( 'http-request-error' );
246 }
247 }
248 } catch ( GuzzleHttp\Exception\GuzzleException $e ) {
249 $this->status->fatal( 'http-internal-error' );
250 }
251
252 if ( $this->profiler ) {
253 $profileSection = $this->profiler->scopedProfileIn(
254 __METHOD__ . '-' . $this->profileName
255 );
256 }
257
258 if ( $this->profiler ) {
259 $this->profiler->scopedProfileOut( $profileSection );
260 }
261
262 $this->parseHeader();
263 $this->setStatus();
264
265 return Status::wrap( $this->status ); // TODO B/C; move this to callers
266 }
267
268 protected function prepare() {
269 $this->doSetCallback( $this->callback );
270 parent::prepare();
271 }
272
276 protected function usingCurl() {
277 return ( $this->handler && is_a( $this->handler, 'GuzzleHttp\Handler\CurlHandler' ) ) ||
278 ( !$this->handler && extension_loaded( 'curl' ) );
279 }
280
285 protected function parseHeader() {
286 // Failure without (valid) headers gets a response status of zero
287 if ( !$this->status->isOK() ) {
288 $this->respStatus = '0 Error';
289 }
290
291 foreach ( $this->headerList as $name => $values ) {
292 $this->respHeaders[strtolower( $name )] = $values;
293 }
294
295 $this->parseCookies();
296 }
297}
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.
callable $callback
Profiler $profiler
Profiler base class that defines the interface and some shared functionality.
Definition Profiler.php:37