MediaWiki REL1_39
GuzzleHttpRequest.php
Go to the documentation of this file.
1<?php
21use GuzzleHttp\Client;
22use GuzzleHttp\HandlerStack;
23use GuzzleHttp\Middleware;
24use GuzzleHttp\Psr7\Request;
25use 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' ) {
142 $postData = $this->postData;
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 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:36