1<?php
2
3declare(strict_types=1);
4
5namespace MaxMind\WebService\Http;
6
7use MaxMind\Exception\HttpException;
8
9/**
10 * This class is for internal use only. Semantic versioning does not not apply.
11 *
12 * @internal
13 */
14class CurlRequest implements Request
15{
16    /**
17     * @var resource
18     */
19    private $ch;
20
21    /**
22     * @var string
23     */
24    private $url;
25
26    /**
27     * @var array
28     */
29    private $options;
30
31    public function __construct(string $url, array $options)
32    {
33        $this->url = $url;
34        $this->options = $options;
35        $this->ch = $options['curlHandle'];
36    }
37
38    /**
39     * @throws HttpException
40     */
41    public function post(string $body): array
42    {
43        $curl = $this->createCurl();
44
45        curl_setopt($curl, CURLOPT_POST, true);
46        curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
47
48        return $this->execute($curl);
49    }
50
51    public function get(): array
52    {
53        $curl = $this->createCurl();
54
55        curl_setopt($curl, CURLOPT_HTTPGET, true);
56
57        return $this->execute($curl);
58    }
59
60    /**
61     * @return resource
62     */
63    private function createCurl()
64    {
65        curl_reset($this->ch);
66
67        $opts = [];
68        $opts[CURLOPT_URL] = $this->url;
69
70        if (!empty($this->options['caBundle'])) {
71            $opts[CURLOPT_CAINFO] = $this->options['caBundle'];
72        }
73
74        $opts[CURLOPT_ENCODING] = '';
75        $opts[CURLOPT_SSL_VERIFYHOST] = 2;
76        $opts[CURLOPT_FOLLOWLOCATION] = false;
77        $opts[CURLOPT_SSL_VERIFYPEER] = true;
78        $opts[CURLOPT_RETURNTRANSFER] = true;
79
80        $opts[CURLOPT_HTTPHEADER] = $this->options['headers'];
81        $opts[CURLOPT_USERAGENT] = $this->options['userAgent'];
82        $opts[CURLOPT_PROXY] = $this->options['proxy'];
83
84        // The defined()s are here as the *_MS opts are not available on older
85        // cURL versions
86        $connectTimeout = $this->options['connectTimeout'];
87        if (\defined('CURLOPT_CONNECTTIMEOUT_MS')) {
88            $opts[CURLOPT_CONNECTTIMEOUT_MS] = ceil($connectTimeout * 1000);
89        } else {
90            $opts[CURLOPT_CONNECTTIMEOUT] = ceil($connectTimeout);
91        }
92
93        $timeout = $this->options['timeout'];
94        if (\defined('CURLOPT_TIMEOUT_MS')) {
95            $opts[CURLOPT_TIMEOUT_MS] = ceil($timeout * 1000);
96        } else {
97            $opts[CURLOPT_TIMEOUT] = ceil($timeout);
98        }
99
100        curl_setopt_array($this->ch, $opts);
101
102        return $this->ch;
103    }
104
105    /**
106     * @param resource $curl
107     *
108     * @throws HttpException
109     */
110    private function execute($curl): array
111    {
112        $body = curl_exec($curl);
113        if ($errno = curl_errno($curl)) {
114            $errorMessage = curl_error($curl);
115
116            throw new HttpException(
117                "cURL error ({$errno}): {$errorMessage}",
118                0,
119                $this->url
120            );
121        }
122
123        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
124        $contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
125
126        return [
127          $statusCode,
128          // The PHP docs say "Content-Type: of the requested document. NULL
129          // indicates server did not send valid Content-Type: header" for
130          // CURLINFO_CONTENT_TYPE. However, it will return FALSE if no header
131          // is set. To keep our types simple, we return null in this case.
132          ($contentType === false ? null : $contentType),
133          $body,
134        ];
135    }
136}
137