Completed
Pull Request — master (#37)
by Boris
03:48 queued 01:31
created

CurlRemoteFilesystem   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 332
Duplicated Lines 13.86 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 66.2%

Importance

Changes 20
Bugs 6 Features 1
Metric Value
wmc 45
c 20
b 6
f 1
lcom 1
cbo 11
dl 46
loc 332
ccs 94
cts 142
cp 0.662
rs 8.3673

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setPluginConfig() 0 4 1
A getContents() 0 12 1
A getOptions() 0 4 1
A getLastHeaders() 0 4 1
A progress() 0 18 4
A processHeader() 0 5 1
A copy() 0 20 2
F fetch() 0 64 15
A exec() 0 20 2
D promptAuth() 46 85 16

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CurlRemoteFilesystem often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CurlRemoteFilesystem, and based on these observations, apply Extract Interface, too.

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\Config;
10
use Composer\IO;
11
use Composer\Downloader;
12
use Composer\Util;
13
14
/**
15
 * yet another implementation about Composer\Util\RemoteFilesystem
16
 * non thread safe
17
 */
18
class CurlRemoteFilesystem extends Util\RemoteFilesystem
19
{
20
    protected $io;
21
    protected $config;
22
    protected $options;
23
24
    protected $retryAuthFailure = true;
25
26
    protected $pluginConfig;
27
28
    private $_lastHeaders = array();
29
30
    // global flags
31
    private $_retry = false;
32
    private $_degradedMode = false;
33
34
    /** @var Aspects\JoinPoint */
35
    public $onPreDownload;
36
37
    /** @var Aspects\JoinPoint */
38
    public $onPostDownload;
39
40
    /**
41
     * @param IO\IOInterface $io
42
     * @param Config $config
43
     * @param array $options
44
     */
45 8
    public function __construct(IO\IOInterface $io, Config $config, array $options = array())
46
    {
47 8
        $this->io = $io;
48 8
        $this->config = $config;
49 8
        $this->options = $options;
50 8
    }
51
52 8
    public function setPluginConfig(array $pluginConfig)
53
    {
54 8
        $this->pluginConfig = $pluginConfig;
55 8
    }
56
57
    /**
58
     * Copy the remote file in local.
59
     *
60
     * @param string $origin    host/domain text
61
     * @param string $fileUrl   targeturl
62
     * @param string $fileName  the local filename
63
     * @param bool   $progress  Display the progression
64
     * @param array  $options   Additional context options
65
     *
66
     * @return bool true
67
     */
68 4
    public function copy($origin, $fileUrl, $fileName, $progress = true, $options = array())
69
    {
70 4
        $that = $this; // for PHP5.3
71
72
        return $this->fetch($origin, $fileUrl, $progress, $options, function($ch, $request) use ($that, $fileName) {
73 4
            $outputFile = new OutputFile($fileName);
74 4
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
75 4
            curl_setopt($ch, CURLOPT_FILE, $outputFile->getPointer());
76
77 4
            list(, $response) = $result = $that->exec($ch, $request);
78
79 2
            curl_setopt($ch, CURLOPT_FILE, STDOUT);
80
81 2
            if (200 !== $response->info['http_code']) {
82 1
                $outputFile->setFailure();
83
            }
84
85 2
            return $result;
86 4
        });
87
    }
88
89
    /**
90
     * Get the content.
91
     *
92
     * @param string $origin The origin URL
93
     * @param string $fileUrl   The file URL
94
     * @param bool   $progress  Display the progression
95
     * @param array  $options   Additional context options
96
     *
97
     * @return bool|string The content
98
     */
99 1
    public function getContents($origin, $fileUrl, $progress = true, $options = array())
100
    {
101 1
        $that = $this; // for PHP5.3
102
103 1
        return $this->fetch($origin, $fileUrl, $progress, $options, function($ch, $request) use ($that) {
104
            // This order is important.
105 1
            curl_setopt($ch, CURLOPT_FILE, STDOUT);
106 1
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
107
108 1
            return $that->exec($ch, $request);
109 1
        });
110
    }
111
112
    /**
113
     * @param string $origin
114
     * @param string $fileUrl
115
     * @param boolean $progress
116
     * @param \Closure $exec
117
     */
118 5
    protected function fetch($origin, $fileUrl, $progress, $options, $exec)
119
    {
120
        do {
121 5
            $this->_retry = false;
122
123 5
            $request = new Aspects\HttpGetRequest($origin, $fileUrl, $this->io);
124 5
            $request->setSpecial(array(
125 5
                'github' => $this->config->get('github-domains') ?: array(),
126 5
                'gitlab' => $this->config->get('gitlab-domains') ?: array(),
127
            ));
128 5
            $this->onPreDownload = Factory::getPreEvent($request);
129 5
            $this->onPostDownload = Factory::getPostEvent($request);
130 5
            if ($this->_degradedMode) {
131
                $this->onPreDownload->attach(new Aspects\AspectDegradedMode);
132
            }
133
134 5
            $options += $this->options;
135
            // override
136 5
            if ('github' === $request->special && isset($options['github-token'])) {
137
                $request->query['access_token'] = $options['github-token'];
138
            }
139 5
            if ('gitlab' === $request->special && isset($options['gitlab-token'])) {
140
                $request->query['access_token'] = $options['gitlab-token'];
141
            }
142
143 5
            if ($this->io->isDebug()) {
144
                $this->io->write('Downloading ' . $fileUrl);
145
            }
146
147 5
            if ($progress) {
148 4
                $this->io->write("    Downloading: <comment>Connecting...</comment>", false);
149 4
                $request->curlOpts[CURLOPT_NOPROGRESS] = false;
150 4
                $request->curlOpts[CURLOPT_PROGRESSFUNCTION] = array($this, 'progress');
151
            } else {
152 1
                $request->curlOpts[CURLOPT_NOPROGRESS] = true;
153 1
                $request->curlOpts[CURLOPT_PROGRESSFUNCTION] = null;
154
            }
155
156 5
            $this->onPreDownload->notify();
157
158 5
            $opts = $request->getCurlOpts();
159 5
            $ch = Factory::getConnection($origin, isset($opts[CURLOPT_USERPWD]));
160
161 5
            if ($this->pluginConfig['insecure']) {
162
                $opts[CURLOPT_SSL_VERIFYPEER] = false;
163
            }
164 5
            if (!empty($pluginConfig['userAgent'])) {
165
                $opts[CURLOPT_USERAGENT] = $pluginConfig['userAgent'];
166
            }
167 5
            if (!empty($pluginConfig['capath'])) {
168
                $opts[CURLOPT_CAPATH] = $pluginConfig['capath'];
169
            }
170
171 5
            curl_setopt_array($ch, $opts);
172
173 5
            list($execStatus,) = $exec($ch, $request);
174 3
        } while ($this->_retry);
175
176 3
        if ($progress) {
177 2
            $this->io->overwrite("    Downloading: <comment>100%</comment>");
178
        }
179
180 3
        return $execStatus;
181
    }
182
183
    /**
184
     * Retrieve the options set in the constructor
185
     *
186
     * @return array Options
187
     */
188 1
    public function getOptions()
189
    {
190 1
        return $this->options;
191
    }
192
193
    /**
194
     * Returns the headers of the last request
195
     *
196
     * @return array
197
     */
198 1
    public function getLastHeaders()
199
    {
200 1
        return $this->_lastHeaders;
201
    }
202
203
    /**
204
     * @internal
205
     * @param resource $ch
206
     * @param Aspects\HttpGetRequest $request
207
     * @return array {int, Aspects\HttpGetResponse}
208
     */
209 5
    public function exec($ch, $request)
210
    {
211 5
        $this->_lastHeaders = array();
212 5
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'processHeader'));
213 5
        $execStatus = curl_exec($ch);
214
215 5
        $response = new Aspects\HttpGetResponse(
216
            curl_errno($ch),
217
            curl_error($ch),
218
            curl_getinfo($ch)
219
        );
220 5
        $this->onPostDownload->setResponse($response);
221 5
        $this->onPostDownload->notify();
222
223 5
        if ($response->needAuth()) {
224 3
            $this->promptAuth($request, $response);
225
        }
226
227 3
        return array($execStatus, $response);
228
    }
