MediaWiki REL1_37
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
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' ) {
143 $postData = $this->postData;
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 // Create Middleware to use cookies from $this->getCookieJar(),
161 // which is in MediaWiki CookieJar format, not in Guzzle-specific CookieJar format.
162 // Note: received cookies (from HTTP response) don't need to be handled here,
163 // they will be added back into the CookieJar by MWHttpRequest::parseCookies().
164 $stack = HandlerStack::create( $this->handler );
165
166 // @phan-suppress-next-line PhanUndeclaredFunctionInCallable
167 $stack->remove( 'cookies' );
168
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 $this->guzzleOptions['handler'] = $stack;
186
187 if ( $this->sink ) {
188 $this->guzzleOptions['sink'] = $this->sink;
189 }
190
191 if ( $this->caInfo ) {
192 $this->guzzleOptions['verify'] = $this->caInfo;
193 } elseif ( !$this->sslVerifyHost && !$this->sslVerifyCert ) {
194 $this->guzzleOptions['verify'] = false;
195 }
196
197 $client = new Client( $this->guzzleOptions );
198 $request = new Request( $this->method, $this->url );
199 foreach ( $this->reqHeaders as $name => $value ) {
200 $request = $request->withHeader( $name, $value );
201 }
202
203 try {
204 $response = $client->send( $request );
205 $this->headerList = $response->getHeaders();
206
207 $this->respVersion = $response->getProtocolVersion();
208 $this->respStatus = $response->getStatusCode() . ' ' . $response->getReasonPhrase();
209 } catch ( GuzzleHttp\Exception\ConnectException $e ) {
210 // ConnectException is thrown for several reasons besides generic "timeout":
211 // Connection refused
212 // couldn't connect to host
213 // connection attempt failed
214 // Could not resolve IPv4 address for host
215 // Could not resolve IPv6 address for host
216 if ( $this->usingCurl() ) {
217 $handlerContext = $e->getHandlerContext();
218 if ( $handlerContext['errno'] == CURLE_OPERATION_TIMEOUTED ) {
219 $this->status->fatal( 'http-timed-out', $this->url );
220 } else {
221 $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
222 }
223 } else {
224 $this->status->fatal( 'http-request-error' );
225 }
226 } catch ( GuzzleHttp\Exception\RequestException $e ) {
227 if ( $this->usingCurl() ) {
228 $handlerContext = $e->getHandlerContext();
229 $this->status->fatal( 'http-curl-error', $handlerContext['error'] );
230 } else {
231 // Non-ideal, but the only way to identify connection timeout vs other conditions
232 $needle = 'Connection timed out';
233 if ( strpos( $e->getMessage(), $needle ) !== false ) {
234 $this->status->fatal( 'http-timed-out', $this->url );
235 } else {
236 $this->status->fatal( 'http-request-error' );
237 }
238 }
239 } catch ( GuzzleHttp\Exception\GuzzleException $e ) {
240 $this->status->fatal( 'http-internal-error' );
241 }
242
243 if ( $this->profiler ) {
244 $profileSection = $this->profiler->scopedProfileIn(
245 __METHOD__ . '-' . $this->profileName
246 );
247 }
248
249 if ( $this->profiler ) {
250 $this->profiler->scopedProfileOut( $profileSection );
251 }
252
253 $this->parseHeader();
254 $this->setStatus();
255
256 return Status::wrap( $this->status ); // TODO B/C; move this to callers
257 }
258
259 protected function prepare() {
260 $this->doSetCallback( $this->callback );
261 parent::prepare();
262 }
263
267 protected function usingCurl() {
268 return ( $this->handler && is_a( $this->handler, 'GuzzleHttp\Handler\CurlHandler' ) ) ||
269 ( !$this->handler && extension_loaded( 'curl' ) );
270 }
271
276 protected function parseHeader() {
277 // Failure without (valid) headers gets a response status of zero
278 if ( !$this->status->isOK() ) {
279 $this->respStatus = '0 Error';
280 }
281
282 foreach ( $this->headerList as $name => $values ) {
283 $this->respHeaders[strtolower( $name )] = $values;
284 }
285
286 $this->parseCookies();
287 }
288}
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