Passed
Push — master ( 158dd5...473203 )
by Radu
02:49
created

CurlBrowser::headerCallback()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
namespace WebServCo\Framework;
3
4
use WebServCo\Framework\Http\Method;
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
    protected $responseHeadersArray;
24
    protected $responseHeaderArray;
25
26
    protected $logger;
27
28
    protected $curlError;
29
30
    public function __construct(\WebServCo\Framework\Interfaces\LoggerInterface $logger)
31
    {
32
        $this->logger = $logger;
33
        $this->debug = false;
34
        $this->skipSslVerification = false;
35
        $this->requestHeaders = [];
36
    }
37
38
    public function get($url)
39
    {
40
        $this->setMethod(Method::GET);
41
        return $this->retrieve($url);
42
    }
43
44
    public function getRequestHeaders()
45
    {
46
        return $this->requestHeaders;
47
    }
48
49
    public function getResponseHeaders()
50
    {
51
        return $this->responseHeaders;
52
    }
53
54
    public function head($url)
55
    {
56
        $this->setMethod(Method::HEAD);
57
        return $this->retrieve($url);
58
    }
59
60
    public function post($url, $postData = null)
61
    {
62
        $this->setMethod(Method::POST);
63
        if (!empty($postData)) {
64
            $this->setPostData($postData);
65
        }
66
        return $this->retrieve($url);
67
    }
68
69
    public function retrieve($url)
70
    {
71
        $this->debugInit();
72
73
        $this->curl = curl_init();
74
        if (!is_resource($this->curl)) {
75
            throw new ApplicationException('Not a valid resource');
76
        }
77
78
        $this->setCurlOptions($url);
79
80
        $this->debugDo();
81
82
        $this->handleRequestMethod();
83
84
        $this->setRequestHeaders();
85
86
        $this->processResponse();
87
88
        $this->debugInfo = curl_getinfo($this->curl);
89
90
        curl_close($this->curl);
91
92
        $httpCode = $this->getHttpCode();
93
94
        if (empty($httpCode)) {
95
            throw new ApplicationException(sprintf("Empty HTTP status code. cURL error: %s", $this->curlError));
96
        }
97
98
        $body = trim($this->response);
99
100
        $this->processResponseHeaders();
101
102
        $this->debugFinish();
103
104
        return new \WebServCo\Framework\Http\Response(
105
            $body,
106
            $httpCode,
107
            end($this->responseHeaders)
108
        );
109
    }
110
111
    public function setDebug($debug)
112
    {
113
        $this->debug = (bool) $debug;
114
    }
115
116
    public function setMethod($method)
117
    {
118
        if (!in_array($method, Method::getSupported())) {
119
            throw new ApplicationException('Unsupported method');
120
        }
121
        $this->method = $method;
122
        return true;
123
    }
124
125
    public function setPostData($postData)
126
    {
127
        if (is_array($postData)) {
128
            $this->postData = [];
129
            foreach ($postData as $key => $value) {
130
                if (is_array($value)) {
131
                    throw new \InvalidArgumentException('POST value can not be an array');
132
                }
133
                $this->postData[$key] = $value;
134
            }
135
            return true;
136
        }
137
        $this->postData = $postData;
138
        return true;
139
    }
140
141
    public function setRequestHeader($name, $value)
142
    {
143
        $this->requestHeaders[$name] = $value;
144
    }
145
146
    public function setSkipSSlVerification($skipSslVerification)
147
    {
148
        $this->skipSslVerification = (bool) $skipSslVerification;
149
    }
150
151
    protected function debugDo()
152
    {
153
        if ($this->debug) {
154
            //curl_setopt($this->curl, CURLINFO_HEADER_OUT, 1); /* verbose not working if this is enabled */
155
            curl_setopt($this->curl, CURLOPT_VERBOSE, 1);
156
            curl_setopt($this->curl, CURLOPT_STDERR, $this->debugStderr);
157
            return false;
158
        }
159
        return false;
160
    }
161
162
    protected function debugFinish()
163
    {
164
        if ($this->debug) {
165
            fclose($this->debugStderr);
166
            $this->debugOutput = ob_get_clean();
167
168
            $this->logger->debug('CURL INFO:', $this->debugInfo);
169
            $this->logger->debug('CURL VERBOSE:', $this->debugOutput);
170
            $this->logger->debug('CURL RESPONSE:', $this->response);
171
172
            return true;
173
        }
174
        return false;
175
    }
176
177
    protected function debugInit()
178
    {
179
        if ($this->debug) {
180
            ob_start();
181
            $this->debugStderr = fopen('php://output', 'w');
182
            return true;
183
        }
184
        return false;
185
    }
186
187
    protected function getHttpCode()
188
    {
189
        return isset($this->debugInfo['http_code']) ? $this->debugInfo['http_code']: false;
190
    }
191
192
    protected function handleRequestMethod()
