Completed
Push — master ( eb9b51...1f6bdc )
by Hiraku
02:15
created

CurlRemoteFilesystem::progress()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 18
ccs 6
cts 6
cp 1
rs 9.2
cc 4
eloc 10
nc 4
nop 0
crap 4
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 1
            }
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 5
    protected function fetch($origin, $fileUrl, $progress, $options, $exec)
113
    {
114
        do {
115 5
            $this->_retry = false;
116
117 5
            $request = new Aspects\HttpGetRequest($origin, $fileUrl, $this->io);
118 5
            $request->setSpecial(array(
119 5
                'github' => $this->config->get('github-domains') ?: array(),
120 5
                'gitlab' => $this->config->get('gitlab-domains') ?: array(),
121 5
            ));
122 5
            $this->onPreDownload = Factory::getPreEvent($request);
123 5
            $this->onPostDownload = Factory::getPostEvent($request);
124 5
            if ($this->_degradedMode) {
125
                $this->onPreDownload->attach(new Aspects\AspectDegradedMode);
126
            }
127
128 5
            $options += $this->options;
129
            // override
130 5
            if ('github' === $request->special && isset($options['github-token'])) {
131
                $request->query['access_token'] = $options['github-token'];
132
            }
133 5
            if ('gitlab' === $request->special && isset($options['gitlab-token'])) {
134
                $request->query['access_token'] = $options['gitlab-token'];
135
            }
136
137 5
            if ($this->io->isDebug()) {
138
                $this->io->write('Downloading ' . $fileUrl);
139
            }
140
141 5
            if ($progress) {
142 4
                $this->io->write("    Downloading: <comment>Connecting...</comment>", false);
143 4
                $request->curlOpts[CURLOPT_NOPROGRESS] = false;
144 4
                $request->curlOpts[CURLOPT_PROGRESSFUNCTION] = array($this, 'progress');
145 4
            } else {
146 1
                $request->curlOpts[CURLOPT_NOPROGRESS] = true;
147 1
                $request->curlOpts[CURLOPT_PROGRESSFUNCTION] = null;
148
            }
149
150 5
            $this->onPreDownload->notify();
151
152 5
            $opts = $request->getCurlOpts();
153 5
            $ch = Factory::getConnection($origin, isset($opts[CURLOPT_USERPWD]));
154
155 5
            if ($this->pluginConfig['insecure']) {
156
                $opts[CURLOPT_SSL_VERIFYPEER] = false;
157
            }
158 5
            if (! empty($pluginConfig['capath'])) {
159
                $opts[CURLOPT_CAPATH] = $pluginConfig['capath'];
160
            }
161
162 5
            curl_setopt_array($ch, $opts);
163
164 5
            list($execStatus, ) = $exec($ch, $request);
165 3
        } while ($this->_retry);
166
167 3
        if ($progress) {
168 2
            $this->io->overwrite("    Downloading: <comment>100%</comment>");
169 2
        }
170
171 3
        return $execStatus;
172
    }
173
174
    /**
175
     * Retrieve the options set in the constructor
176
     *
177
     * @return array Options
178
     */
179 1
    public function getOptions()
180
    {
181 1
        return $this->options;
182
    }
183
184
    /**
185
     * Returns the headers of the last request
186
     *
187
     * @return array
188
     */
189 1
    public function getLastHeaders()
190
    {
191 1
        return $this->_lastHeaders;
192
    }
193
194
    /**
195
     * @internal
196
     * @param resource $ch
197
     * @param Aspects\HttpGetRequest $request
198
     * @return array {int, Aspects\HttpGetResponse}
199
     */
200 5
    public function exec($ch, $request)
201
    {
202 5
        $this->_lastHeaders = array();
203 5
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'processHeader'));
204 5
        $execStatus = curl_exec($ch);
205
206 5
        $response = new Aspects\HttpGetResponse(
207 5
            curl_errno($ch),
208 5
            curl_error($ch),
209 5
            curl_getinfo($ch)
210 5
        );
211 5
        $this->onPostDownload->setResponse($response);
212 5
        $this->onPostDownload->notify();
213
214 5
        if ($response->needAuth()) {
215 3
            $this->promptAuth($request, $response);
216 1
        }
