Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.51% |
85 / 89 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
HttpRequestFactory | |
95.51% |
85 / 89 |
|
70.00% |
7 / 10 |
29 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
create | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
5.00 | |||
normalizeTimeout | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
6.29 | |||
canMakeRequests | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
request | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
post | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserAgent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createMultiClient | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
5 | |||
createGuzzleClient | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | namespace MediaWiki\Http; |
21 | |
22 | use GuzzleHttp\Client; |
23 | use GuzzleHttpRequest; |
24 | use InvalidArgumentException; |
25 | use MediaWiki\Config\ServiceOptions; |
26 | use MediaWiki\Logger\LoggerFactory; |
27 | use MediaWiki\MainConfigNames; |
28 | use MediaWiki\Status\Status; |
29 | use MultiHttpClient; |
30 | use MWHttpRequest; |
31 | use Profiler; |
32 | use Psr\Log\LoggerInterface; |
33 | |
34 | /** |
35 | * Factory creating MWHttpRequest objects. |
36 | * @internal |
37 | */ |
38 | class HttpRequestFactory { |
39 | /** @var ServiceOptions */ |
40 | private $options; |
41 | /** @var LoggerInterface */ |
42 | private $logger; |
43 | /** @var Telemetry|null */ |
44 | private $telemetry; |
45 | |
46 | /** |
47 | * @internal For use by ServiceWiring |
48 | */ |
49 | public const CONSTRUCTOR_OPTIONS = [ |
50 | MainConfigNames::HTTPTimeout, |
51 | MainConfigNames::HTTPConnectTimeout, |
52 | MainConfigNames::HTTPMaxTimeout, |
53 | MainConfigNames::HTTPMaxConnectTimeout, |
54 | MainConfigNames::LocalVirtualHosts, |
55 | MainConfigNames::LocalHTTPProxy, |
56 | ]; |
57 | |
58 | public function __construct( |
59 | ServiceOptions $options, |
60 | LoggerInterface $logger, |
61 | Telemetry $telemetry = null |
62 | ) { |
63 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
64 | $this->options = $options; |
65 | $this->logger = $logger; |
66 | $this->telemetry = $telemetry; |
67 | } |
68 | |
69 | /** |
70 | * Generate a new MWHttpRequest object |
71 | * @param string $url Url to use |
72 | * @param array $options Possible keys for the array: |
73 | * - timeout Timeout length in seconds or 'default' |
74 | * - connectTimeout Timeout for connection, in seconds (curl only) or 'default' |
75 | * - maxTimeout Override for the configured maximum timeout. This should not be |
76 | * used in production code. |
77 | * - maxConnectTimeout Override for the configured maximum connect timeout. This should |
78 | * not be used in production code. |
79 | * - postData An array of key-value pairs or a url-encoded form data |
80 | * - proxy The proxy to use. |
81 | * Otherwise it will use $wgHTTPProxy or $wgLocalHTTPProxy (if set) |
82 | * Otherwise it will use the environment variable "http_proxy" (if set) |
83 | * - noProxy Don't use any proxy at all. Takes precedence over proxy value(s). |
84 | * - sslVerifyHost Verify hostname against certificate |
85 | * - sslVerifyCert Verify SSL certificate |
86 | * - caInfo Provide CA information |
87 | * - maxRedirects Maximum number of redirects to follow (defaults to 5) |
88 | * - followRedirects Whether to follow redirects (defaults to false). |
89 | * Note: this should only be used when the target URL is trusted, |
90 | * to avoid attacks on intranet services accessible by HTTP. |
91 | * - userAgent A user agent, if you want to override the default |
92 | * "MediaWiki/{MW_VERSION}". |
93 | * - logger A \Psr\Logger\LoggerInterface instance for debug logging |
94 | * - username Username for HTTP Basic Authentication |
95 | * - password Password for HTTP Basic Authentication |
96 | * - originalRequest Information about the original request (as a WebRequest object or |
97 | * an associative array with 'ip' and 'userAgent'). |
98 | * @phpcs:ignore Generic.Files.LineLength |
99 | * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:string|array,proxy?:?string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:?string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:\MediaWiki\Request\WebRequest|array{ip:string,userAgent:string}} $options |
100 | * @param string $caller The method making this request, for profiling |
101 | * @return MWHttpRequest |
102 | * @see MWHttpRequest::__construct |
103 | */ |
104 | public function create( $url, array $options = [], $caller = __METHOD__ ) { |
105 | if ( !isset( $options['logger'] ) ) { |
106 | $options['logger'] = $this->logger; |
107 | } |
108 | $options['timeout'] = $this->normalizeTimeout( |
109 | $options['timeout'] ?? null, |
110 | $options['maxTimeout'] ?? null, |
111 | $this->options->get( MainConfigNames::HTTPTimeout ), |
112 | $this->options->get( MainConfigNames::HTTPMaxTimeout ) ?: INF |
113 | ); |
114 | $options['connectTimeout'] = $this->normalizeTimeout( |
115 | $options['connectTimeout'] ?? null, |
116 | $options['maxConnectTimeout'] ?? null, |
117 | $this->options->get( MainConfigNames::HTTPConnectTimeout ), |
118 | $this->options->get( MainConfigNames::HTTPMaxConnectTimeout ) ?: INF |
119 | ); |
120 | $client = new GuzzleHttpRequest( $url, $options, $caller, Profiler::instance() ); |
121 | if ( $this->telemetry ) { |
122 | $client->addTelemetry( $this->telemetry ); |
123 | } |
124 | return $client; |
125 | } |
126 | |
127 | /** |
128 | * Given a passed parameter value, a default and a maximum, figure out the |
129 | * correct timeout to pass to the backend. |
130 | * |
131 | * @param int|float|string|null $parameter The timeout in seconds, or "default" or null |
132 | * @param int|float|null $maxParameter The maximum timeout specified by the caller |
133 | * @param int|float $default The configured default timeout |
134 | * @param int|float $maxConfigured The configured maximum timeout |
135 | * @return int|float |
136 | */ |
137 | private function normalizeTimeout( $parameter, $maxParameter, $default, $maxConfigured ) { |
138 | if ( $parameter === 'default' || $parameter === null ) { |
139 | if ( !is_numeric( $default ) ) { |
140 | throw new InvalidArgumentException( |
141 | '$wgHTTPTimeout and $wgHTTPConnectTimeout must be set to a number' ); |
142 | } |
143 | $value = $default; |
144 | } else { |
145 | $value = $parameter; |
146 | } |
147 | $max = $maxParameter ?? $maxConfigured; |
148 | if ( $max && $value > $max ) { |
149 | return $max; |
150 | } |
151 | |
152 | return $value; |
153 | } |
154 | |
155 | /** |
156 | * Simple function to test if we can make any sort of requests at all, using |
157 | * cURL or fopen() |
158 | * @return bool |
159 | */ |
160 | public function canMakeRequests() { |
161 | return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); |
162 | } |
163 | |
164 | /** |
165 | * Perform an HTTP request |
166 | * |
167 | * @since 1.34 |
168 | * @param string $method HTTP method. Usually GET/POST |
169 | * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// |
170 | * URL |
171 | * @param array $options See HttpRequestFactory::create |
172 | * @param string $caller The method making this request, for profiling |
173 | * @return string|null null on failure or a string on success |
174 | */ |
175 | public function request( $method, $url, array $options = [], $caller = __METHOD__ ) { |
176 | $logger = LoggerFactory::getInstance( 'http' ); |
177 | $logger->debug( "$method: $url" ); |
178 | |
179 | $options['method'] = strtoupper( $method ); |
180 | |
181 | $req = $this->create( $url, $options, $caller ); |
182 | $status = $req->execute(); |
183 | |
184 | if ( $status->isOK() ) { |
185 | return $req->getContent(); |
186 | } else { |
187 | $errors = $status->getErrorsByType( 'error' ); |
188 | $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ), |
189 | [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] ); |
190 | return null; |
191 | } |
192 | } |
193 | |
194 | /** |
195 | * Simple wrapper for `request( 'GET' )`, parameters have the same meaning as for `request()` |
196 | * |
197 | * @since 1.34 |
198 | * @param string $url |
199 | * @param array $options |
200 | * @param string $caller |
201 | * @return string|null |
202 | */ |
203 | public function get( $url, array $options = [], $caller = __METHOD__ ) { |
204 | return $this->request( 'GET', $url, $options, $caller ); |
205 | } |
206 | |
207 | /** |
208 | * Simple wrapper for `request( 'POST' )`, parameters have the same meaning as for `request()` |
209 | * |
210 | * @since 1.34 |
211 | * @param string $url |
212 | * @param array $options |
213 | * @param string $caller |
214 | * @return string|null |
215 | */ |
216 | public function post( $url, array $options = [], $caller = __METHOD__ ) { |
217 | return $this->request( 'POST', $url, $options, $caller ); |
218 | } |
219 | |
220 | /** |
221 | * @return string |
222 | */ |
223 | public function getUserAgent() { |
224 | return 'MediaWiki/' . MW_VERSION; |
225 | } |
226 | |
227 | /** |
228 | * Get a MultiHttpClient with MediaWiki configured defaults applied. |
229 | * |
230 | * Unlike create(), by default, no proxy will be used. To use a proxy, |
231 | * specify the 'proxy' option. |
232 | * |
233 | * @param array $options Options as documented in MultiHttpClient::__construct(), |
234 | * except that for consistency with create(), 'timeout' is accepted as an |
235 | * alias for 'reqTimeout', and 'connectTimeout' is accepted as an alias for |
236 | * 'connTimeout'. |
237 | * @return MultiHttpClient |
238 | */ |
239 | public function createMultiClient( $options = [] ) { |
240 | $options['reqTimeout'] = $this->normalizeTimeout( |
241 | $options['reqTimeout'] ?? $options['timeout'] ?? null, |
242 | $options['maxReqTimeout'] ?? $options['maxTimeout'] ?? null, |
243 | $this->options->get( MainConfigNames::HTTPTimeout ), |
244 | $this->options->get( MainConfigNames::HTTPMaxTimeout ) ?: INF |
245 | ); |
246 | $options['connTimeout'] = $this->normalizeTimeout( |
247 | $options['connTimeout'] ?? $options['connectTimeout'] ?? null, |
248 | $options['maxConnTimeout'] ?? $options['maxConnectTimeout'] ?? null, |
249 | $this->options->get( MainConfigNames::HTTPConnectTimeout ), |
250 | $this->options->get( MainConfigNames::HTTPMaxConnectTimeout ) ?: INF |
251 | ); |
252 | $options += [ |
253 | 'maxReqTimeout' => $this->options->get( MainConfigNames::HTTPMaxTimeout ) ?: INF, |
254 | 'maxConnTimeout' => |
255 | $this->options->get( MainConfigNames::HTTPMaxConnectTimeout ) ?: INF, |
256 | 'userAgent' => $this->getUserAgent(), |
257 | 'logger' => $this->logger, |
258 | 'localProxy' => $this->options->get( MainConfigNames::LocalHTTPProxy ), |
259 | 'localVirtualHosts' => $this->options->get( MainConfigNames::LocalVirtualHosts ), |
260 | 'telemetry' => Telemetry::getInstance(), |
261 | ]; |
262 | return new MultiHttpClient( $options ); |
263 | } |
264 | |
265 | /** |
266 | * Get a GuzzleHttp\Client instance. |
267 | * |
268 | * @since 1.36 |
269 | * @param array $config Client configuration settings. |
270 | * @return Client |
271 | * |
272 | * @see \GuzzleHttp\RequestOptions for a list of available request options. |
273 | * @see Client::__construct() for additional options. |
274 | * Additional options that should not be used in production code: |
275 | * - maxTimeout Override for the configured maximum timeout. |
276 | * - maxConnectTimeout Override for the configured maximum connect timeout. |
277 | */ |
278 | public function createGuzzleClient( array $config = [] ): Client { |
279 | $config['timeout'] = $this->normalizeTimeout( |
280 | $config['timeout'] ?? null, |
281 | $config['maxTimeout'] ?? null, |
282 | $this->options->get( MainConfigNames::HTTPTimeout ), |
283 | $this->options->get( MainConfigNames::HTTPMaxTimeout ) ?: INF |
284 | ); |
285 | |
286 | $config['connect_timeout'] = $this->normalizeTimeout( |
287 | $config['connect_timeout'] ?? null, |
288 | $config['maxConnectTimeout'] ?? null, |
289 | $this->options->get( MainConfigNames::HTTPConnectTimeout ), |
290 | $this->options->get( MainConfigNames::HTTPMaxConnectTimeout ) ?: INF |
291 | ); |
292 | |
293 | if ( !isset( $config['headers']['User-Agent'] ) ) { |
294 | $config['headers']['User-Agent'] = $this->getUserAgent(); |
295 | } |
296 | if ( $this->telemetry ) { |
297 | $config['headers'] = array_merge( |
298 | $this->telemetry->getRequestHeaders(), $config['headers'] |
299 | ); |
300 | } |
301 | |
302 | return new Client( $config ); |
303 | } |
304 | } |