Add HttpDownloader to wrap/replace RemoteFilesystem with a new curl multi implementation
parent
618e21f1c1
commit
56805ecafe
@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Util\Http;
|
||||
|
||||
use Composer\Config;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use React\Promise\Promise;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class CurlDownloader
|
||||
{
|
||||
private $multiHandle;
|
||||
private $shareHandle;
|
||||
private $jobs = array();
|
||||
private $io;
|
||||
private $selectTimeout = 5.0;
|
||||
protected $multiErrors = array(
|
||||
CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'),
|
||||
CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."),
|
||||
CURLM_OUT_OF_MEMORY => array('CURLM_OUT_OF_MEMORY', 'You are doomed.'),
|
||||
CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!')
|
||||
);
|
||||
|
||||
private static $options = array(
|
||||
'http' => array(
|
||||
'method' => CURLOPT_CUSTOMREQUEST,
|
||||
'content' => CURLOPT_POSTFIELDS,
|
||||
'proxy' => CURLOPT_PROXY,
|
||||
),
|
||||
'ssl' => array(
|
||||
'ciphers' => CURLOPT_SSL_CIPHER_LIST,
|
||||
'cafile' => CURLOPT_CAINFO,
|
||||
'capath' => CURLOPT_CAPATH,
|
||||
),
|
||||
);
|
||||
|
||||
private static $timeInfo = array(
|
||||
'total_time' => true,
|
||||
'namelookup_time' => true,
|
||||
'connect_time' => true,
|
||||
'pretransfer_time' => true,
|
||||
'starttransfer_time' => true,
|
||||
'redirect_time' => true,
|
||||
);
|
||||
|
||||
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
|
||||
{
|
||||
$this->io = $io;
|
||||
|
||||
$this->multiHandle = $mh = curl_multi_init();
|
||||
if (function_exists('curl_multi_setopt')) {
|
||||
curl_multi_setopt($mh, CURLMOPT_PIPELINING, /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3);
|
||||
if (defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
|
||||
curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8);
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('curl_share_init')) {
|
||||
$this->shareHandle = $sh = curl_share_init();
|
||||
curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
|
||||
curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
|
||||
curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
|
||||
}
|
||||
}
|
||||
|
||||
public function download($resolve, $reject, $origin, $url, $options, $copyTo = null)
|
||||
{
|
||||
$ch = curl_init();
|
||||
$hd = fopen('php://temp/maxmemory:32768', 'w+b');
|
||||
|
||||
// TODO auth & other context
|
||||
// TODO cleanup
|
||||
|
||||
if ($copyTo && !$fd = @fopen($copyTo.'~', 'w+b')) {
|
||||
// TODO throw here probably?
|
||||
$copyTo = null;
|
||||
}
|
||||
if (!$copyTo) {
|
||||
$fd = @fopen('php://temp/maxmemory:524288', 'w+b');
|
||||
}
|
||||
|
||||
if (!isset($options['http']['header'])) {
|
||||
$options['http']['header'] = array();
|
||||
}
|
||||
|
||||
$headers = array_diff($options['http']['header'], array('Connection: close'));
|
||||
|
||||
// TODO
|
||||
$degradedMode = false;
|
||||
if ($degradedMode) {
|
||||
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
|
||||
} else {
|
||||
$headers[] = 'Connection: keep-alive';
|
||||
$version = curl_version();
|
||||
$features = $version['features'];
|
||||
if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) {
|
||||
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||
}
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
//curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // TODO increase
|
||||
curl_setopt($ch, CURLOPT_WRITEHEADER, $hd);
|
||||
curl_setopt($ch, CURLOPT_FILE, $fd);
|
||||
if (function_exists('curl_share_init')) {
|
||||
curl_setopt($ch, CURLOPT_SHARE, $this->shareHandle);
|
||||
}
|
||||
|
||||
foreach (self::$options as $type => $curlOptions) {
|
||||
foreach ($curlOptions as $name => $curlOption) {
|
||||
if (isset($options[$type][$name])) {
|
||||
curl_setopt($ch, $curlOption, $options[$type][$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$progress = array_diff_key(curl_getinfo($ch), self::$timeInfo);
|
||||
|
||||
$this->jobs[(int) $ch] = array(
|
||||
'progress' => $progress,
|
||||
'ch' => $ch,
|
||||
//'callback' => $params['notification'],
|
||||
'file' => $copyTo,
|
||||
'hd' => $hd,
|
||||
'fd' => $fd,
|
||||
'resolve' => $resolve,
|
||||
'reject' => $reject,
|
||||
);
|
||||
|
||||
$this->io->write('Downloading '.$url, true, IOInterface::DEBUG);
|
||||
|
||||
$this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $ch));
|
||||
//$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false);
|
||||
}
|
||||
|
||||
public function tick()
|
||||
{
|
||||
// TODO check we have active handles before doing this
|
||||
if (!$this->jobs) {
|
||||
return;
|
||||
}
|
||||
|
||||
$active = true;
|
||||
try {
|
||||
$this->checkCurlResult(curl_multi_exec($this->multiHandle, $active));
|
||||
if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) {
|
||||
// sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select
|
||||
usleep(150);
|
||||
}
|
||||
|
||||
while ($progress = curl_multi_info_read($this->multiHandle)) {
|
||||
$h = $progress['handle'];
|
||||
$i = (int) $h;
|
||||
if (!isset($this->jobs[$i])) {
|
||||
continue;
|
||||
}
|
||||
$progress = array_diff_key(curl_getinfo($h), self::$timeInfo);
|
||||
$job = $this->jobs[$i];
|
||||
unset($this->jobs[$i]);
|
||||
curl_multi_remove_handle($this->multiHandle, $h);
|
||||
$error = curl_error($h);
|
||||
$errno = curl_errno($h);
|
||||
curl_close($h);
|
||||
|
||||
try {
|
||||
//$this->onProgress($h, $job['callback'], $progress, $job['progress']);
|
||||
if ('' !== $error) {
|
||||
throw new TransportException(curl_error($h));
|
||||
}
|
||||
|
||||
if ($job['file']) {
|
||||
if (CURLE_OK === $errno) {
|
||||
fclose($job['fd']);
|
||||
rename($job['file'].'~', $job['file']);
|
||||
call_user_func($job['resolve'], true);
|
||||
}
|
||||
// TODO otherwise show error?
|
||||
} else {
|
||||
rewind($job['hd']);
|
||||
$headers = explode("\r\n", rtrim(stream_get_contents($job['hd'])));
|
||||
fclose($job['hd']);
|
||||
rewind($job['fd']);
|
||||
$contents = stream_get_contents($job['fd']);
|
||||
fclose($job['fd']);
|
||||
$this->io->writeError('['.$progress['http_code'].'] '.$progress['url'], true, IOInterface::DEBUG);
|
||||
call_user_func($job['resolve'], new Response(array('url' => $progress['url']), $progress['http_code'], $headers, $contents));
|
||||
}
|
||||
} catch (TransportException $e) {
|
||||
fclose($job['hd']);
|
||||
fclose($job['fd']);
|
||||
if ($job['file']) {
|
||||
@unlink($job['file'].'~');
|
||||
}
|
||||
call_user_func($job['reject'], $e);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->jobs as $i => $h) {
|
||||
if (!isset($this->jobs[$i])) {
|
||||
continue;
|
||||
}
|
||||
$h = $this->jobs[$i]['ch'];
|
||||
$progress = array_diff_key(curl_getinfo($h), self::$timeInfo);
|
||||
|
||||
if ($this->jobs[$i]['progress'] !== $progress) {
|
||||
$previousProgress = $this->jobs[$i]['progress'];
|
||||
$this->jobs[$i]['progress'] = $progress;
|
||||
try {
|
||||
//$this->onProgress($h, $this->jobs[$i]['callback'], $progress, $previousProgress);
|
||||
} catch (TransportException $e) {
|
||||
var_dump('Caught '.$e->getMessage());die;
|
||||
unset($this->jobs[$i]);
|
||||
curl_multi_remove_handle($this->multiHandle, $h);
|
||||
curl_close($h);
|
||||
|
||||
fclose($job['hd']);
|
||||
fclose($job['fd']);
|
||||
if ($job['file']) {
|
||||
@unlink($job['file'].'~');
|
||||
}
|
||||
call_user_func($job['reject'], $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
var_dump('Caught2', get_class($e), $e->getMessage(), $e);die;
|
||||
}
|
||||
|
||||
// TODO finalize / resolve
|
||||
// if ($copyTo && !isset($this->exceptions[(int) $ch])) {
|
||||
// $fd = fopen($copyTo, 'rb');
|
||||
// }
|
||||
//
|
||||
}
|
||||
|
||||
private function onProgress($ch, callable $notify, array $progress, array $previousProgress)
|
||||
{
|
||||
if (300 <= $progress['http_code'] && $progress['http_code'] < 400) {
|
||||
return;
|
||||
}
|
||||
if (!$previousProgress['http_code'] && $progress['http_code'] && $progress['http_code'] < 200 || 400 <= $progress['http_code']) {
|
||||
$code = 403 === $progress['http_code'] ? STREAM_NOTIFY_AUTH_RESULT : STREAM_NOTIFY_FAILURE;
|
||||
$notify($code, STREAM_NOTIFY_SEVERITY_ERR, curl_error($ch), $progress['http_code'], 0, 0, false);
|
||||
}
|
||||
if ($previousProgress['download_content_length'] < $progress['download_content_length']) {
|
||||
$notify(STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false);
|
||||
}
|
||||
if ($previousProgress['size_download'] < $progress['size_download']) {
|
||||
$notify(STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['download_content_length'], false);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkCurlResult($code)
|
||||
{
|
||||
if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) {
|
||||
throw new \RuntimeException(isset($this->multiErrors[$code])
|
||||
? "cURL error: {$code} ({$this->multiErrors[$code][0]}): cURL message: {$this->multiErrors[$code][1]}"
|
||||
: 'Unexpected cURL error: ' . $code
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Util\Http;
|
||||
|
||||
use Composer\Json\JsonFile;
|
||||
|
||||
class Response
|
||||
{
|
||||
private $request;
|
||||
private $code;
|
||||
private $headers;
|
||||
private $body;
|
||||
|
||||
public function __construct(array $request, $code, array $headers, $body)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->code = $code;
|
||||
$this->headers = $headers;
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function getHeaders()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getHeader($name)
|
||||
{
|
||||
$value = null;
|
||||
foreach ($this->headers as $header) {
|
||||
if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) {
|
||||
$value = $match[1];
|
||||
} elseif (preg_match('{^HTTP/}i', $header)) {
|
||||
// TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary
|
||||
//
|
||||
// In case of redirects, http_response_headers contains the headers of all responses
|
||||
// so we reset the flag when a new response is being parsed as we are only interested in the last response
|
||||
$value = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function decodeJson()
|
||||
{
|
||||
return JsonFile::parseJson($this->body, $this->request['url']);
|
||||
}
|
||||
|
||||
public function collect()
|
||||
{
|
||||
$this->request = $this->code = $this->headers = $this->body = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Util;
|
||||
|
||||
use Composer\Config;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use React\Promise\Promise;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class HttpDownloader
|
||||
{
|
||||
const STATUS_QUEUED = 1;
|
||||
const STATUS_STARTED = 2;
|
||||
const STATUS_COMPLETED = 3;
|
||||
const STATUS_FAILED = 4;
|
||||
|
||||
private $io;
|
||||
private $config;
|
||||
private $jobs = array();
|
||||
private $index;
|
||||
private $progress;
|
||||
private $lastProgress;
|
||||
private $disableTls = false;
|
||||
private $curl;
|
||||
private $rfs;
|
||||
private $idGen = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param IOInterface $io The IO instance
|
||||
* @param Config $config The config
|
||||
* @param array $options The options
|
||||
* @param bool $disableTls
|
||||
*/
|
||||
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
|
||||
{
|
||||
$this->io = $io;
|
||||
|
||||
// Setup TLS options
|
||||
// The cafile option can be set via config.json
|
||||
if ($disableTls === false) {
|
||||
$logger = $io instanceof LoggerInterface ? $io : null;
|
||||
$this->options = StreamContextFactory::getTlsDefaults($options, $logger);
|
||||
} else {
|
||||
$this->disableTls = true;
|
||||
}
|
||||
|
||||
// handle the other externally set options normally.
|
||||
$this->options = array_replace_recursive($this->options, $options);
|
||||
$this->config = $config;
|
||||
|
||||
if (extension_loaded('curl')) {
|
||||
$this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls);
|
||||
}
|
||||
|
||||
$this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
|
||||
}
|
||||
|
||||
public function get($url, $options = array())
|
||||
{
|
||||
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true);
|
||||
$this->wait($job['id']);
|
||||
|
||||
return $this->getResponse($job['id']);
|
||||
}
|
||||
|
||||
public function add($url, $options = array())
|
||||
{
|
||||
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false));
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
public function copy($url, $to, $options = array())
|
||||
{
|
||||
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true);
|
||||
$this->wait($job['id']);
|
||||
|
||||
return $this->getResponse($job['id']);
|
||||
}
|
||||
|
||||
public function addCopy($url, $to, $options = array())
|
||||
{
|
||||
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to));
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
private function addJob($request, $sync = false)
|
||||
{
|
||||
$job = array(
|
||||
'id' => $this->idGen++,
|
||||
'status' => self::STATUS_QUEUED,
|
||||
'request' => $request,
|
||||
'sync' => $sync,
|
||||
);
|
||||
|
||||
$curl = $this->curl;
|
||||
$rfs = $this->rfs;
|
||||
$io = $this->io;
|
||||
|
||||
$origin = $this->getOrigin($job['request']['url']);
|
||||
|
||||
// TODO only send http/https through curl
|
||||
if ($curl) {
|
||||
$resolver = function ($resolve, $reject) use (&$job, $curl, $origin) {
|
||||
// start job
|
||||
$url = $job['request']['url'];
|
||||
$options = $job['request']['options'];
|
||||
|
||||
$job['status'] = HttpDownloader::STATUS_STARTED;
|
||||
|
||||
if ($job['request']['copyTo']) {
|
||||
$curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
|
||||
} else {
|
||||
$curl->download($resolve, $reject, $origin, $url, $options);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
$resolver = function ($resolve, $reject) use (&$job, $rfs, $curl, $origin) {
|
||||
// start job
|
||||
$url = $job['request']['url'];
|
||||
$options = $job['request']['options'];
|
||||
|
||||
$job['status'] = HttpDownloader::STATUS_STARTED;
|
||||
|
||||
if ($job['request']['copyTo']) {
|
||||
if ($curl) {
|
||||
$result = $curl->download($origin, $url, $options, $job['request']['copyTo']);
|
||||
} else {
|
||||
$result = $rfs->copy($origin, $url, $job['request']['copyTo'], false /* TODO progress */, $options);
|
||||
}
|
||||
|
||||
$resolve($result);
|
||||
} else {
|
||||
$body = $rfs->getContents($origin, $url, false /* TODO progress */, $options);
|
||||
$headers = $rfs->getLastHeaders();
|
||||
$response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body);
|
||||
|
||||
$resolve($response);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$canceler = function () {};
|
||||
|
||||
$promise = new Promise($resolver, $canceler);
|
||||
$promise->then(function ($response) use (&$job) {
|
||||
$job['status'] = HttpDownloader::STATUS_COMPLETED;
|
||||
$job['response'] = $response;
|
||||
// TODO look for more jobs to start once we throttle to max X jobs
|
||||
}, function ($e) use ($io, &$job) {
|
||||
var_dump(__CLASS__ . __LINE__);
|
||||
var_dump(gettype($e));
|
||||
var_dump($e->getMessage());
|
||||
die;
|
||||
$job['status'] = HttpDownloader::STATUS_FAILED;
|
||||
$job['exception'] = $e;
|
||||
});
|
||||
$this->jobs[$job['id']] =& $job;
|
||||
|
||||
return array($job, $promise);
|
||||
}
|
||||
|
||||
public function wait($index = null, $progress = false)
|
||||
{
|
||||
while (true) {
|
||||
if ($this->curl) {
|
||||
$this->curl->tick();
|
||||
}
|
||||
|
||||
if (null !== $index) {
|
||||
if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$done = true;
|
||||
foreach ($this->jobs as $job) {
|
||||
if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) {
|
||||
$done = false;
|
||||
break;
|
||||
} elseif (!$job['sync']) {
|
||||
unset($this->jobs[$job['id']]);
|
||||
}
|
||||
}
|
||||
if ($done) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private function getResponse($index)
|
||||
{
|
||||
if (!isset($this->jobs[$index])) {
|
||||
throw new \LogicException('Invalid request id');
|
||||
}
|
||||
|
||||
if ($this->jobs[$index]['status'] === self::STATUS_FAILED) {
|
||||
throw $this->jobs[$index]['exception'];
|
||||
}
|
||||
|
||||
if (!isset($this->jobs[$index]['response'])) {
|
||||
throw new \LogicException('Response not available yet, call wait() first');
|
||||
}
|
||||
|
||||
$resp = $this->jobs[$index]['response'];
|
||||
|
||||
unset($this->jobs[$index]);
|
||||
|
||||
return $resp;
|
||||
}
|
||||
|
||||
private function getOrigin($url)
|
||||
{
|
||||
$origin = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
if ($origin === 'api.github.com') {
|
||||
return 'github.com';
|
||||
}
|
||||
|
||||
if ($origin === 'repo.packagist.org') {
|
||||
return 'packagist.org';
|
||||
}
|
||||
|
||||
return $origin ?: $url;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue