Completed
Pull Request — master (#31)
by Hiraku
02:06
created

CurlRemoteFilesystem::copy()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2.0185

Importance

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