1
0
Fork 0

Add a bunch of type info to Util namespace

pull/10086/head
Jordi Boggiano 2021-08-29 20:07:50 +02:00
parent 8559279025
commit 024f0eda53
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC
20 changed files with 357 additions and 75 deletions

View File

@ -35,6 +35,10 @@ parameters:
# Mock errors # Mock errors
- '~^Call to an undefined method (PHPUnit\\Framework\\MockObject\\MockObject|Prophecy\\Prophecy\\ObjectProphecy)::.*$~' - '~^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: bootstrapFiles:
- ../tests/bootstrap.php - ../tests/bootstrap.php

View File

@ -75,6 +75,7 @@ class AuthHelper
* @param string[] $headers * @param string[] $headers
* @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be * @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 * 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()) public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $headers = array())
{ {

View File

@ -26,11 +26,16 @@ use React\Promise\Promise;
* @internal * @internal
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nicolas Grekas <p@tchwork.com> * @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 class CurlDownloader
{ {
/** @var ?resource */
private $multiHandle; private $multiHandle;
/** @var ?resource */
private $shareHandle; private $shareHandle;
/** @var Job[] */
private $jobs = array(); private $jobs = array();
/** @var IOInterface */ /** @var IOInterface */
private $io; private $io;
@ -38,11 +43,15 @@ class CurlDownloader
private $config; private $config;
/** @var AuthHelper */ /** @var AuthHelper */
private $authHelper; private $authHelper;
/** @var float */
private $selectTimeout = 5.0; private $selectTimeout = 5.0;
/** @var int */
private $maxRedirects = 20; private $maxRedirects = 20;
/** @var ProxyManager */ /** @var ProxyManager */
private $proxyManager; private $proxyManager;
/** @var bool */
private $supportsSecureProxy; private $supportsSecureProxy;
/** @var array<int, string[]> */
protected $multiErrors = array( protected $multiErrors = array(
CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), 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_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!'), 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( private static $options = array(
'http' => array( 'http' => array(
'method' => CURLOPT_CUSTOMREQUEST, 'method' => CURLOPT_CUSTOMREQUEST,
@ -68,6 +78,7 @@ class CurlDownloader
), ),
); );
/** @var array<string, true> */
private static $timeInfo = array( private static $timeInfo = array(
'total_time' => true, 'total_time' => true,
'namelookup_time' => true, 'namelookup_time' => true,
@ -77,6 +88,10 @@ class CurlDownloader
'redirect_time' => true, 'redirect_time' => true,
); );
/**
* @param mixed[] $options
* @param bool $disableTls
*/
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
{ {
$this->io = $io; $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 * @return int internal job id
*/ */
public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) 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 * @return int internal job id
*/ */
private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array())
@ -245,6 +276,10 @@ class CurlDownloader
return (int) $curlHandle; return (int) $curlHandle;
} }
/**
* @param int $id
* @return void
*/
public function abortRequest($id) public function abortRequest($id)
{ {
if (isset($this->jobs[$id], $this->jobs[$id]['handle'])) { if (isset($this->jobs[$id], $this->jobs[$id]['handle'])) {
@ -264,6 +299,9 @@ class CurlDownloader
} }
} }
/**
* @return void
*/
public function tick() public function tick()
{ {
if (!$this->jobs) { if (!$this->jobs) {
@ -409,6 +447,10 @@ class CurlDownloader
} }
} }
/**
* @param Job $job
* @return string
*/
private function handleRedirect(array $job, Response $response) private function handleRedirect(array $job, Response $response)
{ {
if ($locationHeader = $response->getHeader('location')) { 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().')'); 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) private function isAuthenticatedRetryNeeded(array $job, Response $response)
{ {
if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) {
@ -487,6 +533,14 @@ class CurlDownloader
return array('retry' => false, 'storeAuth' => false); 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()) private function restartJob(array $job, $url, array $attributes = array())
{ {
if ($job['filename']) { if ($job['filename']) {
@ -499,6 +553,11 @@ class CurlDownloader
$this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['options'], $job['filename'], $attributes); $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) private function failResponse(array $job, Response $response, $errorMessage)
{ {
if ($job['filename']) { if ($job['filename']) {
@ -513,6 +572,10 @@ class CurlDownloader
return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')' . $details, $response->getStatusCode()); 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) private function rejectJob(array $job, \Exception $e)
{ {
if (is_resource($job['headerHandle'])) { if (is_resource($job['headerHandle'])) {
@ -527,6 +590,10 @@ class CurlDownloader
call_user_func($job['reject'], $e); call_user_func($job['reject'], $e);
} }
/**
* @param int $code
* @return void
*/
private function checkCurlResult($code) private function checkCurlResult($code)
{ {
if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) { if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) {

View File

@ -12,10 +12,20 @@
namespace Composer\Util\Http; 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 class CurlResponse extends Response
{ {
/**
* @see https://www.php.net/curl_getinfo
* @var CurlInfo
*/
private $curlInfo; private $curlInfo;
/**
* @param CurlInfo $curlInfo
*/
public function __construct(array $request, $code, array $headers, $body, array $curlInfo) public function __construct(array $request, $code, array $headers, $body, array $curlInfo)
{ {
parent::__construct($request, $code, $headers, $body); parent::__construct($request, $code, $headers, $body);
@ -23,7 +33,7 @@ class CurlResponse extends Response
} }
/** /**
* @return array * @return CurlInfo
*/ */
public function getCurlInfo() public function getCurlInfo()
{ {

View File

@ -20,14 +20,18 @@ use Composer\Util\Url;
*/ */
class RequestProxy class RequestProxy
{ {
/** @var mixed[] */
private $contextOptions; private $contextOptions;
/** @var bool */
private $isSecure; private $isSecure;
/** @var string */
private $formattedUrl; private $formattedUrl;
/** @var string */
private $url; private $url;
/** /**
* @param string $url * @param string $url
* @param array $contextOptions * @param mixed[] $contextOptions
* @param string $formattedUrl * @param string $formattedUrl
*/ */
public function __construct($url, array $contextOptions, $formattedUrl) public function __construct($url, array $contextOptions, $formattedUrl)
@ -41,7 +45,7 @@ class RequestProxy
/** /**
* Returns an array of context options * Returns an array of context options
* *
* @return array * @return mixed[]
*/ */
public function getContextOptions() public function getContextOptions()
{ {

View File

@ -13,14 +13,28 @@
namespace Composer\Util\Http; namespace Composer\Util\Http;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Util\HttpDownloader;
/**
* @phpstan-import-type Request from HttpDownloader
*/
class Response class Response
{ {
/** @var Request */
private $request; private $request;
/** @var int */
private $code; private $code;
/** @var string[] */
private $headers; private $headers;
/** @var ?string */
private $body; private $body;
/**
* @param Request $request
* @param int $code
* @param string[] $headers
* @param ?string $body
*/
public function __construct(array $request, $code, array $headers, $body) public function __construct(array $request, $code, array $headers, $body)
{ {
if (!isset($request['url'])) { if (!isset($request['url'])) {
@ -32,6 +46,9 @@ class Response
$this->body = $body; $this->body = $body;
} }
/**
* @return int
*/
public function getStatusCode() public function getStatusCode()
{ {
return $this->code; return $this->code;
@ -54,33 +71,51 @@ class Response
return $value; return $value;
} }
/**
* @return string[]
*/
public function getHeaders() public function getHeaders()
{ {
return $this->headers; return $this->headers;
} }
/**
* @param string $name
* @return ?string
*/
public function getHeader($name) public function getHeader($name)
{ {
return self::findHeaderValue($this->headers, $name); return self::findHeaderValue($this->headers, $name);
} }
/**
* @return ?string
*/
public function getBody() public function getBody()
{ {
return $this->body; return $this->body;
} }
/**
* @return mixed
*/
public function decodeJson() public function decodeJson()
{ {
return JsonFile::parseJson($this->body, $this->request['url']); return JsonFile::parseJson($this->body, $this->request['url']);
} }
/**
* @return void
* @phpstan-impure
*/
public function collect() public function collect()
{ {
/** @phpstan-ignore-next-line */
$this->request = $this->code = $this->headers = $this->body = null; $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) * @param string $name header name (case insensitive)
* @return string|null * @return string|null
*/ */

View File

@ -16,14 +16,18 @@ use Composer\Config;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\Util\Http\Response; use Composer\Util\Http\Response;
use Composer\Util\Http\CurlDownloader;
use Composer\Composer; use Composer\Composer;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Exception\IrrecoverableDownloadException; use Composer\Exception\IrrecoverableDownloadException;
use React\Promise\Promise; use React\Promise\Promise;
use React\Promise\PromiseInterface;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @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 class HttpDownloader
{ {
@ -33,22 +37,33 @@ class HttpDownloader
const STATUS_FAILED = 4; const STATUS_FAILED = 4;
const STATUS_ABORTED = 5; const STATUS_ABORTED = 5;
/** @var IOInterface */
private $io; private $io;
/** @var Config */
private $config; private $config;
/** @var array<Job> */
private $jobs = array(); private $jobs = array();
/** @var mixed[] */
private $options = array(); private $options = array();
/** @var int */
private $runningJobs = 0; private $runningJobs = 0;
/** @var int */
private $maxJobs = 12; private $maxJobs = 12;
/** @var ?CurlDownloader */
private $curl; private $curl;
/** @var ?RemoteFilesystem */
private $rfs; private $rfs;
/** @var int */
private $idGen = 0; private $idGen = 0;
/** @var bool */
private $disabled; private $disabled;
/** @var bool */
private $allowAsync = false; private $allowAsync = false;
/** /**
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The config * @param Config $config The config
* @param array $options The options * @param mixed[] $options The options
* @param bool $disableTls * @param bool $disableTls
*/ */
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
@ -68,7 +83,7 @@ class HttpDownloader
$this->config = $config; $this->config = $config;
if (self::isCurlEnabled()) { 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); $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
@ -82,14 +97,14 @@ class HttpDownloader
* Download a file synchronously * Download a file synchronously
* *
* @param string $url URL to download * @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 * although not all options are supported when using the default curl downloader
* @throws TransportException * @throws TransportException
* @return Response * @return Response
*/ */
public function get($url, $options = array()) 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']); $this->wait($job['id']);
$response = $this->getResponse($job['id']); $response = $this->getResponse($job['id']);
@ -106,7 +121,7 @@ class HttpDownloader
$this->curl = null; $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']); $this->wait($job['id']);
$response = $this->getResponse($job['id']); $response = $this->getResponse($job['id']);
@ -119,14 +134,14 @@ class HttpDownloader
* Create an async download operation * Create an async download operation
* *
* @param string $url URL to download * @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 * although not all options are supported when using the default curl downloader
* @throws TransportException * @throws TransportException
* @return Promise * @return PromiseInterface
*/ */
public function add($url, $options = array()) 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; return $promise;
} }
@ -136,7 +151,7 @@ class HttpDownloader
* *
* @param string $url URL to download * @param string $url URL to download
* @param string $to Path to copy to * @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 * although not all options are supported when using the default curl downloader
* @throws TransportException * @throws TransportException
* @return Response * @return Response
@ -154,10 +169,10 @@ class HttpDownloader
* *
* @param string $url URL to download * @param string $url URL to download
* @param string $to Path to copy to * @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 * although not all options are supported when using the default curl downloader
* @throws TransportException * @throws TransportException
* @return Promise * @return PromiseInterface
*/ */
public function addCopy($url, $to, $options = array()) public function addCopy($url, $to, $options = array())
{ {
@ -169,7 +184,7 @@ class HttpDownloader
/** /**
* Retrieve the options set in the constructor * Retrieve the options set in the constructor
* *
* @return array Options * @return mixed[] Options
*/ */
public function getOptions() public function getOptions()
{ {
@ -179,6 +194,7 @@ class HttpDownloader
/** /**
* Merges new options * Merges new options
* *
* @param mixed[] $options
* @return void * @return void
*/ */
public function setOptions(array $options) public function setOptions(array $options)
@ -186,10 +202,17 @@ class HttpDownloader
$this->options = array_replace_recursive($this->options, $options); $this->options = array_replace_recursive($this->options, $options);
} }
/**
* @param Request $request
* @param bool $sync
*
* @return array{Job, PromiseInterface}
*/
private function addJob($request, $sync = false) private function addJob($request, $sync = false)
{ {
$request['options'] = array_replace_recursive($this->options, $request['options']); $request['options'] = array_replace_recursive($this->options, $request['options']);
/** @var Job */
$job = array( $job = array(
'id' => $this->idGen++, 'id' => $this->idGen++,
'status' => self::STATUS_QUEUED, 'status' => self::STATUS_QUEUED,
@ -283,6 +306,10 @@ class HttpDownloader
return array($job, $promise); return array($job, $promise);
} }
/**
* @param int $id
* @return void
*/
private function startJob($id) private function startJob($id)
{ {
$job = &$this->jobs[$id]; $job = &$this->jobs[$id];
@ -325,6 +352,7 @@ class HttpDownloader
/** /**
* @private * @private
* @return void
*/ */
public function markJobDone() public function markJobDone()
{ {
@ -335,6 +363,8 @@ class HttpDownloader
* Wait for current async download jobs to complete * Wait for current async download jobs to complete
* *
* @param int|null $index For internal use only, the job id * @param int|null $index For internal use only, the job id
*
* @return void
*/ */
public function wait($index = null) public function wait($index = null)
{ {
@ -345,6 +375,8 @@ class HttpDownloader
/** /**
* @internal * @internal
*
* @return void
*/ */
public function enableAsync() public function enableAsync()
{ {
@ -387,6 +419,10 @@ class HttpDownloader
return $active; return $active;
} }
/**
* @param int $index Job id
* @return Response
*/
private function getResponse($index) private function getResponse($index)
{ {
if (!isset($this->jobs[$index])) { if (!isset($this->jobs[$index])) {
@ -410,6 +446,10 @@ class HttpDownloader
/** /**
* @internal * @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) public static function outputWarnings(IOInterface $io, $url, $data)
{ {
@ -433,11 +473,13 @@ class HttpDownloader
/** /**
* @internal * @internal
*
* @return ?string[]
*/ */
public static function getExceptionHints(\Exception $e) public static function getExceptionHints(\Exception $e)
{ {
if (!$e instanceof TransportException) { if (!$e instanceof TransportException) {
return; return null;
} }
if ( if (
@ -460,8 +502,14 @@ class HttpDownloader
'<error>The following exception probably indicates you are offline or have misconfigured DNS resolver(s)</error>', '<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) private function canUseCurl(array $job)
{ {
if (!$this->curl) { if (!$this->curl) {

View File

@ -29,7 +29,7 @@ class IniHelper
* The equivalent of calling php_ini_loaded_file then php_ini_scanned_files. * 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. * The loaded ini location is the first entry and may be empty.
* *
* @return array * @return string[]
*/ */
public static function getAll() public static function getAll()
{ {

View File

@ -12,8 +12,9 @@
namespace Composer\Util; namespace Composer\Util;
use React\Promise\Promise; use React\Promise\CancellablePromiseInterface;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;
use React\Promise\PromiseInterface;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
@ -24,7 +25,7 @@ class Loop
private $httpDownloader; private $httpDownloader;
/** @var ProcessExecutor|null */ /** @var ProcessExecutor|null */
private $processExecutor; private $processExecutor;
/** @var Promise[][] */ /** @var PromiseInterface[][] */
private $currentPromises = array(); private $currentPromises = array();
/** @var int */ /** @var int */
private $waitIndex = 0; private $waitIndex = 0;
@ -56,6 +57,11 @@ class Loop
return $this->processExecutor; return $this->processExecutor;
} }
/**
* @param PromiseInterface[] $promises
* @param ?ProgressBar $progress
* @return void
*/
public function wait(array $promises, ProgressBar $progress = null) public function wait(array $promises, ProgressBar $progress = null)
{ {
/** @var \Exception|null */ /** @var \Exception|null */
@ -113,12 +119,17 @@ class Loop
} }
} }
/**
* @return void
*/
public function abortJobs() public function abortJobs()
{ {
foreach ($this->currentPromises as $promiseGroup) { foreach ($this->currentPromises as $promiseGroup) {
foreach ($promiseGroup as $promise) { foreach ($promiseGroup as $promise) {
if ($promise instanceof CancellablePromiseInterface) {
$promise->cancel(); $promise->cancel();
} }
} }
} }
}
} }

View File

@ -420,6 +420,8 @@ class NoProxyPattern
* @param string $int * @param string $int
* @param int $min * @param int $min
* @param int $max * @param int $max
*
* @return bool
*/ */
private function validateInt($int, $min, $max) private function validateInt($int, $min, $max)
{ {

View File

@ -142,6 +142,10 @@ class Platform
return \strlen($str); return \strlen($str);
} }
/**
* @param ?resource $fd Open file descriptor or null to default to STDOUT
* @return bool
*/
public static function isTty($fd = null) public static function isTty($fd = null)
{ {
if ($fd === null) { if ($fd === null) {
@ -170,6 +174,9 @@ class Platform
return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
} }
/**
* @return void
*/
public static function workaroundFilesystemIssues() public static function workaroundFilesystemIssues()
{ {
if (self::isVirtualBoxGuest()) { if (self::isVirtualBoxGuest()) {

View File

@ -31,6 +31,7 @@ class ProcessExecutor
const STATUS_FAILED = 4; const STATUS_FAILED = 4;
const STATUS_ABORTED = 5; const STATUS_ABORTED = 5;
/** @var int */
protected static $timeout = 300; protected static $timeout = 300;
/** @var bool */ /** @var bool */
@ -44,9 +45,13 @@ class ProcessExecutor
* @phpstan-var array<int, array<string, mixed>> * @phpstan-var array<int, array<string, mixed>>
*/ */
private $jobs = array(); private $jobs = array();
/** @var int */
private $runningJobs = 0; private $runningJobs = 0;
/** @var int */
private $maxJobs = 10; private $maxJobs = 10;
/** @var int */
private $idGen = 0; private $idGen = 0;
/** @var bool */
private $allowAsync = false; private $allowAsync = false;
public function __construct(IOInterface $io = null) public function __construct(IOInterface $io = null)
@ -60,7 +65,7 @@ class ProcessExecutor
* @param string $command the command to execute * @param string $command the command to execute
* @param mixed $output the output will be written into this var if passed by ref * @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 * if a callable is passed it will be used as output handler
* @param string $cwd the working directory * @param ?string $cwd the working directory
* @return int statuscode * @return int statuscode
*/ */
public function execute($command, &$output = null, $cwd = null) public function execute($command, &$output = null, $cwd = null)
@ -76,7 +81,7 @@ class ProcessExecutor
* runs a process on the commandline in TTY mode * runs a process on the commandline in TTY mode
* *
* @param string $command the command to execute * @param string $command the command to execute
* @param string $cwd the working directory * @param ?string $cwd the working directory
* @return int statuscode * @return int statuscode
*/ */
public function executeTty($command, $cwd = null) public function executeTty($command, $cwd = null)
@ -88,6 +93,13 @@ class ProcessExecutor
return $this->doExecute($command, $cwd, false); 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) private function doExecute($command, $cwd, $tty, &$output = null)
{ {
if ($this->io && $this->io->isDebug()) { if ($this->io && $this->io->isDebug()) {
@ -120,6 +132,7 @@ class ProcessExecutor
if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
$process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout());
} else { } else {
/** @phpstan-ignore-next-line */
$process = new Process($command, $cwd, null, null, static::getTimeout()); $process = new Process($command, $cwd, null, null, static::getTimeout());
} }
if (!Platform::isWindows() && $tty) { if (!Platform::isWindows() && $tty) {
@ -218,6 +231,10 @@ class ProcessExecutor
return $promise; return $promise;
} }
/**
* @param int $id
* @return void
*/
private function startJob($id) private function startJob($id)
{ {
$job = &$this->jobs[$id]; $job = &$this->jobs[$id];
@ -286,6 +303,10 @@ class ProcessExecutor
} }
} }
/**
* @param ?int $index job id
* @return void
*/
public function wait($index = null) public function wait($index = null)
{ {
while (true) { while (true) {
@ -299,6 +320,8 @@ class ProcessExecutor
/** /**
* @internal * @internal
*
* @return void
*/ */
public function enableAsync() public function enableAsync()
{ {
@ -308,6 +331,7 @@ class ProcessExecutor
/** /**
* @internal * @internal
* *
* @param ?int $index job id
* @return int number of active (queued or started) jobs * @return int number of active (queued or started) jobs
*/ */
public function countActiveJobs($index = null) public function countActiveJobs($index = null)
@ -345,6 +369,8 @@ class ProcessExecutor
/** /**
* @private * @private
*
* @return void
*/ */
public function markJobDone() public function markJobDone()
{ {
@ -352,6 +378,7 @@ class ProcessExecutor
} }
/** /**
* @param ?string $output
* @return string[] * @return string[]
*/ */
public function splitLines($output) public function splitLines($output)
@ -373,6 +400,11 @@ class ProcessExecutor
/** /**
* @private * @private
*
* @param Process::ERR|Process::OUT $type
* @param string $buffer
*
* @return void
*/ */
public function outputHandler($type, $buffer) public function outputHandler($type, $buffer)
{ {
@ -403,6 +435,7 @@ class ProcessExecutor
/** /**
* @param int $timeout the timeout in seconds * @param int $timeout the timeout in seconds
* @return void
*/ */
public static function setTimeout($timeout) public static function setTimeout($timeout)
{ {
@ -466,6 +499,11 @@ class ProcessExecutor
return "'".str_replace("'", "'\\''", $argument)."'"; return "'".str_replace("'", "'\\''", $argument)."'";
} }
/**
* @param string $arg
* @param string $char
* @return bool
*/
private static function isSurroundedBy($arg, $char) private static function isSurroundedBy($arg, $char)
{ {
return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];

View File

@ -28,25 +28,45 @@ use Composer\Util\Http\ProxyManager;
*/ */
class RemoteFilesystem class RemoteFilesystem
{ {
/** @var IOInterface */
private $io; private $io;
/** @var Config */
private $config; private $config;
/** @var string */
private $scheme; private $scheme;
/** @var int */
private $bytesMax; private $bytesMax;
/** @var string */
private $originUrl; private $originUrl;
/** @var string */
private $fileUrl; private $fileUrl;
/** @var ?string */
private $fileName; private $fileName;
private $retry; /** @var bool */
private $retry = false;
/** @var bool */
private $progress; private $progress;
/** @var ?int */
private $lastProgress; private $lastProgress;
/** @var mixed[] */
private $options = array(); private $options = array();
/** @var array<string, array{cn: string, fp: string}> */
private $peerCertificateMap = array(); private $peerCertificateMap = array();
/** @var bool */
private $disableTls = false; private $disableTls = false;
/** @var string[] */
private $lastHeaders; private $lastHeaders;
private $storeAuth; /** @var bool */
private $storeAuth = false;
/** @var AuthHelper */
private $authHelper; private $authHelper;
/** @var bool */
private $degradedMode = false; private $degradedMode = false;
/** @var int */
private $redirects; private $redirects;
/** @var int */
private $maxRedirects = 20; private $maxRedirects = 20;
/** @var ProxyManager */
private $proxyManager; private $proxyManager;
/** /**
@ -54,7 +74,7 @@ class RemoteFilesystem
* *
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The config * @param Config $config The config
* @param array $options The options * @param mixed[] $options The options
* @param bool $disableTls * @param bool $disableTls
* @param AuthHelper $authHelper * @param AuthHelper $authHelper
*/ */
@ -84,7 +104,7 @@ class RemoteFilesystem
* @param string $fileUrl The file URL * @param string $fileUrl The file URL
* @param string $fileName the local filename * @param string $fileName the local filename
* @param bool $progress Display the progression * @param bool $progress Display the progression
* @param array $options Additional context options * @param mixed[] $options Additional context options
* *
* @return bool true * @return bool true
*/ */
@ -99,7 +119,7 @@ class RemoteFilesystem
* @param string $originUrl The origin URL * @param string $originUrl The origin URL
* @param string $fileUrl The file URL * @param string $fileUrl The file URL
* @param bool $progress Display the progression * @param bool $progress Display the progression
* @param array $options Additional context options * @param mixed[] $options Additional context options
* *
* @return bool|string The content * @return bool|string The content
*/ */
@ -111,7 +131,7 @@ class RemoteFilesystem
/** /**
* Retrieve the options set in the constructor * Retrieve the options set in the constructor
* *
* @return array Options * @return mixed[] Options
*/ */
public function getOptions() public function getOptions()
{ {
@ -121,7 +141,8 @@ class RemoteFilesystem
/** /**
* Merges new options * Merges new options
* *
* @param array $options * @param mixed[] $options
* @return void
*/ */
public function setOptions(array $options) public function setOptions(array $options)
{ {
@ -141,7 +162,7 @@ class RemoteFilesystem
/** /**
* Returns the headers of the last request * Returns the headers of the last request
* *
* @return array * @return string[]
*/ */
public function getLastHeaders() 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 * @return int|null
*/ */
public static function findStatusCode(array $headers) 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 * @return string|null
*/ */
public function findStatusMessage(array $headers) public function findStatusMessage(array $headers)
@ -189,7 +210,7 @@ class RemoteFilesystem
* *
* @param string $originUrl The origin URL * @param string $originUrl The origin URL
* @param string $fileUrl The file URL * @param string $fileUrl The file URL
* @param array $additionalOptions context options * @param mixed[] $additionalOptions context options
* @param string $fileName the local filename * @param string $fileName the local filename
* @param bool $progress Display the progression * @param bool $progress Display the progression
* *
@ -260,7 +281,7 @@ class RemoteFilesystem
unset($origFileUrl, $proxy, $usingProxy); unset($origFileUrl, $proxy, $usingProxy);
// Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 // 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); $this->config->prohibitUrlByConfig($fileUrl, $this->io);
} }
@ -375,7 +396,7 @@ class RemoteFilesystem
// check for gitlab 404 when downloading archives // check for gitlab 404 when downloading archives
if ($statusCode === 404 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') && false !== strpos($fileUrl, 'archive.zip')
) { ) {
$result = false; $result = false;
@ -493,7 +514,7 @@ class RemoteFilesystem
$result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); $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->authHelper->storeAuth($this->originUrl, $this->storeAuth);
$this->storeAuth = false; $this->storeAuth = false;
} }
@ -596,7 +617,7 @@ class RemoteFilesystem
case STREAM_NOTIFY_PROGRESS: case STREAM_NOTIFY_PROGRESS:
if ($this->bytesMax > 0 && $this->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) { if ((0 === $progression % 5) && 100 !== $progression && $progression !== $this->lastProgress) {
$this->lastProgress = $progression; $this->lastProgress = $progression;
@ -741,6 +762,8 @@ class RemoteFilesystem
* Fetch certificate common name and fingerprint for validation of SAN. * Fetch certificate common name and fingerprint for validation of SAN.
* *
* @todo Remove when PHP 5.6 is minimum supported version. * @todo Remove when PHP 5.6 is minimum supported version.
*
* @return ?array{cn: string, fp: string}
*/ */
private function getCertificateCnAndFp($url, $options) private function getCertificateCnAndFp($url, $options)
{ {
@ -761,7 +784,7 @@ class RemoteFilesystem
// Ideally this would just use stream_socket_client() to avoid sending a // Ideally this would just use stream_socket_client() to avoid sending a
// HTTP request but that does not capture the certificate. // HTTP request but that does not capture the certificate.
if (false === $handle = @fopen($url, 'rb', false, $context)) { if (false === $handle = @fopen($url, 'rb', false, $context)) {
return; return null;
} }
// Close non authenticated connection without reading any content. // Close non authenticated connection without reading any content.
@ -780,6 +803,8 @@ class RemoteFilesystem
); );
} }
} }
return null;
} }
private function getUrlAuthority($url) private function getUrlAuthority($url)

View File

@ -44,6 +44,8 @@ class Silencer
/** /**
* Restores a single state. * Restores a single state.
*
* @return void
*/ */
public static function restore() public static function restore()
{ {

View File

@ -32,8 +32,8 @@ final class StreamContextFactory
* *
* @param string $url URL the context is to be used for * @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 * @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 mixed[] $defaultOptions Options to merge with the default
* @param array $defaultParams Parameters to specify on the context * @param mixed[] $defaultParams Parameters to specify on the context
* @throws \RuntimeException if https proxy required and OpenSSL uninstalled * @throws \RuntimeException if https proxy required and OpenSSL uninstalled
* @return resource Default context * @return resource Default context
*/ */
@ -58,7 +58,7 @@ final class StreamContextFactory
/** /**
* @param string $url * @param string $url
* @param array $options * @param mixed[] $options
* @param bool $forCurl When true, will not add proxy values as these are handled separately * @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} * @phpstan-return array{http: array{header: string[], proxy?: string, request_fulluri: bool}, ssl: array}
* @return array formatted as a stream context 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) 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 * This method fixes the array by moving the content-type header to the end
* *
* @link https://bugs.php.net/bug.php?id=61548 * @link https://bugs.php.net/bug.php?id=61548
* @param string|array $header * @param string|string[] $header
* @return array * @return string[]
*/ */
private static function fixHttpHeaderField($header) private static function fixHttpHeaderField($header)
{ {

View File

@ -24,7 +24,7 @@ class Svn
const MAX_QTY_AUTH_TRIES = 5; const MAX_QTY_AUTH_TRIES = 5;
/** /**
* @var array * @var ?array{username: string, password: string}
*/ */
protected $credentials; protected $credentials;
@ -82,6 +82,9 @@ class Svn
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
} }
/**
* @return void
*/
public static function cleanEnv() public static function cleanEnv()
{ {
// clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940 // 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); 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) private function executeWithAuthRetry($svnCommand, $cwd, $url, $path, $verbose)
{ {
// Regenerate the command at each try, to use the newly user-provided credentials // Regenerate the command at each try, to use the newly user-provided credentials
@ -136,10 +148,10 @@ class Svn
$io = $this->io; $io = $this->io;
$handler = function ($type, $buffer) use (&$output, $io, $verbose) { $handler = function ($type, $buffer) use (&$output, $io, $verbose) {
if ($type !== 'out') { if ($type !== 'out') {
return; return null;
} }
if (strpos($buffer, 'Redirecting to URL ') === 0) { if (strpos($buffer, 'Redirecting to URL ') === 0) {
return; return null;
} }
$output .= $buffer; $output .= $buffer;
if ($verbose) { if ($verbose) {
@ -179,6 +191,7 @@ class Svn
/** /**
* @param bool $cacheCredentials * @param bool $cacheCredentials
* @return void
*/ */
public function setCacheCredentials($cacheCredentials) public function setCacheCredentials($cacheCredentials)
{ {

View File

@ -28,6 +28,8 @@ class SyncHelper
* @param string $path the installation path for the package * @param string $path the installation path for the package
* @param PackageInterface $package the package to install * @param PackageInterface $package the package to install
* @param PackageInterface|null $prevPackage the previous package if this is an update and not an initial installation * @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) 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 Loop $loop Loop instance which you can get from $composer->getLoop()
* @param PromiseInterface|null $promise * @param PromiseInterface|null $promise
*
* @return void
*/ */
public static function await(Loop $loop, PromiseInterface $promise = null) public static function await(Loop $loop, PromiseInterface $promise = null)
{ {

View File

@ -57,7 +57,7 @@ final class TlsHelper
* *
* @param mixed $certificate X.509 certificate * @param mixed $certificate X.509 certificate
* *
* @return array|null * @return array{cn: string, san: string[]}|null
*/ */
public static function getCertificateNames($certificate) public static function getCertificateNames($certificate)
{ {
@ -130,6 +130,9 @@ final class TlsHelper
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 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 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @param string $certificate
* @return string
*/ */
public static function getCertificateFingerprint($certificate) public static function getCertificateFingerprint($certificate)
{ {

View File

@ -103,6 +103,10 @@ class Url
return $origin; return $origin;
} }
/**
* @param string $url
* @return string
*/
public static function sanitize($url) public static function sanitize($url)
{ {
// GitHub repository rename result in redirect locations containing the access_token as GET parameter // GitHub repository rename result in redirect locations containing the access_token as GET parameter

View File

@ -10,9 +10,13 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
/**
* @param string $file
* @return ?\Composer\Autoload\ClassLoader
*/
function includeIfExists($file) 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'))) { if ((!$loader = includeIfExists(__DIR__.'/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../autoload.php'))) {