Completed
Push — master ( c482b6...ddabff )
by Jeroen van
23s queued 18s
created

CurlHelper::callFunction()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 0
cp 0
rs 9.8333
c 0
b 0
f 0
cc 4
nc 2
nop 3
crap 20
1
<?php
2
3
namespace VCR\Util;
4
5
use VCR\Request;
6
use VCR\Response;
7
8
/**
9
 * cURL helper class.
10
 */
11
class CurlHelper
12
{
13
    /**
14
     * @var array<int, string> list of cURL info constants
15
     */
16
    private static $curlInfoList = [
17
        //"certinfo"?
18
        CURLINFO_HTTP_CODE => 'http_code',
19
        CURLINFO_EFFECTIVE_URL => 'url',
20
        CURLINFO_TOTAL_TIME => 'total_time',
21
        CURLINFO_NAMELOOKUP_TIME => 'namelookup_time',
22
        CURLINFO_CONNECT_TIME => 'connect_time',
23
        CURLINFO_PRETRANSFER_TIME => 'pretransfer_time',
24
        CURLINFO_STARTTRANSFER_TIME => 'starttransfer_time',
25
        CURLINFO_REDIRECT_COUNT => 'redirect_count',
26
        CURLINFO_REDIRECT_TIME => 'redirect_time',
27
        CURLINFO_SIZE_UPLOAD => 'size_upload',
28
        CURLINFO_SIZE_DOWNLOAD => 'size_download',
29
        CURLINFO_SPEED_DOWNLOAD => 'speed_download',
30
        CURLINFO_SPEED_UPLOAD => 'speed_upload',
31
        CURLINFO_HEADER_SIZE => 'header_size',
32
        CURLINFO_HEADER_OUT => 'request_header',
33
        CURLINFO_FILETIME => 'filetime',
34
        CURLINFO_REQUEST_SIZE => 'request_size',
35
        CURLINFO_SSL_VERIFYRESULT => 'ssl_verify_result',
36
        CURLINFO_CONTENT_LENGTH_DOWNLOAD => 'download_content_length',
37
        CURLINFO_CONTENT_LENGTH_UPLOAD => 'upload_content_length',
38
        CURLINFO_CONTENT_TYPE => 'content_type',
39
    ];
40
41
    /**
42
     * Outputs a response depending on the set cURL option.
43
     *
44
     * The response body can be written to a file, returned, echoed or
45
     * passed to a custom function.
46
     *
47
     * The response header might be passed to a custom function.
48
     *
49
     * @param Response          $response    response which contains the response body
50
     * @param array<int, mixed> $curlOptions cURL options which are not stored within the Response
51
     * @param resource          $ch          cURL handle to add headers if needed
52
     */
53
    public static function handleOutput(Response $response, array $curlOptions, $ch): ?string
54
    {
55
        // If there is a header function set, feed the http status and headers to it.
56 140
        if (isset($curlOptions[CURLOPT_HEADERFUNCTION])) {
57
            $headerList = [HttpUtil::formatAsStatusString($response)];
58
            $headerList = array_merge($headerList, HttpUtil::formatHeadersForCurl($response->getHeaders()));
59 140
            $headerList[] = '';
60 7
            foreach ($headerList as $header) {
61 7
                self::callFunction($curlOptions[CURLOPT_HEADERFUNCTION], $ch, $header);
62 7
            }
63 7
        }
64 7
65 2
        $body = $response->getBody();
66 2
67
        if (!empty($curlOptions[CURLOPT_HEADER])) {
68 140
            $body = HttpUtil::formatAsStatusWithHeadersString($response).$body;
69
        }
70 140
71 7
        if (isset($curlOptions[CURLOPT_WRITEFUNCTION])) {
72 2
            self::callFunction($curlOptions[CURLOPT_WRITEFUNCTION], $ch, $body);
73
        } elseif (isset($curlOptions[CURLOPT_RETURNTRANSFER]) && true == $curlOptions[CURLOPT_RETURNTRANSFER]) {
74 140
            return $body;
75 7
        } elseif (isset($curlOptions[CURLOPT_FILE])) {
76 135
            $fp = $curlOptions[CURLOPT_FILE];
77 70
            fwrite($fp, $body);
78 63
            fflush($fp);
79 14
        } else {
80 14
            echo $body;
81 14
        }
82 4
83 49
        return null;
84
    }
85 70
86
    /**
87
     * Returns a cURL option from a Response.
88
     *
89
     * @param Response $response response to get cURL option from
90
     * @param int      $option   cURL option to get
91
     *
92
     * @throws \BadMethodCallException
93
     *
94
     * @return mixed value of the cURL option
95
     */
96 63
    public static function getCurlOptionFromResponse(Response $response, int $option = 0)
97
    {
98
        switch ($option) {
99 63
            case 0: // 0 == array of all curl options
100 14
                $info = [];
101 14
                foreach (self::$curlInfoList as $option => $key) {
102 14
                    $info[$key] = $response->getCurlInfo($key);
103 4
                }
104 14
                break;
105 49
            case CURLINFO_HTTP_CODE:
106 14
                $info = (int) $response->getStatusCode();
107 14
                break;
108 35
            case CURLINFO_SIZE_DOWNLOAD:
109
                $info = $response->getHeader('Content-Length');
110
                break;
111 35
            case CURLINFO_HEADER_SIZE:
112 35
                $info = mb_strlen(HttpUtil::formatAsStatusWithHeadersString($response), 'ISO-8859-1');
113 35
                break;
114
            case CURLPROXY_HTTPS:
115
                $info = '';
116
                break;
117
            default:
118
                $info = $response->getCurlInfo(self::$curlInfoList[$option]);
119 63
                break;
120 63
        }
121
122
        if (null !== $info) {
123
            return $info;
124
        }
125
126
        $constants = get_defined_constants(true);
127
        $constantNames = array_flip($constants['curl']);
128
        throw new \BadMethodCallException("Not implemented: {$constantNames[$option]} ({$option}) ");
129
    }
130
131
    /**
132
     * Sets a cURL option on a Request.
133
     *
134
     * @param Request  $request    request to set cURL option to
135
     * @param int      $option     cURL option to set
136 245
     * @param mixed    $value      value of the cURL option
137
     * @param resource $curlHandle cURL handle where this option is set on (optional)
138
     */
139 245
    public static function setCurlOptionOnRequest(Request $request, int $option, $value, $curlHandle = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $curlHandle is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
140 21
    {
141 21
        switch ($option) {
142 245
            case CURLOPT_URL:
143 21
                $request->setUrl($value);
144 21
                break;
145 231
            case CURLOPT_CUSTOMREQUEST:
146 21
                $request->setCurlOption(CURLOPT_CUSTOMREQUEST, $value);
147
                break;
148
            case CURLOPT_POST:
149 21
                if (true == $value) {
150 231
                    $request->setMethod('POST');
151
                }
152 63
                break;
153 35
            case CURLOPT_POSTFIELDS:
154 28
                // todo: check for file @
155 10
                if (\is_array($value)) {
156
                    foreach ($value as $name => $fieldValue) {
157 35
                        $request->setPostField($name, $fieldValue);
158 27
                    }
159 2
160 38
                    if (0 == \count($value)) {
161
                        $request->removeHeader('Content-Type');
162
                    }
163 21
                } elseif (!empty($value)) {
164 6
                    // Empty values are ignored to be consistent with how requests are read out of
165 63
                    // storage using \VCR\Request::fromArray(array $request).
166 63
                    $request->setBody($value);
167 175
                }
168 84
                $request->setMethod('POST');
169 84
                break;
170 84
            case CURLOPT_HTTPHEADER:
171
                foreach ($value as $header) {
172
                    $headerParts = explode(': ', $header, 2);
173
                    if (!isset($headerParts[1])) {
174 84
                        $headerParts[0] = rtrim($headerParts[0], ':');
175 24
                        $headerParts[1] = '';
176 84
                    }
177 91
                    $request->setHeader($headerParts[0], $headerParts[1]);
178 86
                }
179 86
                break;
180 86
            case CURLOPT_FILE:
181 86
            case CURLOPT_HEADER:
182
            case CURLOPT_WRITEFUNCTION:
183
            case CURLOPT_HEADERFUNCTION:
184 7
            case CURLOPT_UPLOAD:
185 24
                // Ignore header, file and writer functions.
186 84
                // These options are stored and will be handled later in handleOutput().
187 84
                break;
188 24
            default:
189 245
                $request->setCurlOption($option, $value);
190
                break;
191
        }
192
    }
193
194
    /**
195
     * Makes sure we've properly handled the POST body, such as ensuring that
196
     * CURLOPT_INFILESIZE is set if CURLOPT_READFUNCTION is set.
197
     *
198 112
     * @param Request  $request    request to set cURL option to
199
     * @param resource $curlHandle cURL handle associated with the request
200 112
     */
201 112
    public static function validateCurlPOSTBody(Request $request, $curlHandle = null): void
202 98
    {
203
        $readFunction = $request->getCurlOption(CURLOPT_READFUNCTION);
204
        if (null === $readFunction) {
205
            return;
206
        }
207 14
208
        // Guzzle 4 sometimes sets the post body in CURLOPT_POSTFIELDS even if
209
        // they have already set CURLOPT_READFUNCTION.
210
        if ($request->getBody()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getBody() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
211 14
            return;
212 14
        }
213 7
214 7
        $bodySize = $request->getCurlOption(CURLOPT_INFILESIZE);
215 7
        Assertion::notEmpty($bodySize, 'To set a CURLOPT_READFUNCTION, CURLOPT_INFILESIZE must be set.');
216
        $body = \call_user_func_array($readFunction, [$curlHandle, fopen('php://memory', 'r'), $bodySize]);
217
        $request->setBody($body);
218
    }
219
220
    /**
221
     * A wrapper around call_user_func that attempts to properly handle private
222
     * and protected methods on objects.
223
     *
224
     * @param mixed    $callback   The callable to pass to call_user_func
225
     * @param resource $curlHandle cURL handle associated with the request
226
     * @param mixed    $argument   The third argument to pass to call_user_func
227
     *
228
     * @return mixed value returned by the callback function
229
     */
230
    private static function callFunction($callback, $curlHandle, $argument)
231
    {
232
        if (!\is_callable($callback) && \is_array($callback) && 2 === \count($callback)) {
233
            // This is probably a private or protected method on an object. Try and
234
            // make it accessible.
235
            $method = new \ReflectionMethod($callback[0], $callback[1]);
236
            $method->setAccessible(true);
237
238
            return $method->invoke($callback[0], $curlHandle, $argument);
239
        } else {
240
            return \call_user_func($callback, $curlHandle, $argument);
241
        }
242
    }
243
}
244