MediaWiki fundraising/REL1_35
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 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 // @phan-suppress-previous-line PhanRedefinedClassReference False positive
241 $this->status->fatal( 'http-internal-error' );
242 }
243
244 if ( $this->profiler ) {
245 $profileSection = $this->profiler->scopedProfileIn(
246 __METHOD__ . '-' . $this->profileName
247 );
248 }
249
250 if ( $this->profiler ) {
251 $this->profiler->scopedProfileOut( $profileSection );
252 }
253
254 $this->parseHeader();
255 $this->setStatus();
256
257 return Status::wrap( $this->status ); // TODO B/C; move this to callers
258 }
259
260 protected function prepare() {
261 $this->doSetCallback( $this->callback );
262 parent::prepare();
263 }
264
268 protected function usingCurl() {
269 return ( $this->handler && is_a( $this->handler, 'GuzzleHttp\Handler\CurlHandler' ) ) ||
270 ( !$this->handler && extension_loaded( 'curl' ) );
271 }
272
277 protected function parseHeader() {
278 // Failure without (valid) headers gets a response status of zero
279 if ( !$this->status->isOK() ) {
280 $this->respStatus = '0 Error';
281 }
282
283 foreach ( $this->headerList as $name => $values ) {
284 $this->respHeaders[strtolower( $name )] = $values;
285 }
286
287 $this->parseCookies();
288 }
289}
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)
Callback-aware stream.
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:33