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 resource 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 Request $request
41     * @param array   $params  Host, Port, ...
42     *
43     * @throws ConnectionException
44     * @throws ResponseException
45     * @throws HttpException
46     *
47     * @return Response Response object
48     */
49    public function exec(Request $request, array $params): Response
50    {
51        $connection = $this->getConnection();
52
53        $conn = $this->_getConnection($connection->isPersistent());
54
55        // If url is set, url is taken. Otherwise port, host and path
56        $url = $connection->hasConfig('url') ? $connection->getConfig('url') : '';
57
58        if (!empty($url)) {
59            $baseUri = $url;
60        } else {
61            $baseUri = $this->_scheme.'://'.$connection->getHost().':'.$connection->getPort().'/'.$connection->getPath();
62        }
63
64        $requestPath = $request->getPath();
65        if (!Util::isDateMathEscaped($requestPath)) {
66            $requestPath = Util::escapeDateMath($requestPath);
67        }
68
69        $baseUri .= $requestPath;
70
71        $query = $request->getQuery();
72
73        if (!empty($query)) {
74            $baseUri .= '?'.\http_build_query(
75                $this->sanityzeQueryStringBool($query)
76                );
77        }
78
79        \curl_setopt($conn, CURLOPT_URL, $baseUri);
80        \curl_setopt($conn, CURLOPT_TIMEOUT, $connection->getTimeout());
81        \curl_setopt($conn, CURLOPT_FORBID_REUSE, 0);
82
83        // Tell ES that we support the compressed responses
84        // An "Accept-Encoding" header containing all supported encoding types is sent
85        // curl will decode the response automatically if the response is encoded
86        \curl_setopt($conn, CURLOPT_ENCODING, '');
87
88        /* @see Connection::setConnectTimeout() */
89        $connectTimeout = $connection->getConnectTimeout();
90        if ($connectTimeout > 0) {
91            \curl_setopt($conn, CURLOPT_CONNECTTIMEOUT, $connectTimeout);
92        }
93
94        $proxy = $connection->getProxy();
95
96        // See: https://github.com/facebook/hhvm/issues/4875
97        if (\is_null($proxy) && \defined('HHVM_VERSION')) {
98            $proxy = \getenv('http_proxy') ?: null;
99        }
100
101        if (!\is_null($proxy)) {
102            \curl_setopt($conn, CURLOPT_PROXY, $proxy);
103        }
104
105        $username = $connection->getUsername();
106        $password = $connection->getPassword();
107        if (!\is_null($username) && !\is_null($password)) {
108            \curl_setopt($conn, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
109            \curl_setopt($conn, CURLOPT_USERPWD, "$username:$password");
110        }
111
112        $this->_setupCurl($conn);
113
114        $headersConfig = $connection->hasConfig('headers') ? $connection->getConfig('headers') : [];
115
116        $headers = [];
117
118        if (!empty($headersConfig)) {
119            $headers = [];
120            foreach ($headersConfig as $header => $headerValue) {
121                \array_push($headers, $header.': '.$headerValue);
122            }
123        }
124
125        // TODO: REFACTOR
126        $data = $request->getData();
127        $httpMethod = $request->getMethod();
128
129        if (!empty($data) || '0' === $data) {
130            if ($this->hasParam('postWithRequestBody') && true == $this->getParam('postWithRequestBody')) {
131                $httpMethod = Request::POST;
132            }
133
134            if (\is_array($data)) {
135                $content = JSON::stringify($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
136            } else {
137                $content = $data;
138
139                // Escaping of / not necessary. Causes problems in base64 encoding of files
140                $content = \str_replace('\/', '/', $content);
141            }
142
143            \array_push($headers, \sprintf('Content-Type: %s', $request->getContentType()));
144            if ($connection->hasCompression()) {
145                // Compress the body of the request ...
146                \curl_setopt($conn, CURLOPT_POSTFIELDS, \gzencode($content));
147
148                // ... and tell ES that it is compressed
149                \array_push($headers, 'Content-Encoding: gzip');
150            } else {
151                \curl_setopt($conn, CURLOPT_POSTFIELDS, $content);
152            }
153        } else {
154            \curl_setopt($conn, CURLOPT_POSTFIELDS, '');
155        }
156
157        \curl_setopt($conn, CURLOPT_HTTPHEADER, $headers);
158
159        \curl_setopt($conn, CURLOPT_NOBODY, 'HEAD' == $httpMethod);
160
161        \curl_setopt($conn, CURLOPT_CUSTOMREQUEST, $httpMethod);
162
163        $start = \microtime(true);
164
165        // cURL opt returntransfer leaks memory, therefore OB instead.
166        \ob_start();
167        \curl_exec($conn);
168        $responseString = \ob_get_clean();
169
170        $end = \microtime(true);
171
172        // Checks if error exists
173        $errorNumber = \curl_errno($conn);
174
175        $response = new Response($responseString, \curl_getinfo($conn, CURLINFO_HTTP_CODE));
176        $response->setQueryTime($end - $start);
177        $response->setTransferInfo(\curl_getinfo($conn));
178        if ($connection->hasConfig('bigintConversion')) {
179            $response->setJsonBigintConversion($connection->getConfig('bigintConversion'));
180        }
181
182        if ($response->hasError()) {
183            throw new ResponseException($request, $response);
184        }
185
186        if ($response->hasFailedShards()) {
187            throw new PartialShardFailureException($request, $response);
188        }
189
190        if ($errorNumber > 0) {
191            throw new HttpException($errorNumber, $request, $response);
192        }
193
194        return $response;
195    }
196
197    /**
198     * Called to add additional curl params.
199     *
200     * @param resource $curlConnection Curl connection
201     */
202    protected function _setupCurl($curlConnection)
203    {
204        if ($this->getConnection()->hasConfig('curl')) {
205            foreach ($this->getConnection()->getConfig('curl') as $key => $param) {
206                \curl_setopt($curlConnection, $key, $param);
207            }
208        }
209    }
210
211    /**
212     * Return Curl resource.
213     *
214     * @param bool $persistent False if not persistent connection
215     *
216     * @return resource Connection resource
217     */
218    protected function _getConnection(bool $persistent = true)
219    {
220        if (!$persistent || !self::$_curlConnection) {
221            self::$_curlConnection = \curl_init();
222        }
223
224        return self::$_curlConnection;
225    }
226}
227