Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.80% covered (success)
97.80%
89 / 91
92.31% covered (success)
92.31%
12 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpService
97.80% covered (success)
97.80%
89 / 91
92.31% covered (success)
92.31%
12 / 13
35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setCurlProxyOptions
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 hasCurlAdapterAsDefault
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 proxify
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 get
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 post
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 postForm
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultAdapter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createClient
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 createQueryString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 send
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 isAssocParams
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 isLocal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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
30namespace VuFindHttp;
31
32use function get_class;
33use function in_array;
34use 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 */
45class 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}