Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.80% |
89 / 91 |
|
92.31% |
12 / 13 |
CRAP | |
0.00% |
0 / 1 |
HttpService | |
97.80% |
89 / 91 |
|
92.31% |
12 / 13 |
35 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setCurlProxyOptions | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
hasCurlAdapterAsDefault | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
proxify | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
get | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
post | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
postForm | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setDefaultAdapter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createClient | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
createQueryString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
send | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
isAssocParams | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
isLocal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind HTTP service class file. |
5 | * |
6 | * PHP version 7 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Http |
25 | * @author David Maus <maus@hab.de> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development |
28 | */ |
29 | |
30 | namespace VuFindHttp; |
31 | |
32 | use function get_class; |
33 | use function in_array; |
34 | use function strlen; |
35 | |
36 | /** |
37 | * VuFind HTTP service. |
38 | * |
39 | * @category VuFind |
40 | * @package Http |
41 | * @author David Maus <maus@hab.de> |
42 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
43 | * @link https://vufind.org/wiki/development |
44 | */ |
45 | class HttpService implements HttpServiceInterface |
46 | { |
47 | /** |
48 | * Default regular expression matching a request to localhost. |
49 | * |
50 | * @var string |
51 | */ |
52 | public const LOCAL_ADDRESS_RE = '@^(localhost|127(\.\d+){3}|\[::1\])@'; |
53 | |
54 | /** |
55 | * Proxy configuration. |
56 | * |
57 | * @see \Laminas\Http\Client\Adapter\Proxy::$config |
58 | * |
59 | * @var array |
60 | */ |
61 | protected $proxyConfig; |
62 | |
63 | /** |
64 | * Regular expression matching a request to localhost or hosts |
65 | * that are not proxied. |
66 | * |
67 | * @see \Laminas\Http\Client\Adapter\Proxy::$config |
68 | * |
69 | * @var string |
70 | */ |
71 | protected $localAddressesRegEx = self::LOCAL_ADDRESS_RE; |
72 | |
73 | /** |
74 | * Default client options. |
75 | * |
76 | * @var array |
77 | */ |
78 | protected $defaults; |
79 | |
80 | /** |
81 | * Default adapter |
82 | * |
83 | * @var \Laminas\Http\Client\Adapter\AdapterInterface |
84 | */ |
85 | protected $defaultAdapter = null; |
86 | |
87 | /** |
88 | * Constructor. |
89 | * |
90 | * @param array $proxyConfig Proxy configuration |
91 | * @param array $defaults Default HTTP options |
92 | * @param array $config Other configuration |
93 | * |
94 | * @return void |
95 | */ |
96 | public function __construct( |
97 | array $proxyConfig = [], |
98 | array $defaults = [], |
99 | array $config = [] |
100 | ) { |
101 | $this->proxyConfig = $proxyConfig; |
102 | $this->defaults = $defaults; |
103 | if (isset($config['localAddressesRegEx'])) { |
104 | $this->localAddressesRegEx = $config['localAddressesRegEx']; |
105 | } |
106 | } |
107 | |
108 | /** |
109 | * Set proxy options in a Curl adapter. |
110 | * |
111 | * @param \Laminas\Http\Client\Adapter\Curl $adapter Adapter to configure |
112 | * |
113 | * @return void |
114 | */ |
115 | protected function setCurlProxyOptions($adapter) |
116 | { |
117 | $adapter->setCurlOption(CURLOPT_PROXY, $this->proxyConfig['proxy_host']); |
118 | if (!empty($this->proxyConfig['proxy_port'])) { |
119 | $adapter |
120 | ->setCurlOption(CURLOPT_PROXYPORT, $this->proxyConfig['proxy_port']); |
121 | } |
122 | // HTTP is default, so handle only the SOCKS 5 proxy types |
123 | switch ($this->proxyConfig['proxy_type'] ?? '') { |
124 | case 'socks5': |
125 | $adapter->setCurlOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); |
126 | break; |
127 | case 'socks5_hostname': |
128 | $adapter |
129 | ->setCurlOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME); |
130 | break; |
131 | } |
132 | } |
133 | |
134 | /** |
135 | * Are we configured to use the CURL adapter? |
136 | * |
137 | * @return bool |
138 | */ |
139 | protected function hasCurlAdapterAsDefault() |
140 | { |
141 | $default = $this->defaults['adapter'] |
142 | ?? ($this->defaultAdapter ? get_class($this->defaultAdapter) : ''); |
143 | return $default === 'Laminas\Http\Client\Adapter\Curl'; |
144 | } |
145 | |
146 | /** |
147 | * Proxify an existing client. |
148 | * |
149 | * Returns the client given as argument with appropriate proxy setup. |
150 | * |
151 | * @param \Laminas\Http\Client $client HTTP client |
152 | * @param array $options Laminas ProxyAdapter options |
153 | * |
154 | * @return \Laminas\Http\Client |
155 | */ |
156 | public function proxify(\Laminas\Http\Client $client, array $options = []) |
157 | { |
158 | if ($this->proxyConfig) { |
159 | $host = $client->getUri()->getHost(); |
160 | if (null === $host || !$this->isLocal($host)) { |
161 | $proxyType = $this->proxyConfig['proxy_type'] ?? 'default'; |
162 | |
163 | if (in_array($proxyType, ['socks5', 'socks5_hostname'])) { |
164 | $adapter = new \Laminas\Http\Client\Adapter\Curl(); |
165 | // Apply proxy options for Curl adapter: |
166 | $this->setCurlProxyOptions($adapter); |
167 | $client->setAdapter($adapter); |
168 | } elseif ($proxyType == 'default') { |
169 | // If the user has manually configured a Curl adapter, |
170 | // configure it for proxy compatibility; otherwise, create |
171 | // a fresh Proxy adapter. |
172 | if ($this->hasCurlAdapterAsDefault()) { |
173 | $adapter = new \Laminas\Http\Client\Adapter\Curl(); |
174 | $this->setCurlProxyOptions($adapter); |
175 | } else { |
176 | $adapter = new \Laminas\Http\Client\Adapter\Proxy(); |
177 | $options = array_replace($this->proxyConfig, $options); |
178 | $adapter->setOptions($options); |
179 | } |
180 | $client->setAdapter($adapter); |
181 | } |
182 | } |
183 | } |
184 | return $client; |
185 | } |
186 | |
187 | /** |
188 | * Perform a GET request. |
189 | * |
190 | * @param string $url Request URL |
191 | * @param array $params Request parameters |
192 | * @param float $timeout Request timeout in seconds |
193 | * @param array $headers Request headers |
194 | * |
195 | * @return \Laminas\Http\Response |
196 | */ |
197 | public function get( |
198 | $url, |
199 | array $params = [], |
200 | $timeout = null, |
201 | array $headers = [] |
202 | ) { |
203 | if ($params) { |
204 | $query = $this->createQueryString($params); |
205 | if (str_contains($url, '?')) { |
206 | $url .= '&' . $query; |
207 | } else { |
208 | $url .= '?' . $query; |
209 | } |
210 | } |
211 | $client |
212 | = $this->createClient($url, \Laminas\Http\Request::METHOD_GET, $timeout); |
213 | if ($headers) { |
214 | $client->setHeaders($headers); |
215 | } |
216 | return $this->send($client); |
217 | } |
218 | |
219 | /** |
220 | * Perform a POST request. |
221 | * |
222 | * @param string $url Request URL |
223 | * @param mixed $body Request body document |
224 | * @param string $type Request body content type |
225 | * @param float $timeout Request timeout in seconds |
226 | * @param array $headers Request http-headers |
227 | * |
228 | * @return \Laminas\Http\Response |
229 | */ |
230 | public function post( |
231 | $url, |
232 | $body = null, |
233 | $type = 'application/octet-stream', |
234 | $timeout = null, |
235 | array $headers = [] |
236 | ) { |
237 | $client = $this |
238 | ->createClient($url, \Laminas\Http\Request::METHOD_POST, $timeout); |
239 | $client->setRawBody($body); |
240 | $client->setHeaders( |
241 | array_merge( |
242 | ['Content-Type' => $type, 'Content-Length' => strlen($body ?? '')], |
243 | $headers |
244 | ) |
245 | ); |
246 | return $this->send($client); |
247 | } |
248 | |
249 | /** |
250 | * Post form data. |
251 | * |
252 | * @param string $url Request URL |
253 | * @param array $params Form data |
254 | * @param float $timeout Request timeout in seconds |
255 | * |
256 | * @return \Laminas\Http\Response |
257 | */ |
258 | public function postForm($url, array $params = [], $timeout = null) |
259 | { |
260 | $body = $this->createQueryString($params); |
261 | return $this->post( |
262 | $url, |
263 | $body, |
264 | \Laminas\Http\Client::ENC_URLENCODED, |
265 | $timeout |
266 | ); |
267 | } |
268 | |
269 | /** |
270 | * Set a default HTTP adapter (primarily for testing purposes). |
271 | * |
272 | * @param \Laminas\Http\Client\Adapter\AdapterInterface $adapter Adapter |
273 | * |
274 | * @return void |
275 | */ |
276 | public function setDefaultAdapter( |
277 | \Laminas\Http\Client\Adapter\AdapterInterface $adapter |
278 | ) { |
279 | $this->defaultAdapter = $adapter; |
280 | } |
281 | |
282 | /** |
283 | * Return a new HTTP client. |
284 | * |
285 | * @param string $url Target URL |
286 | * @param string $method Request method |
287 | * @param float $timeout Request timeout in seconds |
288 | * |
289 | * @return \Laminas\Http\Client |
290 | */ |
291 | public function createClient( |
292 | $url = null, |
293 | $method = \Laminas\Http\Request::METHOD_GET, |
294 | $timeout = null |
295 | ) { |
296 | $client = new \Laminas\Http\Client(); |
297 | $client->setMethod($method); |
298 | if (!empty($this->defaults)) { |
299 | $client->setOptions($this->defaults); |
300 | } |
301 | if (null !== $this->defaultAdapter) { |
302 | $client->setAdapter($this->defaultAdapter); |
303 | } |
304 | if (null !== $url) { |
305 | $client->setUri($url); |
306 | } |
307 | if ($timeout) { |
308 | $client->setOptions(['timeout' => $timeout]); |
309 | } |
310 | $this->proxify($client); |
311 | return $client; |
312 | } |
313 | |
314 | /// Internal API |
315 | |
316 | /** |
317 | * Return query string based on params. |
318 | * |
319 | * @param array $params Parameters |
320 | * |
321 | * @return string |
322 | */ |
323 | protected function createQueryString(array $params = []) |
324 | { |
325 | if ($this->isAssocParams($params)) { |
326 | return http_build_query($params); |
327 | } else { |
328 | return implode('&', $params); |
329 | } |
330 | } |
331 | |
332 | /** |
333 | * Send HTTP request and return response. |
334 | * |
335 | * @param \Laminas\Http\Client $client HTTP client to use |
336 | * |
337 | * @throws Exception\RuntimeException |
338 | * @return \Laminas\Http\Response |
339 | * |
340 | * @todo Catch more exceptions, maybe? |
341 | */ |
342 | protected function send(\Laminas\Http\Client $client) |
343 | { |
344 | try { |
345 | $response = $client->send(); |
346 | } catch (\Laminas\Http\Client\Exception\RuntimeException $e) { |
347 | throw new Exception\RuntimeException( |
348 | sprintf('Laminas HTTP Client exception: %s', $e), |
349 | -1, |
350 | $e |
351 | ); |
352 | } |
353 | return $response; |
354 | } |
355 | |
356 | /** |
357 | * Return TRUE if argument is an associative array. |
358 | * |
359 | * @param array $array Array to test |
360 | * |
361 | * @return boolean |
362 | */ |
363 | public static function isAssocParams(array $array) |
364 | { |
365 | foreach (array_keys($array) as $key) { |
366 | if (!is_numeric($key)) { |
367 | return true; |
368 | } |
369 | } |
370 | return false; |
371 | } |
372 | |
373 | /** |
374 | * Return TRUE if argument refers to localhost. |
375 | * |
376 | * @param string $host Host to check |
377 | * |
378 | * @return boolean |
379 | */ |
380 | protected function isLocal($host) |
381 | { |
382 | return preg_match($this->localAddressesRegEx, $host); |
383 | } |
384 | } |