Completed
Pull Request — master (#98)
by Hiraku
09:04
created

CopyRequest::setDestination()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 9.4285
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
/*
3
 * hirak/prestissimo
4
 * @author Hiraku NAKANO
5
 * @license MIT https://github.com/hirak/prestissimo
6
 */
7
namespace Hirak\Prestissimo;
8
9
use Composer\Util;
10
use Composer\IO;
11
use Composer\Config;
12
13
class CopyRequest
14
{
15
    private $scheme;
16
    private $user;
17
    private $pass;
18
    private $host;
19
    private $port;
20
    private $path;
21
    private $query = array();
22
23
    /** @var [string => string] */
24
    private $headers = array();
25
26
    /** @var string */
27
    private $destination;
28
29
    /** @var resource<stream<plainfile>> */
30
    private $fp;
31
32
    private $success = false;
33
34
    private static $defaultCurlOptions = array(
35
        CURLOPT_HTTPGET => true,
36
        CURLOPT_FOLLOWLOCATION => true,
37
        CURLOPT_MAXREDIRS => 20,
38
        CURLOPT_ENCODING => '',
39
    );
40
41
    private $githubDomains = array();
42
    private $gitlabDomains = array();
43
44
    private $capath;
45
    private $cafile;
46
47
    private static $NSS_CIPHERS = array(
48
        'rsa_3des_sha',
49
        'rsa_des_sha',
50
        'rsa_null_md5',
51
        'rsa_null_sha',
52
        'rsa_rc2_40_md5',
53
        'rsa_rc4_128_md5',
54
        'rsa_rc4_128_sha',
55
        'rsa_rc4_40_md5',
56
        'fips_des_sha',
57
        'fips_3des_sha',
58
        'rsa_des_56_sha',
59
        'rsa_rc4_56_sha',
60
        'rsa_aes_128_sha',
61
        'rsa_aes_256_sha',
62
        'rsa_aes_128_gcm_sha_256',
63
        'dhe_rsa_aes_128_gcm_sha_256',
64
        'ecdh_ecdsa_null_sha',
65
        'ecdh_ecdsa_rc4_128_sha',
66
        'ecdh_ecdsa_3des_sha',
67
        'ecdh_ecdsa_aes_128_sha',
68
        'ecdh_ecdsa_aes_256_sha',
69
        'ecdhe_ecdsa_null_sha',
70
        'ecdhe_ecdsa_rc4_128_sha',
71
        'ecdhe_ecdsa_3des_sha',
72
        'ecdhe_ecdsa_aes_128_sha',
73
        'ecdhe_ecdsa_aes_256_sha',
74
        'ecdh_rsa_null_sha',
75
        'ecdh_rsa_128_sha',
76
        'ecdh_rsa_3des_sha',
77
        'ecdh_rsa_aes_128_sha',
78
        'ecdh_rsa_aes_256_sha',
79
        'echde_rsa_null',
80
        'ecdhe_rsa_rc4_128_sha',
81
        'ecdhe_rsa_3des_sha',
82
        'ecdhe_rsa_aes_128_sha',
83
        'ecdhe_rsa_aes_256_sha',
84
        'ecdhe_ecdsa_aes_128_gcm_sha_256',
85
        'ecdhe_rsa_aes_128_gcm_sha_256',
86
    );
87
88
    /**
89
     * @param string $url
90
     * @param string $destination
91
     * @param bool $useRedirector
92
     * @param IO\IOInterface $io
93
     * @param Config $config
94
     */
95
    public function __construct($url, $destination, $useRedirector, IO\IOInterface $io, Config $config)
96
    {
97
        $this->setURL($url);
98
        $this->setDestination($destination);
99
        $this->githubDomains = $config->get('github-domains');
0 ignored issues
show
Documentation Bug introduced by
It seems like $config->get('github-domains') of type * is incompatible with the declared type array of property $githubDomains.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
100
        $this->gitlabDomains = $config->get('gitlab-domains');
0 ignored issues
show
Documentation Bug introduced by
It seems like $config->get('gitlab-domains') of type * is incompatible with the declared type array of property $gitlabDomains.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
101
        $this->capath = $config->get('capath');
102
        $this->cafile = $config->get('cafile');
103
        $this->setupAuthentication($io, $useRedirector);
104
    }
105
106
    public function __destruct()
107
    {
108
        if ($this->fp) {
109
            fclose($this->fp);
110
        }
111
112
        if (!$this->success) {
113
            if (file_exists($this->destination)) {
114
                unlink($this->destination);
115
            }
116
        }
117
    }
118
119
    /**
120
     * @return string
121
     */
122
    public function getURL()
123
    {
124
        $url = self::ifOr($this->scheme, '', '://');
125
        if ($this->user) {
126
            $user = $this->user;
127
            $user .= self::ifOr($this->pass, ':');
128
            $url .= $user . '@';
129
        }
130
        $url .= self::ifOr($this->host);
131
        $url .= self::ifOr($this->port, ':');
132
        $url .= self::ifOr($this->path);
133
        $url .= self::ifOr(http_build_query($this->query), '?');
134
        return $url;
135
    }
136
137
    /**
138
     * @return string user/pass/access_token masked url
139
     */
140
    public function getMaskedURL()
141
    {
142
        $url = self::ifOr($this->scheme, '', '://');
143
        $url .= self::ifOr($this->host);
144
        $url .= self::ifOr($this->port, ':');
145
        $url .= self::ifOr($this->path);
146
        return $url;
147
    }
148
149
    private static function ifOr($str, $pre = '', $post = '')
150
    {
151
        if ($str) {
152
            return $pre . $str . $post;
153
        }
154
        return '';
155
    }
156
157
    /**
158
     * @param string $url
159
     */
160
    public function setURL($url)
161
    {
162
        $struct = parse_url($url);
163
        foreach ($struct as $key => $val) {
0 ignored issues
show
Bug introduced by
The expression $struct of type array<string,string>|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
164
            if ($key === 'query') {
165
                parse_str($val, $this->query);
166
            } else {
167
                $this->$key = $val;
168
            }
169
        }
170
    }
171
172
    public function addParam($key, $val)
173
    {
174
        $this->query[$key] = $val;
175
    }
176
177
    public function addHeader($key, $val)
178
    {
179
        $this->headers[strtolower($key)] = $val;
180
    }
181
182
    public function makeSuccess()
183
    {
184
        $this->success = true;
185
    }
186
187
    /**
188
     * @return array
189
     */
190
    public function getCurlOptions()
191
    {
192
        $headers = array();
193
        foreach ($this->headers as $key => $val) {
194
            $headers[] = strtr(ucwords(strtr($key, '-', ' ')), ' ', '-') . ': ' . $val;
195
        }
196
197
        $url = $this->getURL();
198
199
        $curlOpts = array(
200
            CURLOPT_URL => $url,
201
            CURLOPT_HTTPHEADER => $headers,
202
            CURLOPT_USERAGENT => ConfigFacade::getUserAgent(),
203
            CURLOPT_FILE => $this->fp,
204
            //CURLOPT_VERBOSE => true, //for debug
205
        );
206
        $curlOpts += self::$defaultCurlOptions;
207
208
        if ($ciphers = $this->nssCiphers()) {
209
            $curlOpts[CURLOPT_SSL_CIPHER_LIST] = $ciphers;
210
        }
211
        if ($proxy = $this->getProxy($url)) {
212
            $curlOpts[CURLOPT_PROXY] = $proxy;
213
        }
214
        if ($this->capath) {
215
            $curlOpts[CURLOPT_CAPATH] = $this->capath;
216
        }
217
        if ($this->cafile) {
218
            $curlOpts[CURLOPT_CAINFO] = $this->cafile;
219
        }
220
221
        return $curlOpts;
222
    }
223
224
    /**
225
     * @param IO\IOInterface $io
226
     * @param bool $useRedirector
227
     */
228
    private function setupAuthentication(IO\IOInterface $io, $useRedirector)
229
    {
230
        if (preg_match('/\.github\.com$/', $this->host)) {
231
            $authKey = 'github.com';
232
            if ($useRedirector) {
233
                if ($this->host === 'api.github.com' && preg_match('%^/repos(/[^/]+/[^/]+/)zipball(.+)$%', $this->path, $_)) {
234
                    $this->host = 'codeload.github.com';
235
                    $this->path = $_[1] . 'legacy.zip' . $_[2];
236
                }
237
            }
238
        } else {
239
            $authKey = $this->host;
240
        }
241
        if (!$io->hasAuthentication($authKey)) {
242
            if ($this->user || $this->pass) {
243
                $io->setAuthentication($authKey, $this->user, $this->pass);
244
            } else {
245
                return;
246
            }
247
        }
248
249
        $auth = $io->getAuthentication($authKey);
250
251
        // is github
252
        if (in_array($authKey, $this->githubDomains) && 'x-oauth-basic' === $auth['password']) {
253
            $this->addParam('access_token', $auth['username']);
254
            $this->user = $this->pass = null;
255
            return;
256
        }
257
        // is gitlab
258
        if (in_array($authKey, $this->gitlabDomains) && 'oauth2' === $auth['password']) {
259
            $this->addHeader('authorization', 'Bearer ' . $auth['username']);
260
            $this->user = $this->pass = null;
261
            return;
262
        }
263
        // others, includes bitbucket
264
        $this->user = $auth['username'];
265
        $this->pass = $auth['password'];
266
    }
267
268
    private function getProxy($url)
0 ignored issues
show
Coding Style introduced by
getProxy uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
269
    {
270
        if (isset($_SERVER['no_proxy'])) {
271
            $pattern = new Util\NoProxyPattern($_SERVER['no_proxy']);
272
            if ($pattern->test($url)) {
273
                return null;
274
            }
275
        }
276
277 View Code Duplication
        if ($this->scheme === 'https') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
278
            if (isset($_SERVER['HTTPS_PROXY'])) {
279
                return $_SERVER['HTTPS_PROXY'];
280
            }
281
            if (isset($_SERVER['https_proxy'])) {
282
                return $_SERVER['https_proxy'];
283
            }
284
        }
285
286 View Code Duplication
        if ($this->scheme === 'http') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
287
            if (isset($_SERVER['HTTP_PROXY'])) {
288
                return $_SERVER['HTTP_PROXY'];
289
            }
290
            if (isset($_SERVER['http_proxy'])) {
291
                return $_SERVER['http_proxy'];
292
            }
293
        }
294
        return null;
295
    }
