Passed
Push — master ( fb5c02...277033 )
by Radu
01:30
created

CurlBrowser::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
namespace WebServCo\Framework;
3
4
use WebServCo\Framework\Http;
5
use WebServCo\Framework\Exceptions\ApplicationException;
6
7
final class CurlBrowser implements
8
    \WebServCo\Framework\Interfaces\HttpBrowserInterface
9
{
10
    protected $debug;
11
    protected $skipSslVerification;
12
    protected $requestHeaders;
13
14
    protected $method;
15
    protected $postData;
16
17
    protected $curl;
18
    protected $debugStderr;
19
    protected $debugOutput;
20
    protected $debugInfo;
21
    protected $response;
22
    protected $responseHeaders;
23
24
    protected $logger;
25
26
    protected $curlError;
27
28
    public function __construct(\WebServCo\Framework\Interfaces\LoggerInterface $logger)
29
    {
30
        $this->logger = $logger;
31
        $this->debug = false;
32
        $this->skipSslVerification = false;
33
        $this->requestHeaders = [];
34
    }
35
36
    public function setDebug(bool $debug)
37
    {
38
        $this->debug = $debug;
39
    }
40
41
    public function setSkipSSlVerification(bool $skipSslVerification)
42
    {
43
        $this->skipSslVerification = $skipSslVerification;
44
    }
45
46
    public function setRequestHeader($name, $value)
47
    {
48
        $this->requestHeaders[$name] = $value;
49
    }
50
51
    public function getRequestHeaders()
52
    {
53
        return $this->requestHeaders;
54
    }
55
56
    public function getResponseHeaders()
57
    {
58
        return $this->responseHeaders;
59
    }
60
61
    public function get($url)
62
    {
63
        $this->setMethod(Http::METHOD_GET);
64
        return $this->retrieve($url);
65
    }
66
67
    public function head($url)
68
    {
69
        $this->setMethod(Http::METHOD_HEAD);
70
        return $this->retrieve($url);
71
    }
72
73
    public function post($url, $postData = null)
74
    {
75
        $this->setMethod(Http::METHOD_POST);
76
        $this->setPostData($postData);
77
        return $this->retrieve($url);
78
    }
79
80
    protected function setMethod($method)
81
    {
82
        if (!in_array($method, Http::getMethods())) {
83
            throw new ApplicationException('Unsupported method');
84
        }
85
        $this->method = $method;
86
        return true;
87
    }
88
89
    protected function setPostData($postData)
90
    {
91
        if (is_array($postData)) {
92
            $this->postData = [];
93
            foreach ($postData as $key => $value) {
94
                if (is_array($value)) {
95
                    throw new \InvalidArgumentException('POST value can not be an array');
96
                }
97
                $this->postData[$key] = $value;
98
            }
99
            return true;
100
        }
101
        $this->postData = $postData;
102
        return true;
103
    }
104
105
    protected function debugInit()
106
    {
107
        if ($this->debug) {
108
            ob_start();
109
            $this->debugStderr = fopen('php://output', 'w');
110
            return true;
111
        }
112
        return false;
113
    }
114
115
    protected function debugDo()
116
    {
117
        if ($this->debug) {
118
            //curl_setopt($this->curl, CURLINFO_HEADER_OUT, 1); /* verbose not working if this is enabled */
119
            curl_setopt($this->curl, CURLOPT_VERBOSE, 1);
120
            curl_setopt($this->curl, CURLOPT_STDERR, $this->debugStderr);
121
            return false;
122
        }
123
        return false;
124
    }
125
126
    protected function debugFinish()
127
    {
128
        if ($this->debug) {
129
            fclose($this->debugStderr);
130
            $this->debugOutput = ob_get_clean();
131
132
            $this->logger->debug('CURL INFO:', $this->debugInfo);
133
            $this->logger->debug('CURL VERBOSE:', $this->debugOutput);
134
            $this->logger->debug('CURL RESPONSE:', $this->response);
135
136
            return true;
137
        }
138
        return false;
139
    }
140
141
    protected function getHttpCode()
142
    {
143
        return isset($this->debugInfo['http_code']) ? $this->debugInfo['http_code']: false;
144
    }
145
146
    protected function parseRequestHeaders($headers)
147
    {
148
        $data = [];
149
        foreach ($headers as $k => $v) {
150
            if (is_array($v)) {
151
                foreach ($v as $item) {
152
                    $data[] = sprintf('%s: %s', $k, $item);
153
                }
154
            } else {
155
                $data[] = sprintf('%s: %s', $k, $v);
156
            }
157
        }
158
        return $data;
159
    }
160
161
    protected function parseResponseHeaders($headerString)
162
    {
163
        $headers = [];
164
        $lines = explode("\r\n", $headerString);
165
        foreach ($lines as $index => $line) {
166
            if (0 === $index) {
167
                continue; /* we'll get the status code elsewhere */
168
            }
169
            $parts = explode(': ', $line, 2);
170
            if (!isset($parts[1])) {
171
                continue; // invalid header (missing colon)
172
            }
173
            list($key, $value) = $parts;
174
            if (isset($headers[$key])) {
175
                if (!is_array($headers[$key])) {
176
                    $headers[$key] = [$headers[$key]];
177
                }
178
                // check cookies
179
                if ('Set-Cookie' == $key) {
180
                    $parts = explode('=', $value, 2);
181
                    $cookieName = $parts[0];
182
                    if (is_array($headers[$key])) {
183
                        foreach ($headers[$key] as $cookieIndex => $existingCookie) {
184
                            //check if we already have a cookie with the same name
185
                            if (0 === mb_stripos($existingCookie, $cookieName)) {
186
                                // remove previous cookie with the same name
187
                                unset($headers[$key][$cookieIndex]);
188
                            }
189
                        }
190
                    }
191
                }
192
                $headers[$key][] = $value;
193
                $headers[$key] = array_values((array) $headers[$key]); // re-index array
194
            } else {
195
                $headers[$key] = $value;
196
            }
197
        }
198
        return $headers;
199
    }
200
201
    protected function retrieve($url)
202
    {
203
        $this->debugInit();
204
205
206
        $this->curl = curl_init();
207
        if (!is_resource($this->curl)) {
208
            throw new ApplicationException('Not a valid resource');
209
        }
210
        curl_setopt_array(
211
            $this->curl,
212
            [
213
                CURLOPT_RETURNTRANSFER => true, /* return instead of outputting */
214
                CURLOPT_URL => $url,
215
                CURLOPT_HEADER => true, /* include the header in the output */
216
                CURLOPT_FOLLOWLOCATION => true, /* follow redirects */
217
                CURLOPT_CONNECTTIMEOUT => 60, // The number of seconds to wait while trying to connect.
218
                CURLOPT_TIMEOUT => 60, // The maximum number of seconds to allow cURL functions to execute.
219
            ]
220
        );
221
        if ($this->skipSslVerification) {
222
            // stop cURL from verifying the peer's certificate
223
            curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false);
224
            // don't check the existence of a common name in the SSL peer certificate
225
            curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, 0);
226
        }