217
218 3
        return array($execStatus, $response);
219
    }
220
221
    /**
222
     * @internal
223
     */
224 4
    public function progress()
225
    {
226
        // @codeCoverageIgnoreStart
227
        if (PHP_VERSION_ID >= 50500) {
228
            list(, $downBytesMax, $downBytes, , ) = func_get_args();
229
        } else {
230
            list($downBytesMax, $downBytes, , ) = func_get_args();
231
        }
232
        // @codeCoverageIgnoreEnd
233
234 4
        if ($downBytesMax <= 0 || $downBytesMax < $downBytes) {
235 1
            return 0;
236
        }
237
238 4
        $progression = intval($downBytes / $downBytesMax * 100);
239 4
        $this->io->overwrite("    Downloading: <comment>$progression%</comment>", false);
240 4
        return 0;
241
    }
242
243
    /**
244
     * @internal
245
     * @param resource $ch
246
     * @param string $header
247
     * @return int
248
     */
249 5
    public function processHeader($ch, $header)
250
    {
251 5
        $this->_lastHeaders[] = trim($header);
252 5
        return strlen($header);
253
    }
254
255 3
    protected function promptAuth(Aspects\HttpGetRequest $req, Aspects\HttpGetResponse $res)
256
    {
257 3
        $io = $this->io;
258 3
        $httpCode = $res->info['http_code'];
259
260 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...
261
            $message = "\nCould not fetch {$req->getURL()}, please create a GitHub OAuth token ";
262
            if (404 === $httpCode) {
263
                $message .= 'to access private repos';
264
            } else {
265
                $message .= 'to go over the API rate limit';
266
            }
267
            $github = new Util\GitHub($io, $this->config, null);
268
            if ($github->authorizeOAuth($req->origin)) {
269
                $this->_retry = true;
270
                return;
271
            }
272
            if ($io->isInteractive() &&
273
                $github->authorizeOAuthInteractively($req->origin, $message)) {
274
                $this->_retry = true;
275
                return;
276
            }
277
278
            throw new Downloader\TransportException(
279
                "Could not authenticate against $req->origin",
280
                401
281
            );
282
        }
283
        
284 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...
285
            $message = "\nCould not fetch {$req->getURL()}, enter your $req->origin credentials ";
286
            if (401 === $httpCode) {
287
                $message .= 'to access private repos';
288
            } else {
289
                $message .= 'to go over the API rate limit';
290
            }
291
            $gitlab = new Util\GitLab($io, $this->config, null);
292
            if ($gitlab->authorizeOAuth($req->origin)) {
293
                $this->_retry = true;
294
                return;
295
            }
296
            if ($io->isInteractive() &&
297
                $gitlab->authorizeOAuthInteractively($req->origin, $message)) {
298
                $this->_retry = true;
299
                return;
300
            }
301
302
            throw new Downloader\TransportException(
303
                "Could not authenticate against $req->origin",
304
                401
305
            );
306
        }
307
308
        // 404s are only handled for github
309 3
        if (404 === $httpCode) {
310 1
            return;
311
        }
312
313
        // fail if the console is not interactive
314 2
        if (!$io->isInteractive()) {
315
            switch ($httpCode) {
316 2
                case 401:
317 1
                    $message = "The '{$req->getURL()}' URL required authentication.\nYou must be using the interactive console to authenticate";
318 1
                    break;
319 1
                case 403:
320 1
                    $message = "The '{$req->getURL()}' URL could not be accessed.";
321 1
                    break;
322
            }
323 2
            throw new Downloader\TransportException($message, $httpCode);
324
        }
325
326
        // fail if we already have auth
327
        if ($io->hasAuthentication($req->origin)) {
328
            throw new Downloader\TransportException(
329
                "Invalid credentials for '{$req->getURL()}', aborting.",
330
                $res->info['http_code']
331
            );
332
        }
333
334
        $io->overwrite("    Authentication required (<info>$req->host</info>):");
335
        $username = $io->ask('      Username: ');
336
        $password = $io->askAndHideAnswer('      Password: ');
337
        $io->setAuthentication($req->origin, $username, $password);
338
        $this->_retry = true;
339
    }
340
}
341