193
    {
194
        if (!is_resource($this->curl)) {
195
            throw new ApplicationException('Not a valid resource');
196
        }
197
198
        switch ($this->method) {
199
            case Method::POST:
200
                curl_setopt($this->curl, CURLOPT_POST, true);
201
                if (!empty($this->postData)) {
202
                    curl_setopt($this->curl, CURLOPT_POSTFIELDS, $this->postData);
203
                    if (!is_array($this->postData)) {
204
                        $this->setRequestHeader('Content-Length', mb_strlen($this->postData));
205
                    }
206
                }
207
                break;
208
            case Method::HEAD:
209
                curl_setopt($this->curl, CURLOPT_NOBODY, true);
210
                break;
211
        }
212
    }
213
214
    protected function headerCallback($curlResource, $headerData)
215
    {
216
        $headerDataTrimmed = trim($headerData);
217
        if (empty($headerDataTrimmed)) {
218
            $this->responseHeadersArray[] = $this->responseHeaderArray;
219
            $this->responseHeaderArray = [];
220
        }
221
        $this->responseHeaderArray[] = $headerData;
222
223
        return strlen($headerData);
224
    }
225
226
    protected function parseRequestHeaders($headers)
227
    {
228
        $data = [];
229
        foreach ($headers as $k => $v) {
230
            if (is_array($v)) {
231
                foreach ($v as $item) {
232
                    $data[] = sprintf('%s: %s', $k, $item);
233
                }
234
            } else {
235
                $data[] = sprintf('%s: %s', $k, $v);
236
            }
237
        }
238
        return $data;
239
    }
240
241
    protected function parseResponseHeadersArray($responseHeadersArray = [])
242
    {
243
        $headers = [];
244
245
        foreach ($responseHeadersArray as $index => $line) {
246
            if (0 === $index) {
247
                continue; /* we'll get the status code elsewhere */
248
            }
249
            $parts = explode(': ', $line, 2);
250
            if (!isset($parts[1])) {
251
                continue; // invalid header (missing colon)
252
            }
253
            list($key, $value) = $parts;
254
            if (isset($headers[$key])) {
255
                if (!is_array($headers[$key])) {
256
                    $headers[$key] = [$headers[$key]];
257
                }
258
                // check cookies
259
                if ('Set-Cookie' == $key) {
260
                    $parts = explode('=', $value, 2);
261
                    $cookieName = $parts[0];
262
                    if (is_array($headers[$key])) {
263
                        foreach ($headers[$key] as $cookieIndex => $existingCookie) {
264
                            //check if we already have a cookie with the same name
265
                            if (0 === mb_stripos($existingCookie, $cookieName)) {
266
                                // remove previous cookie with the same name
267
                                unset($headers[$key][$cookieIndex]);
268
                            }
269
                        }
270
                    }
271
                }
272
                $headers[$key][] = trim($value);
273
                $headers[$key] = array_values((array) $headers[$key]); // re-index array
274
            } else {
275
                $headers[$key] = trim($value);
276
            }
277
        }
278
        return $headers;
279
    }
280
281
    protected function processResponse()
282
    {
283
        $this->response = curl_exec($this->curl);
284
        $this->curlError = curl_error($this->curl);
285
        if (false === $this->response) {
286
            throw new ApplicationException(sprintf("cURL error: %s", $this->curlError));
287
        }
288
    }
289
290
    protected function processResponseHeaders()
291
    {
292
        $this->responseHeaders = [];
293
        foreach ($this->responseHeadersArray as $item) {
294
            $this->responseHeaders[] = $this->parseResponseHeadersArray($item);
295
        }
296
    }
297
298
    protected function setCurlOptions($url)
299
    {
300
        if (!is_resource($this->curl)) {
301
            throw new ApplicationException('Not a valid resource');
302
        }
303
304
        // set options
305
        curl_setopt_array(
306
            $this->curl,
307
            [
308
                CURLOPT_RETURNTRANSFER => true, /* return instead of outputting */
309
                CURLOPT_URL => $url,
310
                CURLOPT_HEADER => false, /* do not include the header in the output */
311
                CURLOPT_FOLLOWLOCATION => true, /* follow redirects */
312
                CURLOPT_CONNECTTIMEOUT => 60, // The number of seconds to wait while trying to connect.
313
                CURLOPT_TIMEOUT => 60, // The maximum number of seconds to allow cURL functions to execute.
314
            ]
315
        );
316
        // check if we should ignore ssl errors
317
        if ($this->skipSslVerification) {
318
            // stop cURL from verifying the peer's certificate
319
            curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false);
320
            // don't check the existence of a common name in the SSL peer certificate
321
            curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, 0);
322
        }
323
    }
324
325
    protected function setRequestHeaders()
326
    {
327
        if (!is_resource($this->curl)) {
328
            throw new ApplicationException('Not a valid resource');
329
        }
330
331
        // set headers
332
        if (!empty($this->requestHeaders)) {
333
            curl_setopt(
334
                $this->curl,
335
                CURLOPT_HTTPHEADER,
336
                $this->parseRequestHeaders($this->requestHeaders)
337
            );
338
        }
339
340
        // Callback to process response headers
341
        curl_setopt($this->curl, CURLOPT_HEADERFUNCTION, [$this, 'headerCallback']);
342
    }
343
}
344