227
228
        $this->debugDo();
229
230
        switch ($this->method) {
231
            case Http::METHOD_POST:
232
                curl_setopt($this->curl, CURLOPT_POST, true);
233
                if (!empty($this->postData)) {
234
                    curl_setopt($this->curl, CURLOPT_POSTFIELDS, $this->postData);
235
                    if (!is_array($this->postData)) {
236
                        $this->setRequestHeader('Content-Length', mb_strlen($this->postData));
237
                    }
238
                }
239
                break;
240
            case Http::METHOD_HEAD:
241
                curl_setopt($this->curl, CURLOPT_NOBODY, true);
242
                break;
243
        }
244
245
        if (!empty($this->requestHeaders)) {
246
            curl_setopt(
247
                $this->curl,
248
                CURLOPT_HTTPHEADER,
249
                $this->parseRequestHeaders($this->requestHeaders)
250
            );
251
        }
252
253
        $this->response = curl_exec($this->curl);
254
        $this->curlError = curl_error($this->curl);
255
        if (false === $this->response) {
256
            throw new ApplicationException(sprintf("cURL error: %s", $this->curlError));
257
        }
258
259
        $this->debugInfo = curl_getinfo($this->curl);
260
261
        curl_close($this->curl);
262
263
        $httpCode = $this->getHttpCode();
264
265
        if (empty($httpCode)) {
266
            throw new ApplicationException(sprintf("Empty HTTP status code. cURL error: %s", $this->curlError));
267
        }
268
269
        /**
270
         * For redirects, the response will contain evey header/body pair.
271
         * The last header/body will be at the end of the response.
272
         */
273
        $responseParts = explode("\r\n\r\n", (string) $this->response);
274
        $body = trim((string) array_pop($responseParts));
275
276
        $this->responseHeaders = [];
277
        foreach ($responseParts as $item) {
278
            $this->responseHeaders[] = $this->parseResponseHeaders($item);
279
        }
280
281
        $this->debugFinish();
282
283
        return new \WebServCo\Framework\HttpResponse(
284
            $body,
285
            $httpCode,
286
            end($this->responseHeaders)
287
        );
288
    }
289
}
290