Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. CurlDownloader.php

function CurlDownloader::tick

File

vendor/composer/composer/src/Composer/Util/Http/CurlDownloader.php, line 297

Class

CurlDownloader
@internal @author Jordi Boggiano <j.boggiano@seld.be> @author Nicolas Grekas <p@tchwork.com> @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth:…

Namespace

Composer\Util\Http

Code

public function tick() : void {
    static $timeoutWarning = false;
    if (count($this->jobs) === 0) {
        return;
    }
    $active = true;
    $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)) {
        $curlHandle = $progress['handle'];
        $result = $progress['result'];
        $i = (int) $curlHandle;
        if (!isset($this->jobs[$i])) {
            continue;
        }
        $progress = curl_getinfo($curlHandle);
        if (false === $progress) {
            throw new \RuntimeException('Failed getting info from curl handle ' . $i . ' (' . $this->jobs[$i]['url'] . ')');
        }
        $job = $this->jobs[$i];
        unset($this->jobs[$i]);
        $error = curl_error($curlHandle);
        $errno = curl_errno($curlHandle);
        curl_multi_remove_handle($this->multiHandle, $curlHandle);
        curl_close($curlHandle);
        $headers = null;
        $statusCode = null;
        $response = null;
        try {
            // TODO progress
            if (CURLE_OK !== $errno || $error || $result !== CURLE_OK) {
                $errno = $errno ?: $result;
                if (!$error && function_exists('curl_strerror')) {
                    $error = curl_strerror($errno);
                }
                $progress['error_code'] = $errno;
                if ((!isset($job['options']['http']['method']) || $job['options']['http']['method'] === 'GET') && (in_array($errno, [
                    7,
                    16,
                    92,
                    6,
                ], true) || in_array($errno, [
                    56,
                    35,
                ], true) && str_contains((string) $error, 'Connection reset by peer')) && $job['attributes']['retries'] < $this->maxRetries) {
                    $attributes = [
                        'retries' => $job['attributes']['retries'] + 1,
                    ];
                    if ($errno === 7 && !isset($job['attributes']['ipResolve'])) {
                        // CURLE_COULDNT_CONNECT, retry forcing IPv4 if no IP stack was selected
                        $attributes['ipResolve'] = 4;
                    }
                    $this->io
                        ->writeError('Retrying (' . ($job['attributes']['retries'] + 1) . ') ' . Url::sanitize($job['url']) . ' due to curl error ' . $errno, true, IOInterface::DEBUG);
                    $this->restartJobWithDelay($job, $job['url'], $attributes);
                    continue;
                }
                // TODO: Remove this as soon as https://github.com/curl/curl/issues/10591 is resolved
                if ($errno === 55) {
                    $this->io
                        ->writeError('Retrying (' . ($job['attributes']['retries'] + 1) . ') ' . Url::sanitize($job['url']) . ' due to curl error ' . $errno, true, IOInterface::DEBUG);
                    $this->restartJobWithDelay($job, $job['url'], [
                        'retries' => $job['attributes']['retries'] + 1,
                    ]);
                    continue;
                }
                if ($errno === 28 && \PHP_VERSION_ID >= 70300 && $progress['namelookup_time'] === 0.0 && !$timeoutWarning) {
                    $timeoutWarning = true;
                    $this->io
                        ->writeError('<warning>A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the command again prefixed with COMPOSER_DISABLE_NETWORK=1 to make Composer run in offline mode.</warning>');
                }
                throw new TransportException('curl error ' . $errno . ' while downloading ' . Url::sanitize($progress['url']) . ': ' . $error);
            }
            $statusCode = $progress['http_code'];
            rewind($job['headerHandle']);
            $headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle'])));
            fclose($job['headerHandle']);
            if ($statusCode === 0) {
                throw new \LogicException('Received unexpected http status code 0 without error for ' . Url::sanitize($progress['url']) . ': headers ' . var_export($headers, true) . ' curl info ' . var_export($progress, true));
            }
            // prepare response object
            if (null !== $job['filename']) {
                $contents = $job['filename'] . '~';
                if ($statusCode >= 300) {
                    rewind($job['bodyHandle']);
                    $contents = stream_get_contents($job['bodyHandle']);
                }
                $response = new CurlResponse([
                    'url' => $job['url'],
                ], $statusCode, $headers, $contents, $progress);
                $this->io
                    ->writeError('[' . $statusCode . '] ' . Url::sanitize($job['url']), true, IOInterface::DEBUG);
            }
            else {
                $maxFileSize = $job['options']['max_file_size'] ?? null;
                rewind($job['bodyHandle']);
                if ($maxFileSize !== null) {
                    $contents = stream_get_contents($job['bodyHandle'], $maxFileSize);
                    // Gzipped responses with missing Content-Length header cannot be detected during the file download
                    // because $progress['size_download'] refers to the gzipped size downloaded, not the actual file size
                    if ($contents !== false && Platform::strlen($contents) >= $maxFileSize) {
                        throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($contents) . ' of allowed ' . $maxFileSize . ' bytes');
                    }
                }
                else {
                    $contents = stream_get_contents($job['bodyHandle']);
                }
                $response = new CurlResponse([
                    'url' => $job['url'],
                ], $statusCode, $headers, $contents, $progress);
                $this->io
                    ->writeError('[' . $statusCode . '] ' . Url::sanitize($job['url']), true, IOInterface::DEBUG);
            }
            fclose($job['bodyHandle']);
            if ($response->getStatusCode() >= 400 && $response->getHeader('content-type') === 'application/json') {
                HttpDownloader::outputWarnings($this->io, $job['origin'], json_decode($response->getBody(), true));
            }
            $result = $this->isAuthenticatedRetryNeeded($job, $response);
            if ($result['retry']) {
                $this->restartJob($job, $job['url'], [
                    'storeAuth' => $result['storeAuth'],
                    'retries' => $job['attributes']['retries'] + 1,
                ]);
                continue;
            }
            // handle 3xx redirects, 304 Not Modified is excluded
            if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) {
                $location = $this->handleRedirect($job, $response);
                if ($location) {
                    $this->restartJob($job, $location, [
                        'redirects' => $job['attributes']['redirects'] + 1,
                    ]);
                    continue;
                }
            }
            // fail 4xx and 5xx responses and capture the response
            if ($statusCode >= 400 && $statusCode <= 599) {
                if ((!isset($job['options']['http']['method']) || $job['options']['http']['method'] === 'GET') && in_array($statusCode, [
                    423,
                    425,
                    500,
                    502,
                    503,
                    504,
                    507,
                    510,
                ], true) && $job['attributes']['retries'] < $this->maxRetries) {
                    $this->io
                        ->writeError('Retrying (' . ($job['attributes']['retries'] + 1) . ') ' . Url::sanitize($job['url']) . ' due to status code ' . $statusCode, true, IOInterface::DEBUG);
                    $this->restartJobWithDelay($job, $job['url'], [
                        'retries' => $job['attributes']['retries'] + 1,
                    ]);
                    continue;
                }
                throw $this->failResponse($job, $response, $response->getStatusMessage());
            }
            if ($job['attributes']['storeAuth'] !== false) {
                $this->authHelper
                    ->storeAuth($job['origin'], $job['attributes']['storeAuth']);
            }
            // resolve promise
            if (null !== $job['filename']) {
                rename($job['filename'] . '~', $job['filename']);
                $job['resolve']($response);
            }
            else {
                $job['resolve']($response);
            }
        } catch (\Exception $e) {
            if ($e instanceof TransportException) {
                if (null !== $headers) {
                    $e->setHeaders($headers);
                    $e->setStatusCode($statusCode);
                }
                if (null !== $response) {
                    $e->setResponse($response->getBody());
                }
                $e->setResponseInfo($progress);
            }
            $this->rejectJob($job, $e);
        }
    }
    foreach ($this->jobs as $i => $curlHandle) {
        $curlHandle = $this->jobs[$i]['curlHandle'];
        $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
        if ($this->jobs[$i]['progress'] !== $progress) {
            $this->jobs[$i]['progress'] = $progress;
            if (isset($this->jobs[$i]['options']['max_file_size'])) {
                // Compare max_file_size with the content-length header this value will be -1 until the header is parsed
                if ($this->jobs[$i]['options']['max_file_size'] < $progress['download_content_length']) {
                    $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Content-length header indicates ' . $progress['download_content_length'] . ' bytes. Allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes'));
                }
                // Compare max_file_size with the download size in bytes
                if ($this->jobs[$i]['options']['max_file_size'] < $progress['size_download']) {
                    $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . $progress['size_download'] . ' of allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes'));
                }
            }
            if (isset($progress['primary_ip']) && $progress['primary_ip'] !== $this->jobs[$i]['primaryIp']) {
                if (isset($this->jobs[$i]['options']['prevent_ip_access_callable']) && is_callable($this->jobs[$i]['options']['prevent_ip_access_callable']) && $this->jobs[$i]['options']['prevent_ip_access_callable']($progress['primary_ip'])) {
                    $this->rejectJob($this->jobs[$i], new TransportException(sprintf('IP "%s" is blocked for "%s".', $progress['primary_ip'], $progress['url'])));
                }
                $this->jobs[$i]['primaryIp'] = (string) $progress['primary_ip'];
            }
            // TODO progress
        }
    }
}

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal