Add a bunch of type info to Util namespace
parent
8559279025
commit
024f0eda53
|
@ -35,6 +35,10 @@ parameters:
|
|||
# Mock errors
|
||||
- '~^Call to an undefined method (PHPUnit\\Framework\\MockObject\\MockObject|Prophecy\\Prophecy\\ObjectProphecy)::.*$~'
|
||||
|
||||
# Level 6 TODO
|
||||
#- '~parameter .*? with no return typehint specified.$~'
|
||||
#- '~has no return typehint specified.$~'
|
||||
|
||||
bootstrapFiles:
|
||||
- ../tests/bootstrap.php
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ class AuthHelper
|
|||
* @param string[] $headers
|
||||
* @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be
|
||||
* retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json
|
||||
* @phpstan-return ?array{retry: bool, storeAuth: string|bool}
|
||||
*/
|
||||
public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $headers = array())
|
||||
{
|
||||
|
|
|
@ -26,11 +26,16 @@ use React\Promise\Promise;
|
|||
* @internal
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int, storeAuth: bool}
|
||||
* @phpstan-type Job array{url: string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: resource, filename: string|false, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable}
|
||||
*/
|
||||
class CurlDownloader
|
||||
{
|
||||
/** @var ?resource */
|
||||
private $multiHandle;
|
||||
/** @var ?resource */
|
||||
private $shareHandle;
|
||||
/** @var Job[] */
|
||||
private $jobs = array();
|
||||
/** @var IOInterface */
|
||||
private $io;
|
||||
|
@ -38,11 +43,15 @@ class CurlDownloader
|
|||
private $config;
|
||||
/** @var AuthHelper */
|
||||
private $authHelper;
|
||||
/** @var float */
|
||||
private $selectTimeout = 5.0;
|
||||
/** @var int */
|
||||
private $maxRedirects = 20;
|
||||
/** @var ProxyManager */
|
||||
private $proxyManager;
|
||||
/** @var bool */
|
||||
private $supportsSecureProxy;
|
||||
/** @var array<int, string[]> */
|
||||
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."),
|
||||
|
@ -50,6 +59,7 @@ class CurlDownloader
|
|||
CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!'),
|
||||
);
|
||||
|
||||
/** @var mixed[] */
|
||||
private static $options = array(
|
||||
'http' => array(
|
||||
'method' => CURLOPT_CUSTOMREQUEST,
|
||||
|
@ -68,6 +78,7 @@ class CurlDownloader
|
|||
),
|
||||
);
|
||||
|
||||
/** @var array<string, true> */
|
||||
private static $timeInfo = array(
|
||||
'total_time' => true,
|
||||
'namelookup_time' => true,
|
||||
|
@ -77,6 +88,10 @@ class CurlDownloader
|
|||
'redirect_time' => true,
|
||||
);
|
||||
|
||||
/**
|
||||
* @param mixed[] $options
|
||||
* @param bool $disableTls
|
||||
*/
|
||||
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
|
||||
{
|
||||
$this->io = $io;
|
||||
|
@ -106,6 +121,13 @@ class CurlDownloader
|
|||
}
|
||||
|
||||
/**
|
||||
* @param callable $resolve
|
||||
* @param callable $reject
|
||||
* @param string $origin
|
||||
* @param string $url
|
||||
* @param mixed[] $options
|
||||
* @param ?string $copyTo
|
||||
*
|
||||
* @return int internal job id
|
||||
*/
|
||||
public function download($resolve, $reject, $origin, $url, $options, $copyTo = null)
|
||||
|
@ -120,6 +142,15 @@ class CurlDownloader
|
|||
}
|
||||
|
||||
/**
|
||||
* @param callable $resolve
|
||||
* @param callable $reject
|
||||
* @param string $origin
|
||||
* @param string $url
|
||||
* @param mixed[] $options
|
||||
* @param ?string $copyTo
|
||||
*
|
||||
* @param array{retryAuthFailure?: bool, redirects?: int, storeAuth?: bool} $attributes
|
||||
*
|
||||
* @return int internal job id
|
||||
*/
|
||||
private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array())
|
||||
|
@ -245,6 +276,10 @@ class CurlDownloader
|
|||
return (int) $curlHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
public function abortRequest($id)
|
||||
{
|
||||
if (isset($this->jobs[$id], $this->jobs[$id]['handle'])) {
|
||||
|
@ -264,6 +299,9 @@ class CurlDownloader
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function tick()
|
||||
{
|
||||
if (!$this->jobs) {
|
||||
|
@ -409,6 +447,10 @@ class CurlDownloader
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Job $job
|
||||
* @return string
|
||||
*/
|
||||
private function handleRedirect(array $job, Response $response)
|
||||
{
|
||||
if ($locationHeader = $response->getHeader('location')) {
|
||||
|
@ -440,6 +482,10 @@ class CurlDownloader
|
|||
throw new TransportException('The "'.$job['url'].'" file could not be downloaded, got redirect without Location ('.$response->getStatusMessage().')');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Job $job
|
||||
* @return array{retry: bool, storeAuth: string|bool}
|
||||
*/
|
||||
private function isAuthenticatedRetryNeeded(array $job, Response $response)
|
||||
{
|
||||
if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) {
|
||||
|
@ -487,6 +533,14 @@ class CurlDownloader
|
|||
return array('retry' => false, 'storeAuth' => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Job $job
|
||||
* @param string $url
|
||||
*
|
||||
* @param array{retryAuthFailure?: bool, redirects?: int, storeAuth?: bool} $attributes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function restartJob(array $job, $url, array $attributes = array())
|
||||
{
|
||||
if ($job['filename']) {
|
||||
|
@ -499,6 +553,11 @@ class CurlDownloader
|
|||
$this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['options'], $job['filename'], $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Job $job
|
||||
* @param string $errorMessage
|
||||
* @return TransportException
|
||||
*/
|
||||
private function failResponse(array $job, Response $response, $errorMessage)
|
||||
{
|
||||
if ($job['filename']) {
|
||||
|
@ -513,6 +572,10 @@ class CurlDownloader
|
|||
return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')' . $details, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Job $job
|
||||
* @return void
|
||||
*/
|
||||
private function rejectJob(array $job, \Exception $e)
|
||||
{
|
||||
if (is_resource($job['headerHandle'])) {
|
||||
|
@ -527,6 +590,10 @@ class CurlDownloader
|
|||
call_user_func($job['reject'], $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
* @return void
|
||||
*/
|
||||
private function checkCurlResult($code)
|
||||
{
|
||||
if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) {
|
||||
|
|
|
@ -12,10 +12,20 @@
|
|||
|
||||
namespace Composer\Util\Http;
|
||||
|
||||
/**
|
||||
* @phpstan-type CurlInfo array{url: mixed, content_type: mixed, http_code: mixed, header_size: mixed, request_size: mixed, filetime: mixed, ssl_verify_result: mixed, redirect_count: mixed, total_time: mixed, namelookup_time: mixed, connect_time: mixed, pretransfer_time: mixed, size_upload: mixed, size_download: mixed, speed_download: mixed, speed_upload: mixed, download_content_length: mixed, upload_content_length: mixed, starttransfer_time: mixed, redirect_time: mixed, certinfo: mixed, primary_ip: mixed, primary_port: mixed, local_ip: mixed, local_port: mixed, redirect_url: mixed}
|
||||
*/
|
||||
class CurlResponse extends Response
|
||||
{
|
||||
/**
|
||||
* @see https://www.php.net/curl_getinfo
|
||||
* @var CurlInfo
|
||||
*/
|
||||
private $curlInfo;
|
||||
|
||||
/**
|
||||
* @param CurlInfo $curlInfo
|
||||
*/
|
||||
public function __construct(array $request, $code, array $headers, $body, array $curlInfo)
|
||||
{
|
||||
parent::__construct($request, $code, $headers, $body);
|
||||
|
@ -23,7 +33,7 @@ class CurlResponse extends Response
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @return CurlInfo
|
||||
*/
|
||||
public function getCurlInfo()
|
||||
{
|
||||
|
|
|
@ -20,15 +20,19 @@ use Composer\Util\Url;
|
|||
*/
|
||||
class RequestProxy
|
||||
{
|
||||
/** @var mixed[] */
|
||||
private $contextOptions;
|
||||
/** @var bool */
|
||||
private $isSecure;
|
||||
/** @var string */
|
||||
private $formattedUrl;
|
||||
/** @var string */
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param array $contextOptions
|
||||
* @param string $formattedUrl
|
||||
* @param string $url
|
||||
* @param mixed[] $contextOptions
|
||||
* @param string $formattedUrl
|
||||
*/
|
||||
public function __construct($url, array $contextOptions, $formattedUrl)
|
||||
{
|
||||
|
@ -41,7 +45,7 @@ class RequestProxy
|
|||
/**
|
||||
* Returns an array of context options
|
||||
*
|
||||
* @return array
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getContextOptions()
|
||||
{
|
||||
|
|
|
@ -13,14 +13,28 @@
|
|||
namespace Composer\Util\Http;
|
||||
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Util\HttpDownloader;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type Request from HttpDownloader
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
/** @var Request */
|
||||
private $request;
|
||||
/** @var int */
|
||||
private $code;
|
||||
/** @var string[] */
|
||||
private $headers;
|
||||
/** @var ?string */
|
||||
private $body;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param int $code
|
||||
* @param string[] $headers
|
||||
* @param ?string $body
|
||||
*/
|
||||
public function __construct(array $request, $code, array $headers, $body)
|
||||
{
|
||||
if (!isset($request['url'])) {
|
||||
|
@ -32,6 +46,9 @@ class Response
|
|||
$this->body = $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->code;
|
||||
|
@ -54,33 +71,51 @@ class Response
|
|||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return ?string
|
||||
*/
|
||||
public function getHeader($name)
|
||||
{
|
||||
return self::findHeaderValue($this->headers, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function decodeJson()
|
||||
{
|
||||
return JsonFile::parseJson($this->body, $this->request['url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @phpstan-impure
|
||||
*/
|
||||
public function collect()
|
||||
{
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->request = $this->code = $this->headers = $this->body = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers array of returned headers like from getLastHeaders()
|
||||
* @param string[] $headers array of returned headers like from getLastHeaders()
|
||||
* @param string $name header name (case insensitive)
|
||||
* @return string|null
|
||||
*/
|
||||
|
|
|
@ -16,14 +16,18 @@ use Composer\Config;
|
|||
use Composer\IO\IOInterface;
|
||||
use Composer\Downloader\TransportException;
|
||||
use Composer\Util\Http\Response;
|
||||
use Composer\Util\Http\CurlDownloader;
|
||||
use Composer\Composer;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use Composer\Semver\Constraint\Constraint;
|
||||
use Composer\Exception\IrrecoverableDownloadException;
|
||||
use React\Promise\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @phpstan-type Request array{url: string, options?: mixed[], copyTo?: ?string}
|
||||
* @phpstan-type Job array{id: int, status: int, request: Request, sync: bool, origin: string, resolve?: callable, reject?: callable, curl_id?: int, response?: Response, exception?: TransportException}
|
||||
*/
|
||||
class HttpDownloader
|
||||
{
|
||||
|
@ -33,22 +37,33 @@ class HttpDownloader
|
|||
const STATUS_FAILED = 4;
|
||||
const STATUS_ABORTED = 5;
|
||||
|
||||
/** @var IOInterface */
|
||||
private $io;
|
||||
/** @var Config */
|
||||
private $config;
|
||||
/** @var array<Job> */
|
||||
private $jobs = array();
|
||||
/** @var mixed[] */
|
||||
private $options = array();
|
||||
/** @var int */
|
||||
private $runningJobs = 0;
|
||||
/** @var int */
|
||||
private $maxJobs = 12;
|
||||
/** @var ?CurlDownloader */
|
||||
private $curl;
|
||||
/** @var ?RemoteFilesystem */
|
||||
private $rfs;
|
||||
/** @var int */
|
||||
private $idGen = 0;
|
||||
/** @var bool */
|
||||
private $disabled;
|
||||
/** @var bool */
|
||||
private $allowAsync = false;
|
||||
|
||||
/**
|
||||
* @param IOInterface $io The IO instance
|
||||
* @param Config $config The config
|
||||
* @param array $options The options
|
||||
* @param mixed[] $options The options
|
||||
* @param bool $disableTls
|
||||
*/
|
||||
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
|
||||
|
@ -68,7 +83,7 @@ class HttpDownloader
|
|||
$this->config = $config;
|
||||
|
||||
if (self::isCurlEnabled()) {
|
||||
$this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls);
|
||||
$this->curl = new CurlDownloader($io, $config, $options, $disableTls);
|
||||
}
|
||||
|
||||
$this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
|
||||
|
@ -82,14 +97,14 @@ class HttpDownloader
|
|||
* Download a file synchronously
|
||||
*
|
||||
* @param string $url URL to download
|
||||
* @param array $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* although not all options are supported when using the default curl downloader
|
||||
* @throws TransportException
|
||||
* @return Response
|
||||
*/
|
||||
public function get($url, $options = array())
|
||||
{
|
||||
list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true);
|
||||
list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => null), true);
|
||||
$this->wait($job['id']);
|
||||
|
||||
$response = $this->getResponse($job['id']);
|
||||
|
@ -106,7 +121,7 @@ class HttpDownloader
|
|||
|
||||
$this->curl = null;
|
||||
|
||||
list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true);
|
||||
list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => null), true);
|
||||
$this->wait($job['id']);
|
||||
|
||||
$response = $this->getResponse($job['id']);
|
||||
|
@ -119,14 +134,14 @@ class HttpDownloader
|
|||
* Create an async download operation
|
||||
*
|
||||
* @param string $url URL to download
|
||||
* @param array $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* although not all options are supported when using the default curl downloader
|
||||
* @throws TransportException
|
||||
* @return Promise
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function add($url, $options = array())
|
||||
{
|
||||
list(, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false));
|
||||
list(, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => null));
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
@ -136,7 +151,7 @@ class HttpDownloader
|
|||
*
|
||||
* @param string $url URL to download
|
||||
* @param string $to Path to copy to
|
||||
* @param array $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* although not all options are supported when using the default curl downloader
|
||||
* @throws TransportException
|
||||
* @return Response
|
||||
|
@ -154,10 +169,10 @@ class HttpDownloader
|
|||
*
|
||||
* @param string $url URL to download
|
||||
* @param string $to Path to copy to
|
||||
* @param array $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php
|
||||
* although not all options are supported when using the default curl downloader
|
||||
* @throws TransportException
|
||||
* @return Promise
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function addCopy($url, $to, $options = array())
|
||||
{
|
||||
|
@ -169,7 +184,7 @@ class HttpDownloader
|
|||
/**
|
||||
* Retrieve the options set in the constructor
|
||||
*
|
||||
* @return array Options
|
||||
* @return mixed[] Options
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
|
@ -179,6 +194,7 @@ class HttpDownloader
|
|||
/**
|
||||
* Merges new options
|
||||
*
|
||||
* @param mixed[] $options
|
||||
* @return void
|
||||
*/
|
||||
public function setOptions(array $options)
|
||||
|
@ -186,10 +202,17 @@ class HttpDownloader
|
|||
$this->options = array_replace_recursive($this->options, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param bool $sync
|
||||
*
|
||||
* @return array{Job, PromiseInterface}
|
||||
*/
|
||||
private function addJob($request, $sync = false)
|
||||
{
|
||||
$request['options'] = array_replace_recursive($this->options, $request['options']);
|
||||
|
||||
/** @var Job */
|
||||
$job = array(
|
||||
'id' => $this->idGen++,
|
||||
'status' => self::STATUS_QUEUED,
|
||||
|
@ -283,6 +306,10 @@ class HttpDownloader
|
|||
return array($job, $promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
private function startJob($id)
|
||||
{
|
||||
$job = &$this->jobs[$id];
|
||||
|
@ -325,6 +352,7 @@ class HttpDownloader
|
|||
|
||||
/**
|
||||
* @private
|
||||
* @return void
|
||||
*/
|
||||
public function markJobDone()
|
||||
{
|
||||
|
@ -335,6 +363,8 @@ class HttpDownloader
|
|||
* Wait for current async download jobs to complete
|
||||
*
|
||||
* @param int|null $index For internal use only, the job id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function wait($index = null)
|
||||
{
|
||||
|
@ -345,6 +375,8 @@ class HttpDownloader
|
|||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableAsync()
|
||||
{
|
||||
|
@ -387,6 +419,10 @@ class HttpDownloader
|
|||
return $active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $index Job id
|
||||
* @return Response
|
||||
*/
|
||||
private function getResponse($index)
|
||||
{
|
||||
if (!isset($this->jobs[$index])) {
|
||||
|
@ -410,6 +446,10 @@ class HttpDownloader
|
|||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @param string $url
|
||||
* @param array{warning?: string, info?: string, warning-versions?: string, info-versions?: string} $data
|
||||
* @return void
|
||||
*/
|
||||
public static function outputWarnings(IOInterface $io, $url, $data)
|
||||
{
|
||||
|
@ -433,11 +473,13 @@ class HttpDownloader
|
|||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return ?string[]
|
||||
*/
|
||||
public static function getExceptionHints(\Exception $e)
|
||||
{
|
||||
if (!$e instanceof TransportException) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -460,8 +502,14 @@ class HttpDownloader
|
|||
'<error>The following exception probably indicates you are offline or have misconfigured DNS resolver(s)</error>',
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Job $job
|
||||
* @return bool
|
||||
*/
|
||||
private function canUseCurl(array $job)
|
||||
{
|
||||
if (!$this->curl) {
|
||||
|
|
|
@ -29,7 +29,7 @@ class IniHelper
|
|||
* The equivalent of calling php_ini_loaded_file then php_ini_scanned_files.
|
||||
* The loaded ini location is the first entry and may be empty.
|
||||
*
|
||||
* @return array
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getAll()
|
||||
{
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
|
||||
namespace Composer\Util;
|
||||
|
||||
use React\Promise\Promise;
|
||||
use React\Promise\CancellablePromiseInterface;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
|
@ -24,7 +25,7 @@ class Loop
|
|||
private $httpDownloader;
|
||||
/** @var ProcessExecutor|null */
|
||||
private $processExecutor;
|
||||
/** @var Promise[][] */
|
||||
/** @var PromiseInterface[][] */
|
||||
private $currentPromises = array();
|
||||
/** @var int */
|
||||
private $waitIndex = 0;
|
||||
|
@ -56,6 +57,11 @@ class Loop
|
|||
return $this->processExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseInterface[] $promises
|
||||
* @param ?ProgressBar $progress
|
||||
* @return void
|
||||
*/
|
||||
public function wait(array $promises, ProgressBar $progress = null)
|
||||
{
|
||||
/** @var \Exception|null */
|
||||
|
@ -113,11 +119,16 @@ class Loop
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function abortJobs()
|
||||
{
|
||||
foreach ($this->currentPromises as $promiseGroup) {
|
||||
foreach ($promiseGroup as $promise) {
|
||||
$promise->cancel();
|
||||
if ($promise instanceof CancellablePromiseInterface) {
|
||||
$promise->cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -420,6 +420,8 @@ class NoProxyPattern
|
|||
* @param string $int
|
||||
* @param int $min
|
||||
* @param int $max
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function validateInt($int, $min, $max)
|
||||
{
|
||||
|
|
|
@ -142,6 +142,10 @@ class Platform
|
|||
return \strlen($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?resource $fd Open file descriptor or null to default to STDOUT
|
||||
* @return bool
|
||||
*/
|
||||
public static function isTty($fd = null)
|
||||
{
|
||||
if ($fd === null) {
|
||||
|
@ -170,6 +174,9 @@ class Platform
|
|||
return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function workaroundFilesystemIssues()
|
||||
{
|
||||
if (self::isVirtualBoxGuest()) {
|
||||
|
|
|
@ -31,6 +31,7 @@ class ProcessExecutor
|
|||
const STATUS_FAILED = 4;
|
||||
const STATUS_ABORTED = 5;
|
||||
|
||||
/** @var int */
|
||||
protected static $timeout = 300;
|
||||
|
||||
/** @var bool */
|
||||
|
@ -44,9 +45,13 @@ class ProcessExecutor
|
|||
* @phpstan-var array<int, array<string, mixed>>
|
||||
*/
|
||||
private $jobs = array();
|
||||
/** @var int */
|
||||
private $runningJobs = 0;
|
||||
/** @var int */
|
||||
private $maxJobs = 10;
|
||||
/** @var int */
|
||||
private $idGen = 0;
|
||||
/** @var bool */
|
||||
private $allowAsync = false;
|
||||
|
||||
public function __construct(IOInterface $io = null)
|
||||
|
@ -57,11 +62,11 @@ class ProcessExecutor
|
|||
/**
|
||||
* runs a process on the commandline
|
||||
*
|
||||
* @param string $command the command to execute
|
||||
* @param mixed $output the output will be written into this var if passed by ref
|
||||
* if a callable is passed it will be used as output handler
|
||||
* @param string $cwd the working directory
|
||||
* @return int statuscode
|
||||
* @param string $command the command to execute
|
||||
* @param mixed $output the output will be written into this var if passed by ref
|
||||
* if a callable is passed it will be used as output handler
|
||||
* @param ?string $cwd the working directory
|
||||
* @return int statuscode
|
||||
*/
|
||||
public function execute($command, &$output = null, $cwd = null)
|
||||
{
|
||||
|
@ -75,9 +80,9 @@ class ProcessExecutor
|
|||
/**
|
||||
* runs a process on the commandline in TTY mode
|
||||
*
|
||||
* @param string $command the command to execute
|
||||
* @param string $cwd the working directory
|
||||
* @return int statuscode
|
||||
* @param string $command the command to execute
|
||||
* @param ?string $cwd the working directory
|
||||
* @return int statuscode
|
||||
*/
|
||||
public function executeTty($command, $cwd = null)
|
||||
{
|
||||
|
@ -88,6 +93,13 @@ class ProcessExecutor
|
|||
return $this->doExecute($command, $cwd, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @param ?string $cwd
|
||||
* @param bool $tty
|
||||
* @param mixed $output
|
||||
* @return int
|
||||
*/
|
||||
private function doExecute($command, $cwd, $tty, &$output = null)
|
||||
{
|
||||
if ($this->io && $this->io->isDebug()) {
|
||||
|
@ -120,6 +132,7 @@ class ProcessExecutor
|
|||
if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
|
||||
$process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout());
|
||||
} else {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$process = new Process($command, $cwd, null, null, static::getTimeout());
|
||||
}
|
||||
if (!Platform::isWindows() && $tty) {
|
||||
|
@ -218,6 +231,10 @@ class ProcessExecutor
|
|||
return $promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
private function startJob($id)
|
||||
{
|
||||
$job = &$this->jobs[$id];
|
||||
|
@ -286,6 +303,10 @@ class ProcessExecutor
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?int $index job id
|
||||
* @return void
|
||||
*/
|
||||
public function wait($index = null)
|
||||
{
|
||||
while (true) {
|
||||
|
@ -299,6 +320,8 @@ class ProcessExecutor
|
|||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableAsync()
|
||||
{
|
||||
|
@ -308,7 +331,8 @@ class ProcessExecutor
|
|||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return int number of active (queued or started) jobs
|
||||
* @param ?int $index job id
|
||||
* @return int number of active (queued or started) jobs
|
||||
*/
|
||||
public function countActiveJobs($index = null)
|
||||
{
|
||||
|
@ -345,6 +369,8 @@ class ProcessExecutor
|
|||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function markJobDone()
|
||||
{
|
||||
|
@ -352,6 +378,7 @@ class ProcessExecutor
|
|||
}
|
||||
|
||||
/**
|
||||
* @param ?string $output
|
||||
* @return string[]
|
||||
*/
|
||||
public function splitLines($output)
|
||||
|
@ -373,6 +400,11 @@ class ProcessExecutor
|
|||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param Process::ERR|Process::OUT $type
|
||||
* @param string $buffer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function outputHandler($type, $buffer)
|
||||
{
|
||||
|
@ -402,7 +434,8 @@ class ProcessExecutor
|
|||
}
|
||||
|
||||
/**
|
||||
* @param int $timeout the timeout in seconds
|
||||
* @param int $timeout the timeout in seconds
|
||||
* @return void
|
||||
*/
|
||||
public static function setTimeout($timeout)
|
||||
{
|
||||
|
@ -466,6 +499,11 @@ class ProcessExecutor
|
|||
return "'".str_replace("'", "'\\''", $argument)."'";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $arg
|
||||
* @param string $char
|
||||
* @return bool
|
||||
*/
|
||||
private static function isSurroundedBy($arg, $char)
|
||||
{
|
||||
return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
|
||||
|
|
|
@ -28,25 +28,45 @@ use Composer\Util\Http\ProxyManager;
|
|||
*/
|
||||
class RemoteFilesystem
|
||||
{
|
||||
/** @var IOInterface */
|
||||
private $io;
|
||||
/** @var Config */
|
||||
private $config;
|
||||
/** @var string */
|
||||
private $scheme;
|
||||
/** @var int */
|
||||
private $bytesMax;
|
||||
/** @var string */
|
||||
private $originUrl;
|
||||
/** @var string */
|
||||
private $fileUrl;
|
||||
/** @var ?string */
|
||||
private $fileName;
|
||||
private $retry;
|
||||
/** @var bool */
|
||||
private $retry = false;
|
||||
/** @var bool */
|
||||
private $progress;
|
||||
/** @var ?int */
|
||||
private $lastProgress;
|
||||
/** @var mixed[] */
|
||||
private $options = array();
|
||||
/** @var array<string, array{cn: string, fp: string}> */
|
||||
private $peerCertificateMap = array();
|
||||
/** @var bool */
|
||||
private $disableTls = false;
|
||||
/** @var string[] */
|
||||
private $lastHeaders;
|
||||
private $storeAuth;
|
||||
/** @var bool */
|
||||
private $storeAuth = false;
|
||||
/** @var AuthHelper */
|
||||
private $authHelper;
|
||||
/** @var bool */
|
||||
private $degradedMode = false;
|
||||
/** @var int */
|
||||
private $redirects;
|
||||
/** @var int */
|
||||
private $maxRedirects = 20;
|
||||
/** @var ProxyManager */
|
||||
private $proxyManager;
|
||||
|
||||
/**
|
||||
|
@ -54,7 +74,7 @@ class RemoteFilesystem
|
|||
*
|
||||
* @param IOInterface $io The IO instance
|
||||
* @param Config $config The config
|
||||
* @param array $options The options
|
||||
* @param mixed[] $options The options
|
||||
* @param bool $disableTls
|
||||
* @param AuthHelper $authHelper
|
||||
*/
|
||||
|
@ -80,11 +100,11 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Copy the remote file in local.
|
||||
*
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param string $fileName the local filename
|
||||
* @param bool $progress Display the progression
|
||||
* @param array $options Additional context options
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param string $fileName the local filename
|
||||
* @param bool $progress Display the progression
|
||||
* @param mixed[] $options Additional context options
|
||||
*
|
||||
* @return bool true
|
||||
*/
|
||||
|
@ -96,10 +116,10 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Get the content.
|
||||
*
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param bool $progress Display the progression
|
||||
* @param array $options Additional context options
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param bool $progress Display the progression
|
||||
* @param mixed[] $options Additional context options
|
||||
*
|
||||
* @return bool|string The content
|
||||
*/
|
||||
|
@ -111,7 +131,7 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Retrieve the options set in the constructor
|
||||
*
|
||||
* @return array Options
|
||||
* @return mixed[] Options
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
|
@ -121,7 +141,8 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Merges new options
|
||||
*
|
||||
* @param array $options
|
||||
* @param mixed[] $options
|
||||
* @return void
|
||||
*/
|
||||
public function setOptions(array $options)
|
||||
{
|
||||
|
@ -141,7 +162,7 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Returns the headers of the last request
|
||||
*
|
||||
* @return array
|
||||
* @return string[]
|
||||
*/
|
||||
public function getLastHeaders()
|
||||
{
|
||||
|
@ -149,7 +170,7 @@ class RemoteFilesystem
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $headers array of returned headers like from getLastHeaders()
|
||||
* @param string[] $headers array of returned headers like from getLastHeaders()
|
||||
* @return int|null
|
||||
*/
|
||||
public static function findStatusCode(array $headers)
|
||||
|
@ -167,7 +188,7 @@ class RemoteFilesystem
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $headers array of returned headers like from getLastHeaders()
|
||||
* @param string[] $headers array of returned headers like from getLastHeaders()
|
||||
* @return string|null
|
||||
*/
|
||||
public function findStatusMessage(array $headers)
|
||||
|
@ -187,11 +208,11 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Get file content or copy action.
|
||||
*
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param array $additionalOptions context options
|
||||
* @param string $fileName the local filename
|
||||
* @param bool $progress Display the progression
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param mixed[] $additionalOptions context options
|
||||
* @param string $fileName the local filename
|
||||
* @param bool $progress Display the progression
|
||||
*
|
||||
* @throws TransportException|\Exception
|
||||
* @throws TransportException When the file could not be downloaded
|
||||
|
@ -260,7 +281,7 @@ class RemoteFilesystem
|
|||
unset($origFileUrl, $proxy, $usingProxy);
|
||||
|
||||
// Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
|
||||
if ((!preg_match('{^http://(repo\.)?packagist\.org/p/}', $fileUrl) || (false === strpos($fileUrl, '$') && false === strpos($fileUrl, '%24'))) && empty($degradedPackagist) && $this->config) {
|
||||
if ((!preg_match('{^http://(repo\.)?packagist\.org/p/}', $fileUrl) || (false === strpos($fileUrl, '$') && false === strpos($fileUrl, '%24'))) && empty($degradedPackagist)) {
|
||||
$this->config->prohibitUrlByConfig($fileUrl, $this->io);
|
||||
}
|
||||
|
||||
|
@ -375,7 +396,7 @@ class RemoteFilesystem
|
|||
|
||||
// check for gitlab 404 when downloading archives
|
||||
if ($statusCode === 404
|
||||
&& $this->config && in_array($originUrl, $this->config->get('gitlab-domains'), true)
|
||||
&& in_array($originUrl, $this->config->get('gitlab-domains'), true)
|
||||
&& false !== strpos($fileUrl, 'archive.zip')
|
||||
) {
|
||||
$result = false;
|
||||
|
@ -493,7 +514,7 @@ class RemoteFilesystem
|
|||
|
||||
$result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress);
|
||||
|
||||
if ($this->storeAuth && $this->config) {
|
||||
if ($this->storeAuth) {
|
||||
$this->authHelper->storeAuth($this->originUrl, $this->storeAuth);
|
||||
$this->storeAuth = false;
|
||||
}
|
||||
|
@ -596,7 +617,7 @@ class RemoteFilesystem
|
|||
|
||||
case STREAM_NOTIFY_PROGRESS:
|
||||
if ($this->bytesMax > 0 && $this->progress) {
|
||||
$progression = min(100, round($bytesTransferred / $this->bytesMax * 100));
|
||||
$progression = min(100, (int) round($bytesTransferred / $this->bytesMax * 100));
|
||||
|
||||
if ((0 === $progression % 5) && 100 !== $progression && $progression !== $this->lastProgress) {
|
||||
$this->lastProgress = $progression;
|
||||
|
@ -741,6 +762,8 @@ class RemoteFilesystem
|
|||
* Fetch certificate common name and fingerprint for validation of SAN.
|
||||
*
|
||||
* @todo Remove when PHP 5.6 is minimum supported version.
|
||||
*
|
||||
* @return ?array{cn: string, fp: string}
|
||||
*/
|
||||
private function getCertificateCnAndFp($url, $options)
|
||||
{
|
||||
|
@ -761,7 +784,7 @@ class RemoteFilesystem
|
|||
// Ideally this would just use stream_socket_client() to avoid sending a
|
||||
// HTTP request but that does not capture the certificate.
|
||||
if (false === $handle = @fopen($url, 'rb', false, $context)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Close non authenticated connection without reading any content.
|
||||
|
@ -780,6 +803,8 @@ class RemoteFilesystem
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getUrlAuthority($url)
|
||||
|
|
|
@ -44,6 +44,8 @@ class Silencer
|
|||
|
||||
/**
|
||||
* Restores a single state.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function restore()
|
||||
{
|
||||
|
|
|
@ -32,8 +32,8 @@ final class StreamContextFactory
|
|||
*
|
||||
* @param string $url URL the context is to be used for
|
||||
* @phpstan-param array{http?: array{follow_location?: int, max_redirects?: int, header?: string|array<string>}} $defaultOptions
|
||||
* @param array $defaultOptions Options to merge with the default
|
||||
* @param array $defaultParams Parameters to specify on the context
|
||||
* @param mixed[] $defaultOptions Options to merge with the default
|
||||
* @param mixed[] $defaultParams Parameters to specify on the context
|
||||
* @throws \RuntimeException if https proxy required and OpenSSL uninstalled
|
||||
* @return resource Default context
|
||||
*/
|
||||
|
@ -57,9 +57,9 @@ final class StreamContextFactory
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param bool $forCurl When true, will not add proxy values as these are handled separately
|
||||
* @param string $url
|
||||
* @param mixed[] $options
|
||||
* @param bool $forCurl When true, will not add proxy values as these are handled separately
|
||||
* @phpstan-return array{http: array{header: string[], proxy?: string, request_fulluri: bool}, ssl: array}
|
||||
* @return array formatted as a stream context array
|
||||
*/
|
||||
|
@ -130,9 +130,9 @@ final class StreamContextFactory
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @param mixed[] $options
|
||||
*
|
||||
* @return array
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function getTlsDefaults(array $options, LoggerInterface $logger = null)
|
||||
{
|
||||
|
@ -239,8 +239,8 @@ final class StreamContextFactory
|
|||
* This method fixes the array by moving the content-type header to the end
|
||||
*
|
||||
* @link https://bugs.php.net/bug.php?id=61548
|
||||
* @param string|array $header
|
||||
* @return array
|
||||
* @param string|string[] $header
|
||||
* @return string[]
|
||||
*/
|
||||
private static function fixHttpHeaderField($header)
|
||||
{
|
||||
|
|
|
@ -24,7 +24,7 @@ class Svn
|
|||
const MAX_QTY_AUTH_TRIES = 5;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @var ?array{username: string, password: string}
|
||||
*/
|
||||
protected $credentials;
|
||||
|
||||
|
@ -82,6 +82,9 @@ class Svn
|
|||
$this->process = $process ?: new ProcessExecutor($io);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function cleanEnv()
|
||||
{
|
||||
// clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940
|
||||
|
@ -127,6 +130,15 @@ class Svn
|
|||
return $this->executeWithAuthRetry($command, $cwd, '', $path, $verbose);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $svnCommand
|
||||
* @param string $cwd
|
||||
* @param string $url
|
||||
* @param string $path
|
||||
* @param bool $verbose
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
private function executeWithAuthRetry($svnCommand, $cwd, $url, $path, $verbose)
|
||||
{
|
||||
// Regenerate the command at each try, to use the newly user-provided credentials
|
||||
|
@ -136,10 +148,10 @@ class Svn
|
|||
$io = $this->io;
|
||||
$handler = function ($type, $buffer) use (&$output, $io, $verbose) {
|
||||
if ($type !== 'out') {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (strpos($buffer, 'Redirecting to URL ') === 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
$output .= $buffer;
|
||||
if ($verbose) {
|
||||
|
@ -178,7 +190,8 @@ class Svn
|
|||
}
|
||||
|
||||
/**
|
||||
* @param bool $cacheCredentials
|
||||
* @param bool $cacheCredentials
|
||||
* @return void
|
||||
*/
|
||||
public function setCacheCredentials($cacheCredentials)
|
||||
{
|
||||
|
|
|
@ -28,6 +28,8 @@ class SyncHelper
|
|||
* @param string $path the installation path for the package
|
||||
* @param PackageInterface $package the package to install
|
||||
* @param PackageInterface|null $prevPackage the previous package if this is an update and not an initial installation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function downloadAndInstallPackageSync(Loop $loop, DownloaderInterface $downloader, $path, PackageInterface $package, PackageInterface $prevPackage = null)
|
||||
{
|
||||
|
@ -56,6 +58,8 @@ class SyncHelper
|
|||
*
|
||||
* @param Loop $loop Loop instance which you can get from $composer->getLoop()
|
||||
* @param PromiseInterface|null $promise
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function await(Loop $loop, PromiseInterface $promise = null)
|
||||
{
|
||||
|
|
|
@ -57,7 +57,7 @@ final class TlsHelper
|
|||
*
|
||||
* @param mixed $certificate X.509 certificate
|
||||
*
|
||||
* @return array|null
|
||||
* @return array{cn: string, san: string[]}|null
|
||||
*/
|
||||
public static function getCertificateNames($certificate)
|
||||
{
|
||||
|
@ -130,6 +130,9 @@ final class TlsHelper
|
|||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* @param string $certificate
|
||||
* @return string
|
||||
*/
|
||||
public static function getCertificateFingerprint($certificate)
|
||||
{
|
||||
|
|
|
@ -103,6 +103,10 @@ class Url
|
|||
return $origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize($url)
|
||||
{
|
||||
// GitHub repository rename result in redirect locations containing the access_token as GET parameter
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @return ?\Composer\Autoload\ClassLoader
|
||||
*/
|
||||
function includeIfExists($file)
|
||||
{
|
||||
return file_exists($file) ? include $file : false;
|
||||
return file_exists($file) ? include $file : null;
|
||||
}
|
||||
|
||||
if ((!$loader = includeIfExists(__DIR__.'/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../autoload.php'))) {
|
||||
|
|
Loading…
Reference in New Issue