Completed
Push — master ( d280fe...c482b6 )
by Jeroen van
05:45 queued 13s
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
            default:
115
                $info = $response->getCurlInfo(self::$curlInfoList[$option]);
116
                break;
117
        }
118
119 63
        if (null !== $info) {
120 63
            return $info;
121
        }
122
123
        $constants = get_defined_constants(true);
124
        $constantNames = array_flip($constants['curl']);
125
        throw new \BadMethodCallException("Not implemented: {$constantNames[$option]} ({$option}) ");
126
    }
127
128
    /**
129
     * Sets a cURL option on a Request.
130
     *
131
     * @param Request  $request    request to set cURL option to
132
     * @param int      $option     cURL option to set
133
     * @param mixed    $value      value of the cURL option
134
     * @param resource $curlHandle cURL handle where this option is set on (optional)
135
     */
136 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...
137
    {
138
        switch ($option) {
139 245
            case CURLOPT_URL:
140 21
                $request->setUrl($value);
141 21
                break;
142 245
            case CURLOPT_CUSTOMREQUEST:
143 21
                $request->setCurlOption(CURLOPT_CUSTOMREQUEST, $value);
144 21
                break;
145 231
            case CURLOPT_POST:
146 21
                if (true == $value) {
147
                    $request->setMethod('POST');
148
                }
149 21
                break;
150 231
            case CURLOPT_POSTFIELDS:
151
                // todo: check for file @
152 63
                if (\is_array($value)) {
153 35
                    foreach ($value as $name => $fieldValue) {
154 28
                        $request->setPostField($name, $fieldValue);
155 10
                    }
156
157 35
                    if (0 == \count($value)) {
158 27
                        $request->removeHeader('Content-Type');
159 2
                    }
160 38
                } elseif (!empty($value)) {
161
                    // Empty values are ignored to be consistent with how requests are read out of
162
                    // storage using \VCR\Request::fromArray(array $request).
163 21
                    $request->setBody($value);
164 6
                }
165 63
                $request->setMethod('POST');
166 63
                break;
167 175
            case CURLOPT_HTTPHEADER:
168 84
                foreach ($value as $header) {
169 84
                    $headerParts = explode(': ', $header, 2);
170 84
                    if (!isset($headerParts[1])) {
171
                        $headerParts[0] = rtrim($headerParts[0], ':');
172
                        $headerParts[1] = '';
173
                    }
174 84
                    $request->setHeader($headerParts[0], $headerParts[1]);
175 24
                }
176 84
                break;
177 91
            case CURLOPT_FILE:
178 86
            case CURLOPT_HEADER:
179 86
            case CURLOPT_WRITEFUNCTION:
180 86
            case CURLOPT_HEADERFUNCTION:
181 86
            case CURLOPT_UPLOAD:
182
                // Ignore header, file and writer functions.
183
                // These options are stored and will be handled later in handleOutput().
184 7
                break;
185 24
            default:
186 84
                $request->setCurlOption($option, $value);
187 84
                break;
188 24
        }
189 245
    }
190
191
    /**
192
     * Makes sure we've properly handled the POST body, such as ensuring that
193
     * CURLOPT_INFILESIZE is set if CURLOPT_READFUNCTION is set.
194
     *
195
     * @param Request  $request    request to set cURL option to
196
     * @param resource $curlHandle cURL handle associated with the request
197
     */
198 112
    public static function validateCurlPOSTBody(Request $request, $curlHandle = null): void
199
    {
200 112
        $readFunction = $request->getCurlOption(CURLOPT_READFUNCTION);
201 112
        if (null === $readFunction) {
202 98
            return;
203
        }
204
205
        // Guzzle 4 sometimes sets the post body in CURLOPT_POSTFIELDS even if
206
        // they have already set CURLOPT_READFUNCTION.
207 14
        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...
208
            return;
209
        }
210
211 14
        $bodySize = $request->getCurlOption(CURLOPT_INFILESIZE);
212 14
        Assertion::notEmpty($bodySize, 'To set a CURLOPT_READFUNCTION, CURLOPT_INFILESIZE must be set.');
213 7
        $body = \call_user_func_array($readFunction, [$curlHandle, fopen('php://memory', 'r'), $bodySize]);
214 7
        $request->setBody($body);
215 7
    }
216
217
    /**
218
     * A wrapper around call_user_func that attempts to properly handle private
219
     * and protected methods on objects.
220
     *
221
     * @param mixed    $callback   The callable to pass to call_user_func
222
     * @param resource $curlHandle cURL handle associated with the request
223
     * @param mixed    $argument   The third argument to pass to call_user_func
224
     *
225
     * @return mixed value returned by the callback function
226
     */
227
    private static function callFunction($callback, $curlHandle, $argument)
228
    {
229
        if (!\is_callable($callback) && \is_array($callback) && 2 === \count($callback)) {
230
            // This is probably a private or protected method on an object. Try and
231
            // make it accessible.
232
            $method = new \ReflectionMethod($callback[0], $callback[1]);
233
            $method->setAccessible(true);
234
235
            return $method->invoke($callback[0], $curlHandle, $argument);
236
        } else {
237
            return \call_user_func($callback, $curlHandle, $argument);
238
        }
239
    }
240
}
241