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

CurlRemoteFilesystem::getOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 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\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