229
230
    /**
231
     * @internal
232
     */
233 4
    public function progress()
234
    {
235
        // @codeCoverageIgnoreStart
236
        if (PHP_VERSION_ID >= 50500) {
237
            list(, $downBytesMax, $downBytes,,) = func_get_args();
238
        } else {
239
            list($downBytesMax, $downBytes,,) = func_get_args();
240
        }
241
        // @codeCoverageIgnoreEnd
242
243 4
        if ($downBytesMax <= 0 || $downBytesMax < $downBytes) {
244 1
            return 0;
245
        }
246
247 4
        $progression = intval($downBytes / $downBytesMax * 100);
248 4
        $this->io->overwrite("    Downloading: <comment>$progression%</comment>", false);
249 4
        return 0;
250
    }
251
252
    /**
253
     * @internal
254
     * @param resource $ch
255
     * @param string $header
256
     * @return int
257
     */
258 5
    public function processHeader($ch, $header)
259
    {
260 5
        $this->_lastHeaders[] = trim($header);
261 5
        return strlen($header);
262
    }
263
264 3
    protected function promptAuth(Aspects\HttpGetRequest $req, Aspects\HttpGetResponse $res)
265
    {
266 3
        $io = $this->io;
267 3
        $httpCode = $res->info['http_code'];
268
269 3 View Code Duplication
        if ('github' === $req->special) {
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...
270
            $message = "\nCould not fetch {$req->getURL()}, please create a GitHub OAuth token ";
271
            if (404 === $httpCode) {
272
                $message .= 'to access private repos';
273
            } else {
274
                $message .= 'to go over the API rate limit';
275
            }
276
            $github = new Util\GitHub($io, $this->config, null);
277
            if ($github->authorizeOAuth($req->origin)) {
278
                $this->_retry = true;
279
                return;
280
            }
281
            if ($io->isInteractive() &&
282
                $github->authorizeOAuthInteractively($req->origin, $message)) {
283
                $this->_retry = true;
284
                return;
285
            }
286
287
            throw new Downloader\TransportException(
288
                "Could not authenticate against $req->origin",
289
                401
290
            );
291
        }
292
        
293 3 View Code Duplication
        if ('gitlab' === $req->special) {
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...
294
            $message = "\nCould not fetch {$req->getURL()}, enter your $req->origin credentials ";
295
            if (401 === $httpCode) {
296
                $message .= 'to access private repos';
297
            } else {
298
                $message .= 'to go over the API rate limit';
299
            }
300
            $gitlab = new Util\GitLab($io, $this->config, null);
301
            if ($gitlab->authorizeOAuth($req->origin)) {
302
                $this->_retry = true;
303
                return;
304
            }
305
            if ($io->isInteractive() &&
306
                $gitlab->authorizeOAuthInteractively($req->origin, $message)) {
307
                $this->_retry = true;
308
                return;
309
            }
310
311
            throw new Downloader\TransportException(
312
                "Could not authenticate against $req->origin",
313
                401
314
            );
315
        }
316
317
        // 404s are only handled for github
318 3
        if (404 === $httpCode) {
319 1
            return;
320
        }
321
322
        // fail if the console is not interactive
323 2
        if (!$io->isInteractive()) {
324
            switch ($httpCode) {
325 2
                case 401:
326 1
                    $message = "The '{$req->getURL()}' URL required authentication.\nYou must be using the interactive console to authenticate";
327 1
                    break;
328
                case 403:
329 1
                    $message = "The '{$req->getURL()}' URL could not be accessed.";
330 1
                    break;
331
            }
332 2
            throw new Downloader\TransportException($message, $httpCode);
333
        }
334
335
        // fail if we already have auth
336
        if ($io->hasAuthentication($req->origin)) {
337
            throw new Downloader\TransportException(
338
                "Invalid credentials for '{$req->getURL()}', aborting.",
339
                $res->info['http_code']
340
            );
341
        }
342
343
        $io->overwrite("    Authentication required (<info>$req->host</info>):");
344
        $username = $io->ask('      Username: ');
345
        $password = $io->askAndHideAnswer('      Password: ');
346
        $io->setAuthentication($req->origin, $username, $password);
347
        $this->_retry = true;
348
    }
349
}
350