296
297
    /**
298
     * enable ECC cipher suites in cURL/NSS
299
     * @codeCoverageIgnore
300
     */
301
    public static function nssCiphers()
302
    {
303
        static $cache;
304
        if (isset($cache)) {
305
            return $cache;
306
        }
307
        $ver = curl_version();
308
        if (preg_match('/^NSS.*Basic ECC$/', $ver['ssl_version'])) {
309
            return $cache = implode(',', self::$NSS_CIPHERS);
310
        }
311
        return $cache = false;
312
    }
313
314
    /**
315
     * @param string
316
     */
317
    public function setDestination($destination)
318
    {
319
        $this->destination = $destination;
320
        if (is_dir($destination)) {
321
            throw new FetchException(
322
                'The file could not be written to ' . $destination . '. Directory exists.'
323
            );
324
        }
325
326
        $this->createDir($destination);
327
328
        $this->fp = fopen($destination, 'wb');
329
        if (!$this->fp) {
330
            throw new FetchException(
331
                'The file could not be written to ' . $destination
332
            );
333
        }
334
    }
335
336
    private function createDir($fileName)
337
    {
338
        $targetdir = dirname($fileName);
339
        if (!file_exists($targetdir)) {
340
            if (!mkdir($targetdir, 0766, true)) {
341
                throw new FetchException(
342
                    'The file could not be written to ' . $fileName
343
                );
344
            }
345
        }
346
    }
347
}
348