Completed
Push — master ( ca87f1...eb2839 )
by Julián
02:01
created

OptionFactory::getOptionKey()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.2
cc 4
eloc 8
nc 6
nop 1
1
<?php
2
3
/*
4
 * A PSR7 aware cURL client (https://github.com/juliangut/spiral).
5
 *
6
 * @license BSD-3-Clause
7
 * @link https://github.com/juliangut/spiral
8
 * @author Julián Gutiérrez <[email protected]>
9
 */
10
11
namespace Jgut\Spiral\Option;
12
13
use Jgut\Spiral\Exception\OptionException;
14
15
/**
16
 * cURL option wrappers factory.
17
 */
18
abstract class OptionFactory
19
{
20
    /**
21
     * Accepted cURL option aliases.
22
     *
23
     * @var array
24
     */
25
    protected static $aliasMap = [
26
        'http-header'        => CURLOPT_HTTPHEADER,
27
28
        'crlf'               => CURLOPT_CRLF,
29
        'header-out'         => CURLINFO_HEADER_OUT,
30
        'return-transfer'    => CURLOPT_RETURNTRANSFER,
31
        'verbose'            => CURLOPT_VERBOSE,
32
        'user-agent'         => CURLOPT_USERAGENT,
33
        'ssl-version'        => CURLOPT_SSLVERSION,
34
        'cookie-file'        => CURLOPT_COOKIEFILE,
35
        'cookie-jar'         => CURLOPT_COOKIEJAR,
36
        'referer'            => CURLOPT_REFERER,
37
        'auto-referer'       => CURLOPT_AUTOREFERER,
38
        'file-time'          => CURLOPT_FILETIME,
39
        'user-password'      => CURLOPT_USERPWD,
40
        'http-version'       => CURLOPT_HTTP_VERSION,
41
        'port'               => CURLOPT_PORT,
42
        'encoding'           => CURLOPT_ENCODING,
43
        'buffer-size'        => CURLOPT_BUFFERSIZE,
44
        'post-redir'         => CURLOPT_POSTREDIR,
45
        'stderr'             => CURLOPT_STDERR,
46
        'netrc'              => CURLOPT_NETRC,
47
48
        'header'             => CURLOPT_HEADER,
49
        'include'            => CURLOPT_HEADER,
50
51
        'connect-timeout'    => CURLOPT_CONNECTTIMEOUT,
52
        'connection-timeout' => CURLOPT_CONNECTTIMEOUT,
53
54
        'timeout'            => CURLOPT_TIMEOUT,
55
        'max-time'           => CURLOPT_TIMEOUT,
56
57
        'ssl_verify_host'    => CURLOPT_SSL_VERIFYHOST,
58
        'ssl_verify_peer'    => CURLOPT_SSL_VERIFYPEER,
59
        'insecure'           => CURLOPT_SSL_VERIFYPEER,
60
61
        'follow-location'    => CURLOPT_FOLLOWLOCATION,
62
        'follow-redirects'   => CURLOPT_FOLLOWLOCATION,
63
        'location'           => CURLOPT_FOLLOWLOCATION,
64
65
        'max-redirs'         => CURLOPT_MAXREDIRS,
66
        'max-redirects'      => CURLOPT_MAXREDIRS,
67
68
        'cookie'             => CURLOPT_COOKIE,
69
        'cookies'            => CURLOPT_COOKIE,
70
71
        'http-auth'          => CURLOPT_HTTPAUTH,
72
        'auth'               => CURLOPT_HTTPAUTH,
73
74
        'unrestricted-auth'  => CURLOPT_UNRESTRICTED_AUTH,
75
        'location-trusted'   => CURLOPT_UNRESTRICTED_AUTH,
76
77
        'forbid-reuse'       => CURLOPT_FORBID_REUSE,
78
        'fresh-connect'      => CURLOPT_FRESH_CONNECT,
79
    ];
80
81
    /**
82
     * cURL accepted options and class mapper.
83
     *
84
     * @var array
85
     */
86
    protected static $typeMap = [
87
        // Boolean
88
        // Perform proxy authentication and connection setup but no data transfer
89
        CURLOPT_CONNECT_ONLY      => ['type' => 'bool'],
90
        // Convert Unix newlines to CRLF newlines on transfers
91
        CURLOPT_CRLF              => ['type' => 'bool'],
92
        // Include request header in the output
93
        CURLINFO_HEADER_OUT       => ['type' => 'bool'],
94
        // Include response header in the output
95
        CURLOPT_HEADER            => ['type' => 'bool'],
96
        // Return the transfer as a string instead of outputting
97
        CURLOPT_RETURNTRANSFER    => ['type' => 'bool'],
98
        // Output verbose information
99
        CURLOPT_VERBOSE           => ['type' => 'bool'],
100
        // False to stop cURL from verifying the peer's certificate
101
        CURLOPT_SSL_VERIFYPEER    => ['type' => 'bool'],
102
        // Follow any Location headers sent by server
103
        CURLOPT_FOLLOWLOCATION    => ['type' => 'bool'],
104
        // Automatically set the http referer header when following a redirect
105
        CURLOPT_AUTOREFERER       => ['type' => 'bool'],
106
        // Keep sending the username and password when following locations
107
        CURLOPT_UNRESTRICTED_AUTH => ['type' => 'bool'],
108
        // Get HTTP header for modification date of file
109
        CURLOPT_FILETIME          => ['type' => 'bool'],
110
        // Automatically close connection after processing
111
        CURLOPT_FORBID_REUSE      => ['type' => 'bool'],
112
        // Force to use new connection instead of cached
113
        CURLOPT_FRESH_CONNECT     => ['type' => 'bool'],
114
        // Scan ~/.netrc file for user credentials
115
        CURLOPT_NETRC             => ['type' => 'bool'],
116
        // Tunnel through a given HTTP proxy
117
        CURLOPT_HTTPPROXYTUNNEL   => ['type' => 'bool'],
118
119
        // Integer
120
        // Number of seconds to wait while trying to connect. 0 to wait indefinitely
121
        CURLOPT_CONNECTTIMEOUT => ['type' => 'int'],
122
        // Maximum number of seconds to allow cURL functions to execute
123
        CURLOPT_TIMEOUT        => ['type' => 'int'],
124
        // The maximum amount of HTTP redirections to follow
125
        CURLOPT_MAXREDIRS      => ['type' => 'int'],
126
        // Alternative port number to connect to
127
        CURLOPT_PORT           => ['type' => 'int', 'max' => 99999],
128
        // Port number of the proxy to connect to
129
        CURLOPT_PROXYPORT      => ['type' => 'int'],
130
        // Size of the buffer for each read
131
        CURLOPT_BUFFERSIZE     => ['type' => 'int'],
132
133
        // String
134
        // Contents of the "User-Agent: " header to be used in a HTTP request
135
        CURLOPT_USERAGENT => ['type' => 'string'],
136
        // Contents of the "Referer: " header to be used in a HTTP request
137
        CURLOPT_REFERER   => ['type' => 'string'],
138
        // Contents of the "Accept-Encoding: " header. This enables decoding of the response
139
        CURLOPT_ENCODING  => ['type' => 'string'],
140
        // Alternative location to output errors
141
        CURLOPT_STDERR    => ['type' => 'string'],
142
        // HTTP proxy to tunnel requests through
143
        CURLOPT_PROXY     => ['type' => 'string'],
144
145
        // File
146
        // Name of the file containing the cookie data
147
        CURLOPT_COOKIEFILE => ['type' => 'file'],
148
        // Name of a file to save all internal cookies to when the handle is closed
149
        CURLOPT_COOKIEJAR  => ['type' => 'file'],
150
151
        // Regex
152
        // Which SSL/TLS version to use
153
        CURLOPT_SSLVERSION        => [
154
            'type' => 'regex',
155
            'regex' => '/^[0-6]$/',
156
            'message' => '"%s" is not valid SSL version',
157
        ],
158
        // Verify existence of a common name in peer certificate, and matches hostname
159
        CURLOPT_SSL_VERIFYHOST => [
160
            'type' => 'regex',
161
            'regex' => '/^1|2$/',
162
            'message' => '"%s" is not valid SSL verify host value',
163
        ],
164
        // Bit mask to maintain redirection type
165
        CURLOPT_POSTREDIR => [
166
            'type' => 'regex',
167
            'regex' => '/^1|2|4$/',
168
            'message' => '"%s" is not valid POST redirection value',
169
        ],
170
        /*
171
         * Proxy type
172
         * 0: CURLPROXY_HTTP             4: CURLPROXY_SOCKS4,
173
         * 5: CURLPROXY_SOCKS5           6: CURLPROXY_SOCKS4A
174
         * 7: CURLPROXY_SOCKS5_HOSTNAME
175
         */
176
        CURLOPT_PROXYTYPE => [
177
            'type' => 'regex',
178
            'regex' => '/^0|4|5|6|7$/',
179
            'message' => '"%s" is not a valid CURLOPT_PROXYTYPE value',
180
        ],
181
        // Username and password formatted as "username:password" to use for the connection
182
        CURLOPT_USERPWD => [
183
            'type' => 'regex',
184
            'regex' => '/^[^\n:]+:[^\n:]+$/',
185
            'message' => '"%s" is not a valid CURLOPT_USERPWD value',
186
        ],
187
        // Username and password formatted as "username:password" to use for proxy
188
        CURLOPT_PROXYUSERPWD => [
189
            'type' => 'regex',
190
            'regex' => '/^[^\n:]+:[^\n:]+$/',
191
            'message' => '"%s" is not a valid CURLOPT_PROXYUSERPWD value',
192
        ],
193
        /*
194
         * HTTP authentication method(s)
195
         *
196
         *   1: CURLAUTH_BASIC           2: CURLAUTH_DIGEST
197
         *   4: CURLAUTH_GSSNEGOTIATE    8: CURLAUTH_NTLM
198
         * -17: CURLAUTH_ANY           -18: CURLAUTH_ANYSAFE
199
         */
200
        CURLOPT_HTTPAUTH => [
201
            'type' => 'regex',
202
            'regex' => '/^1|2|4|8|-17|-18$/',
203
            'message' => '"%s" is not a valid CURLOPT_HTTPAUTH value',
204
        ],
205
        /*
206
         * HTTP authentication method(s) for the proxy
207
         *
208
         *   1: CURLAUTH_BASIC           2: CURLAUTH_DIGEST
209
         *   4: CURLAUTH_GSSNEGOTIATE    8: CURLAUTH_NTLM
210
         * -17: CURLAUTH_ANY           -18: CURLAUTH_ANYSAFE
211
         */
212
        CURLOPT_PROXYAUTH => [
213
            'type' => 'regex',
214
            'regex' => '/^1|2|4|8|-17|-18$/',
215
            'message' => '"%s" is not a valid CURLOPT_PROXYAUTH value',
216
        ],
217
218
        // Which HTTP version to use. "1.0" for CURL_HTTP_VERSION_1_0 or "1.1" for CURL_HTTP_VERSION_1_1
219
        CURLOPT_HTTP_VERSION => ['type' => 'callback'],
220
        // Contents of the "Cookie: " header to be used in the HTTP request. Can be an array
221
        CURLOPT_COOKIE       => ['type' => 'callback'],
222
    ];
223
224
    /**
225
     * Defined cURL constants.
226
     *
227
     * @var array
228
     */
229
    protected static $curlConstants;
230
231
    /**
232
     * Build cURL option.
233
     *
234
     * @param int|string $option
235
     * @param mixed      $value
236
     *
237
     * @throws OptionException
238
     *
239
     * @return OptionInterface
240
     */
241
    public static function build($option, $value)
242
    {
243
        $optionDefinition = ['type' => ''];
244
245
        $option = static::getOptionKey($option);
246
        if (array_key_exists($option, static::$typeMap)) {
247
            $optionDefinition = static::$typeMap[$option];
248
        }
249
250
        $optionClassName = preg_replace('/Factory$/', '', self::class) . ucfirst($optionDefinition['type']);
251
        $optionClass = new $optionClassName($option);
252
253
        switch (strtolower($optionDefinition['type'])) {
254
            case 'regex':
255
                /* @var OptionRegex $optionClass */
256
                $optionClass->setRegex($optionDefinition['regex']);
257
                if (array_key_exists('message', $optionDefinition)) {
258
                    $optionClass->setMessage($optionDefinition['message']);
259
                }
260
                break;
261
262
            case 'int':
263
                /* @var OptionInt $optionClass */
264
                $optionClass->setMin(array_key_exists('min', $optionDefinition) ? $optionDefinition['min'] : 0);
265
                if (array_key_exists('max', $optionDefinition)) {
266
                    $optionClass->setMax($optionDefinition['max']);
267
                }
268
                break;
269
270
            case 'callback':
271
                $optionClass = static::configureCallback($optionClass, $option);
272
                break;
273
        }
274
275
        $optionClass->setValue($value);
276
277
        return $optionClass;
278
    }
279
280
    /**
281
     * Get mapped option.
282
     *
283
     * @param int|string $option
284
     *
285
     * @throws OptionException
286
     *
287
     * @return int
288
     */
289
    public static function getOptionKey($option)
290
    {
291
        if (is_string($option)) {
292
            $option = strtolower(preg_replace('/[ _]+/', '-', trim($option)));
293
            if (array_key_exists($option, static::$aliasMap)) {
294
                $option = static::$aliasMap[strtolower($option)];
295
            }
296
        }
297
298
        if (!in_array($option, static::getCurlConstants(), true)) {
299
            throw new OptionException(sprintf('"%s" is not valid cURL option', $option));
300
        }
301
302
        return $option;
303
    }
304
305
    /**
306
     * Configure option callback.
307
     *
308
     * @param OptionCallback $optionClass
309
     * @param int            $option
310
     *
311
     * @return OptionCallback
312
     */
313
    protected static function configureCallback(OptionCallback $optionClass, $option)
314
    {
315
        switch ($option) {
316
            case CURLOPT_HTTP_VERSION:
317
                $optionClass->setCallback(function ($value) {
318
                    $value = number_format((float) $value, 1, '.', '');
319
320
                    if (!preg_match('/^1.(0|1)$/', $value)) {
321
                        throw new OptionException(sprintf('"%s" is not a valid HTTP version', $value));
322
                    }
323
324
                    return constant('CURL_HTTP_VERSION_' . str_replace('.', '_', $value));
325
                });
326
                break;
327
328
            case CURLOPT_COOKIE:
329
                $optionClass->setCallback(function ($value) {
330
                    if (is_array($value)) {
331
                        $value = http_build_query($value, '', '; ');
332
                    }
333
334
                    return $value;
335
                });
336
                break;
337
        }
338
339
        return $optionClass;
340
    }
341
342
    /**
343
     * Get defined cURL constants.
344
     *
345
     * @return array
346
     */
347
    protected static function getCurlConstants()
348
    {
349
        if (static::$curlConstants === null) {
350
            static::$curlConstants = [];
351
352
            foreach (get_defined_constants(true)['curl'] as $key => $val) {
353
                if (preg_match('/(^CURLOPT_)|(^CURLINFO_HEADER_OUT$)/', $key)) {
354
                    static::$curlConstants[] = $val;
355
                }
356
            }
357
        }
358
359
        return static::$curlConstants;
360
    }
361
}
362