1<?php
2
3namespace Elastica\Transport;
4
5use Elastica\Exception\Connection\HttpException;
6use Elastica\Exception\ConnectionException;
7use Elastica\Exception\PartialShardFailureException;
8use Elastica\Exception\ResponseException;
9use Elastica\JSON;
10use Elastica\Request;
11use Elastica\Response;
12use Elastica\Util;
13
14/**
15 * Elastica Http Transport object.
16 *
17 * @author Nicolas Ruflin <spam@ruflin.com>
18 */
19class Http extends AbstractTransport
20{
21    /**
22     * Http scheme.
23     *
24     * @var string Http scheme
25     */
26    protected $_scheme = 'http';
27
28    /**
29     * Curl resource to reuse.
30     *
31     * @var \CurlHandle|resource|null Curl resource to reuse
32     */
33    protected static $_curlConnection;
34
35    /**
36     * Makes calls to the elasticsearch server.
37     *
38     * All calls that are made to the server are done through this function
39     *
40     * @param array<string, mixed> $params Host, Port, ...
41     *
42     * @throws ConnectionException
43     * @throws ResponseException
44     * @throws HttpException
45     *
46     * @return Response Response object
47     */
48    public function exec(Request $request, array $params): Response
49    {
50        $connection = $this->getConnection();
51
52        $conn = $this->_getConnection($connection->isPersistent());
53
54        // If url is set, url is taken. Otherwise port, host and path
55        $url = $connection->hasConfig('url') ? $connection->getConfig('url') : '';
56
57        if (!empty($url)) {
58            $baseUri = $url;
59        } else {
60            $baseUri = $this->_scheme.'://'.$connection->getHost().':'.$connection->getPort().'/'.$connection->getPath();
61        }
62
63        $requestPath = $request->getPath();
64        if (!Util::isDateMathEscaped($requestPath)) {
65            $requestPath = Util::escapeDateMath($requestPath);
66        }
67
68        $baseUri .= $requestPath;
69
70        $query = $request->getQuery();
71
72        if (!empty($query)) {
73            $baseUri .= '?'.\http_build_query(
74                $this->sanityzeQueryStringBool($query)
75            );
76        }
77
78        \curl_setopt($conn, \CURLOPT_URL, $baseUri);
79        \curl_setopt($conn, \CURLOPT_TIMEOUT_MS, $connection->getTimeout() * 1000);
80        \curl_setopt($conn, \CURLOPT_FORBID_REUSE, 0);
81
82        // Tell ES that we support the compressed responses
83        // An "Accept-Encoding" header containing all supported encoding types is sent
84        // curl will decode the response automatically if the response is encoded
85        \curl_setopt($conn, \CURLOPT_ENCODING, '');
86
87        /* @see Connection::setConnectTimeout() */
88        $connectTimeoutMs = $connection->getConnectTimeout() * 1000;
89
90        // Let's only apply this value if the number of ms is greater than or equal to "1".
91        // In case "0" is passed as an argument, the value is reset to its default (300 s)
92        if ($connectTimeoutMs >= 1) {
93            \curl_setopt($conn, \CURLOPT_CONNECTTIMEOUT_MS, $connectTimeoutMs);
94        }
95
96        if (null !== $proxy = $connection->getProxy()) {
97            \curl_setopt($conn, \CURLOPT_PROXY, $proxy);
98        }
99
100        $username = $connection->getUsername();
101        $password = $connection->getPassword();
102        if (null !== $username && null !== $password) {
103            \curl_setopt($conn, \CURLOPT_HTTPAUTH, $this->_getAuthType());
104            \curl_setopt($conn, \CURLOPT_USERPWD, "{$username}:{$password}");
105        }
106
107        $this->_setupCurl($conn);
108
109        $headersConfig = $connection->hasConfig('headers') ? $connection->getConfig('headers') : [];
110
111        $headers = [];
112
113        if (!empty($headersConfig)) {
114            foreach ($headersConfig as $header => $headerValue) {
115                $headers[] = $header.': '.$headerValue;
116            }
117        }
118
119        // TODO: REFACTOR
120        $data = $request->getData();
121        $httpMethod = $request->getMethod();
122
123        $headers[] = 'Content-Type: '.$request->getContentType();
124
125        if (!empty($data)) {
126            if ($this->hasParam('postWithRequestBody') && true == $this->getParam('postWithRequestBody')) {
127                $httpMethod = Request::POST;
128            }
129
130            if (\is_array($data)) {
131                $content = JSON::stringify($data, \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES);
132            } else {
133                $content = $data;
134
135                // Escaping of / not necessary. Causes problems in base64 encoding of files
136                $content = \str_replace('\/', '/', $content);
137            }
138
139            if ($connection->hasCompression()) {
140                // Compress the body of the request ...
141                \curl_setopt($conn, \CURLOPT_POSTFIELDS, \gzencode($content));
142
143                // ... and tell ES that it is compressed
144                $headers[] = 'Content-Encoding: gzip';
145            } else {
146                \curl_setopt($conn, \CURLOPT_POSTFIELDS, $content);
147            }
148        } else {
149            \curl_setopt($conn, \CURLOPT_POSTFIELDS, '');
150        }
151
152        \curl_setopt($conn, \CURLOPT_HTTPHEADER, $headers);
153
154        \curl_setopt($conn, \CURLOPT_NOBODY, 'HEAD' === $httpMethod);
155
156        \curl_setopt($conn, \CURLOPT_CUSTOMREQUEST, $httpMethod);
157
158        $start = \microtime(true);
159
160        // cURL opt returntransfer leaks memory, therefore OB instead.
161        \ob_start();
162        \curl_exec($conn);
163        $responseString = \ob_get_clean();
164
165        $end = \microtime(true);
166
167        // Checks if error exists
168        $errorNumber = \curl_errno($conn);
169
170        $response = new Response($responseString, \curl_getinfo($conn, \CURLINFO_RESPONSE_CODE));
171        $response->setQueryTime($end - $start);
172        $response->setTransferInfo(\curl_getinfo($conn));
173        if ($connection->hasConfig('bigintConversion')) {
174            $response->setJsonBigintConversion($connection->getConfig('bigintConversion'));
175        }
176
177        if ($response->hasError()) {
178            throw new ResponseException($request, $response);
179        }
180
181        if ($response->hasFailedShards()) {
182            throw new PartialShardFailureException($request, $response);
183        }
184
185        if ($errorNumber > 0) {
186            throw new HttpException($errorNumber, $request, $response);
187        }
188
189        return $response;
190    }
191
192    /**
193     * Called to add additional curl params.
194     *
195     * @param \CurlHandle|resource $curlConnection Curl connection
196     */
197    protected function _setupCurl($curlConnection): void
198    {
199        if ($this->getConnection()->hasConfig('curl')) {
200            foreach ($this->getConnection()->getConfig('curl') as $key => $param) {
201                \curl_setopt($curlConnection, $key, $param);
202            }
203        }
204    }
205
206    /**
207     * Return Curl resource.
208     *
209     * @param bool $persistent False if not persistent connection
210     *
211     * @return \CurlHandle|resource Connection resource
212     */
213    protected function _getConnection(bool $persistent = true)
214    {
215        if (!$persistent || !self::$_curlConnection) {
216            self::$_curlConnection = \curl_init();
217        }
218
219        return self::$_curlConnection;
220    }
221
222    /**
223     * @return int
224     */
225    protected function _getAuthType()
226    {
227        switch ($this->_connection->getAuthType()) {
228            case 'digest':
229                return \CURLAUTH_DIGEST;
230            case 'gssnegotiate':
231                return \CURLAUTH_GSSNEGOTIATE;
232            case 'ntlm':
233                return \CURLAUTH_NTLM;
234            case 'basic':
235                return \CURLAUTH_BASIC;
236            default:
237                return \CURLAUTH_ANY;
238        }
239    }
240}
241