Passed
Push — master ( f7ff8e...f4c65d )
by Radu
01:09
created

CurlBrowser::setDebug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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