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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.