* @author Nicolas Grekas
- * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null}
- * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable, primaryIp: string}
+ * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, authRetries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null}
+ * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable, primaryIp: string, hostKey: string|null}
+ * @phpstan-type RetryAttributes array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>, ipResolve?: 4|6}
+ * @phpstan-type DelayedJob array{job: Job, url: non-empty-string, attributes: RetryAttributes, at: float}
*/
class CurlDownloader
{
@@ -41,12 +42,39 @@ class CurlDownloader
*/
private const BAD_MULTIPLEXING_CURL_VERSIONS = ['7.87.0', '7.88.0', '7.88.1'];
+ /**
+ * Longest Retry-After interval which is waited out, in seconds
+ *
+ * Without a ceiling a server could park an install for as long as it likes, see isStatusCodeRetryNeeded()
+ */
+ private const MAX_RETRY_AFTER = 60;
+
/** @var \CurlMultiHandle */
private $multiHandle;
/** @var \CurlShareHandle */
private $shareHandle;
/** @var Job[] */
private $jobs = [];
+ /**
+ * Retries which are waiting for their delay to elapse, see restartJobWithDelay()
+ *
+ * @var array
+ */
+ private $delayedJobs = [];
+ /**
+ * Origins which asked for requests to be retried later, keyed by origin, see restartJobWithDelay()
+ *
+ * until is the latest point in time the origin asked to be left alone for, see getDueAt()
+ *
+ * @var array
+ */
+ private $retryAfterOrigins = [];
+ /**
+ * Hosts which presented a distrusted TLS chain, keyed by host:port, see checkCertificateChain()
+ *
+ * @var array
+ */
+ private $distrustedHosts = [];
/** @var IOInterface */
private $io;
/** @var Config */
@@ -87,13 +115,15 @@ class CurlDownloader
];
/** @var array */
- private static $timeInfo = [
+ private static $ignoredProgressInfo = [
'total_time' => true,
'namelookup_time' => true,
'connect_time' => true,
'pretransfer_time' => true,
'starttransfer_time' => true,
'redirect_time' => true,
+ // large once CURLOPT_CERTINFO is set, and the chain is checked separately
+ 'certinfo' => true,
];
/**
@@ -153,7 +183,7 @@ public function download(callable $resolve, callable $reject, string $origin, st
/**
* @param mixed[] $options
*
- * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, storeAuth?: 'prompt'|bool, ipResolve?: 4|6|null} $attributes
+ * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, retries?: int<0, max>, authRetries?: int<0, max>, storeAuth?: 'prompt'|bool, ipResolve?: 4|6|null} $attributes
* @param non-empty-string $url
*
* @return int internal job id
@@ -164,6 +194,7 @@ private function initDownload(callable $resolve, callable $reject, string $origi
'retryAuthFailure' => true,
'redirects' => 0,
'retries' => 0,
+ 'authRetries' => 0,
'storeAuth' => false,
'ipResolve' => null,
], $attributes);
@@ -189,6 +220,11 @@ private function initDownload(callable $resolve, callable $reject, string $origi
throw new TransportException('Access to "'.Url::sanitize($url).'" is blocked.');
}
+ $hostKey = TlsChainDenylist::getHostKey($url);
+ if (null !== $hostKey && isset($this->distrustedHosts[$hostKey])) {
+ throw TlsChainDenylist::createException($url, $this->distrustedHosts[$hostKey], false);
+ }
+
$curlHandle = curl_init();
$headerHandle = fopen('php://temp/maxmemory:32768', 'w+b');
if (false === $headerHandle) {
@@ -225,6 +261,16 @@ private function initDownload(callable $resolve, callable $reject, string $origi
curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+ if (null !== $hostKey) {
+ curl_setopt($curlHandle, CURLOPT_CERTINFO, true);
+ if (\defined('CURLOPT_PREREQFUNCTION')) {
+ // runs after the TLS handshake but before the request and its credentials are sent
+ curl_setopt($curlHandle, CURLOPT_PREREQFUNCTION, function ($handle) use ($hostKey): int {
+ return null === $this->checkCertificateChain($hostKey, curl_getinfo($handle, CURLINFO_CERTINFO)) ? 0 : 1; // CURL_PREREQFUNC_OK : CURL_PREREQFUNC_ABORT, literals as the constants only exist on PHP 8.4+
+ });
+ }
+ }
+
if ($attributes['ipResolve'] === 4) {
curl_setopt($curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
} elseif ($attributes['ipResolve'] === 6) {
@@ -252,14 +298,12 @@ private function initDownload(callable $resolve, callable $reject, string $origi
$proxy = ProxyManager::getInstance()->getProxyForRequest($url);
- if (0 === strpos($url, 'https://')) {
- $willUseProxy = $proxy->getStatus() !== '' && !$proxy->isExcludedByNoProxy();
-
- if (!$willUseProxy && \defined('CURL_VERSION_HTTP3') && \defined('CURL_HTTP_VERSION_3') && (CURL_VERSION_HTTP3 & $features) !== 0) {
- curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
- } elseif (\defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features) !== 0) {
- curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
- }
+ // HTTP/3 is deliberately not requested: packagist.org, GitHub, GitLab and Bitbucket do not support it yet, and
+ // CURL_HTTP_VERSION_3 makes curl wait for a QUIC handshake before falling back to TCP on every new connection.
+ // Worth revisiting once HTTP/3 is more widespread, ideally via CURLOPT_ALTSVC so only advertising hosts use it,
+ // see https://github.com/composer/composer/issues/12987
+ if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features) !== 0) {
+ curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
}
// curl 8.7.0 - 8.7.1 has a bug whereas automatic accept-encoding header results in an error when reading the response
@@ -285,7 +329,7 @@ private function initDownload(callable $resolve, callable $reject, string $origi
curl_setopt_array($curlHandle, $proxy->getCurlOptions($options['ssl'] ?? []));
- $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
+ $progress = array_diff_key(curl_getinfo($curlHandle), self::$ignoredProgressInfo);
$this->jobs[(int) $curlHandle] = [
'url' => $url,
@@ -300,6 +344,7 @@ private function initDownload(callable $resolve, callable $reject, string $origi
'resolve' => $resolve,
'reject' => $reject,
'primaryIp' => '',
+ 'hostKey' => $hostKey,
];
$usingProxy = $proxy->getStatus(' using proxy (%s)');
@@ -333,17 +378,53 @@ public function abortRequest(int $id): void
}
unset($this->jobs[$id]);
}
+
+ // a retry waiting for its delay is not in flight but must not be started either
+ foreach ($this->delayedJobs as $i => $delayedJob) {
+ if ((int) $delayedJob['job']['curlHandle'] === $id) {
+ if (null !== $delayedJob['job']['filename']) {
+ @unlink($delayedJob['job']['filename'].'~');
+ }
+ unset($this->delayedJobs[$i]);
+ }
+ }
+ }
+
+ /**
+ * @return int number of retries waiting for their delay, which hold no transfer open
+ */
+ public function countDelayedJobs(): int
+ {
+ return count($this->delayedJobs);
+ }
+
+ /**
+ * @return bool whether the origin asked for requests to be held back until later, see restartJobWithDelay()
+ */
+ public function isOriginOnHold(string $origin): bool
+ {
+ return ($this->retryAfterOrigins[$origin]['until'] ?? 0.0) > microtime(true);
}
public function tick(): void
{
+ if (count($this->jobs) === 0 && count($this->delayedJobs) === 0) {
+ return;
+ }
+
+ $this->restartDueJobs();
+
if (count($this->jobs) === 0) {
+ // only delayed retries are left, so there is no transfer this could hold up and the
+ // short sleep merely avoids spinning the CPU until the next one comes due
+ usleep(10000);
+
return;
}
$active = true;
$this->checkCurlResult(curl_multi_exec($this->multiHandle, $active));
- if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) {
+ if (-1 === curl_multi_select($this->multiHandle, $this->getSelectTimeout())) {
// 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);
}
@@ -373,6 +454,16 @@ public function tick(): void
$statusCode = null;
$response = null;
try {
+ if (null !== $job['hostKey'] && null !== ($distrusted = $this->checkCertificateChain($job['hostKey'], $progress['certinfo'] ?? []))) {
+ $requestSent = !\defined('CURLOPT_PREREQFUNCTION');
+ $e = TlsChainDenylist::createException($job['url'], $distrusted, $requestSent);
+ if ($requestSent) {
+ // callers may swallow transport errors, but possibly leaked credentials must be reported
+ $this->io->writeError(''.$e->getMessage().' ');
+ }
+ throw $e;
+ }
+
// TODO progress
if (CURLE_OK !== $errno || $error || $result !== CURLE_OK) {
$errno = $errno ?: $result;
@@ -444,13 +535,13 @@ public function tick(): void
fclose($job['bodyHandle']);
$warningsOutput = false;
- if ($response->getStatusCode() >= 300 && $response->getHeader('content-type') === 'application/json') {
- $warningsOutput = HttpDownloader::outputWarnings($this->io, $job['origin'], json_decode($response->getBody(), true));
+ if ($response->getStatusCode() >= 300 && self::isJsonResponse($response)) {
+ $warningsOutput = ResponseWarnings::output($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]);
+ $this->restartJob($job, $job['url'], ['storeAuth' => $result['storeAuth'], 'retries' => $job['attributes']['retries'] + 1, 'authRetries' => $job['attributes']['authRetries'] + 1]);
continue;
}
@@ -465,20 +556,14 @@ public function tick(): void
// fail 4xx and 5xx responses and capture the response
if ($statusCode >= 400 && $statusCode <= 599) {
- $retryableStatusCode = in_array($statusCode, [423, 425, 500, 502, 503, 504, 507, 510], true)
- // codeload.github.com intermittently returns 400 on reused connections, retry those specifically, see #12958
- || ($statusCode === 400 && parse_url($job['url'], PHP_URL_HOST) === 'codeload.github.com');
- if (
- (!isset($job['options']['http']['method']) || $job['options']['http']['method'] === 'GET')
- && $retryableStatusCode
- && $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]);
+ $statusRetry = $this->isStatusCodeRetryNeeded($job, $response);
+ if ($statusRetry['retry']) {
+ $this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to status code '. $statusCode . (null !== $statusRetry['delay'] ? ' in '.$statusRetry['delay'].'s as requested by Retry-After' : ''), true, IOInterface::DEBUG);
+ $this->restartJobWithDelay($job, $job['url'], ['retries' => $job['attributes']['retries'] + 1], $statusRetry['delay'], $statusCode);
continue;
}
- throw $this->failResponse($job, $response, $response->getStatusMessage(), $warningsOutput);
+ throw $this->failResponse($job, $response, self::getStatusFailureMessage($response), $warningsOutput);
}
if ($job['attributes']['storeAuth'] !== false) {
@@ -508,9 +593,11 @@ public function tick(): void
}
}
+ $this->announceRetryAfterWaits();
+
foreach ($this->jobs as $i => $curlHandle) {
$curlHandle = $this->jobs[$i]['curlHandle'];
- $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
+ $progress = array_diff_key(curl_getinfo($curlHandle), self::$ignoredProgressInfo);
if ($this->jobs[$i]['progress'] !== $progress) {
$this->jobs[$i]['progress'] = $progress;
@@ -544,6 +631,21 @@ public function tick(): void
}
}
+ /**
+ * @param array $certInfo
+ *
+ * @return string|null name of the distrusted certificate if the host presented one
+ */
+ private function checkCertificateChain(string $hostKey, array $certInfo): ?string
+ {
+ // certinfo is only filled for the request which opened the connection, reused ones rely on the earlier verdict
+ if (count($certInfo) > 0 && null !== ($distrusted = TlsChainDenylist::findDistrustedCertificateInCurlInfo($certInfo))) {
+ $this->distrustedHosts[$hostKey] = $distrusted;
+ }
+
+ return $this->distrustedHosts[$hostKey] ?? null;
+ }
+
/**
* @param Job $job
*/
@@ -589,7 +691,7 @@ private function handleRedirect(array $job, Response $response): string
private function isAuthenticatedRetryNeeded(array $job, Response $response): array
{
if (in_array($response->getStatusCode(), [401, 403]) && $job['attributes']['retryAuthFailure']) {
- $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders(), $job['attributes']['retries'], $response->getBody());
+ $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders(), $job['attributes']['authRetries'], $response->getBody());
if ($result['retry']) {
return $result;
@@ -621,7 +723,7 @@ private function isAuthenticatedRetryNeeded(array $job, Response $response): arr
if ($needsAuthRetry) {
if ($job['attributes']['retryAuthFailure']) {
- $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401, null, [], $job['attributes']['retries']);
+ $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401, null, [], $job['attributes']['authRetries']);
if ($result['retry']) {
return $result;
}
@@ -633,11 +735,134 @@ private function isAuthenticatedRetryNeeded(array $job, Response $response): arr
return ['retry' => false, 'storeAuth' => false];
}
+ /**
+ * Decides whether a 4xx/5xx response should be retried, and how long to wait beforehand
+ *
+ * @param Job $job
+ * @return array{retry: bool, delay: float|null} delay is null when the default backoff applies
+ */
+ private function isStatusCodeRetryNeeded(array $job, Response $response): array
+ {
+ $noRetry = ['retry' => false, 'delay' => null];
+
+ if (isset($job['options']['http']['method']) && $job['options']['http']['method'] !== 'GET') {
+ return $noRetry;
+ }
+
+ if ($job['attributes']['retries'] >= $this->maxRetries) {
+ return $noRetry;
+ }
+
+ $statusCode = $response->getStatusCode();
+ $retryAfter = self::getRetryAfterDelay($response);
+
+ $retryableByStatus = in_array($statusCode, [423, 425, 500, 502, 503, 504, 507, 510], true)
+ // codeload.github.com intermittently returns 400 on reused connections, retry those specifically, see #12958
+ || ($statusCode === 400 && parse_url($job['url'], PHP_URL_HOST) === 'codeload.github.com');
+
+ // a rate limit is only retried when the server said when to come back
+ $retryableByHeader = $statusCode === 429 && null !== $retryAfter;
+
+ if (!$retryableByStatus && !$retryableByHeader) {
+ return $noRetry;
+ }
+
+ // an interval beyond the cap is not waited for: a status which is retryable anyway falls
+ // back to the usual backoff, a rate limit has nothing else to go on and fails
+ if (null !== $retryAfter && $retryAfter > self::MAX_RETRY_AFTER) {
+ if (!$retryableByStatus) {
+ return $noRetry;
+ }
+
+ $retryAfter = null;
+ }
+
+ // a wait of 0s is no wait, so the usual backoff spaces such retries out instead
+ return ['retry' => true, 'delay' => null !== $retryAfter && $retryAfter > 0 ? (float) $retryAfter : null];
+ }
+
+ /**
+ * Reads the Retry-After header, in either of the forms RFC 9110 allows, as a number of seconds to wait
+ *
+ * @return int|null seconds to wait, or null if there is no usable Retry-After header
+ */
+ private static function getRetryAfterDelay(Response $response): ?int
+ {
+ $retryAfter = $response->getHeader('retry-after');
+ if (null === $retryAfter) {
+ return null;
+ }
+
+ // more digits would overflow an int, and are no interval anyone can mean
+ if (Preg::isMatch('{^\d{1,9}$}', $retryAfter)) {
+ return (int) $retryAfter;
+ }
+
+ // the three date formats RFC 9110 requires recipients to accept, with the leading day name
+ // skipped as it is redundant and DateTime would otherwise move the date to match it
+ foreach (['!*, d M Y H:i:s \G\M\T', '!*, d-M-y H:i:s \G\M\T', '!* M j H:i:s Y'] as $format) {
+ $retryAt = \DateTime::createFromFormat($format, $retryAfter, new \DateTimeZone('UTC'));
+ if (false !== $retryAt) {
+ // a date which has already passed means the wait is over and we can retry straight away
+ return max(0, $retryAt->getTimestamp() - time());
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Starts the delayed retries whose delay has elapsed
+ */
+ private function restartDueJobs(): void
+ {
+ $now = microtime(true);
+ foreach ($this->delayedJobs as $i => $delayedJob) {
+ if ($this->getDueAt($delayedJob) > $now) {
+ continue;
+ }
+
+ unset($this->delayedJobs[$i]);
+ try {
+ $this->restartJob($delayedJob['job'], $delayedJob['url'], $delayedJob['attributes']);
+ } catch (\Exception $e) {
+ // an immediate restart is covered by tick()'s try/catch, a deferred one is not
+ $this->rejectJob($delayedJob['job'], $e);
+ }
+ }
+ }
+
+ /**
+ * @return float how long curl_multi_select may block for, in seconds
+ */
+ private function getSelectTimeout(): float
+ {
+ $timeout = $this->selectTimeout;
+ $now = microtime(true);
+ foreach ($this->delayedJobs as $delayedJob) {
+ // never sleep past the point where a delayed retry comes due
+ $timeout = min($timeout, max(0.0, $this->getDueAt($delayedJob) - $now));
+ }
+
+ return $timeout;
+ }
+
+ /**
+ * A Retry-After speaks for the whole origin, so a retry also waits for a later hold the origin asked for
+ *
+ * @param DelayedJob $delayedJob
+ * @return float when the retry may be started, as a timestamp
+ */
+ private function getDueAt(array $delayedJob): float
+ {
+ return max($delayedJob['at'], $this->retryAfterOrigins[$delayedJob['job']['origin']]['until'] ?? 0.0);
+ }
+
/**
* @param Job $job
* @param non-empty-string $url
*
- * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>, ipResolve?: 4|6} $attributes
+ * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries?: int<1, max>, authRetries?: int<1, max>, ipResolve?: 4|6} $attributes
*/
private function restartJob(array $job, string $url, array $attributes = []): void
{
@@ -655,17 +880,72 @@ private function restartJob(array $job, string $url, array $attributes = []): vo
* @param Job $job
* @param non-empty-string $url
*
- * @param array{retryAuthFailure?: bool, redirects?: int<0, max>, storeAuth?: 'prompt'|bool, retries: int<1, max>, ipResolve?: 4|6} $attributes
+ * @param RetryAttributes $attributes
+ * @param float|null $retryAfter seconds the response asked to wait before retrying, null to use the default backoff
+ * @param int $statusCode status code of the response which asked for the wait, only used with $retryAfter
*/
- private function restartJobWithDelay(array $job, string $url, array $attributes): void
+ private function restartJobWithDelay(array $job, string $url, array $attributes, ?float $retryAfter = null, int $statusCode = 0): void
{
- if ($attributes['retries'] >= 3) {
- usleep(500000); // half a second delay for 3rd retry and beyond
+ $now = microtime(true);
+ if (null !== $retryAfter) {
+ $delay = $retryAfter;
+ $until = $now + $retryAfter;
+ $origin = $this->retryAfterOrigins[$job['origin']] ?? null;
+ // a hold which has already run out is over, so the origin is announced anew rather than extended
+ if (null === $origin || $origin['until'] <= $now) {
+ $this->retryAfterOrigins[$job['origin']] = ['statusCode' => $statusCode, 'announced' => false, 'until' => $until];
+ } else {
+ $this->retryAfterOrigins[$job['origin']]['until'] = max($origin['until'], $until);
+ }
+ } elseif ($attributes['retries'] >= 3) {
+ $delay = 0.5; // half a second delay for 3rd retry and beyond
} elseif ($attributes['retries'] >= 2) {
- usleep(100000); // 100ms delay for 2nd retry
- } // no sleep for the first retry
+ $delay = 0.1; // 100ms delay for 2nd retry
+ } else {
+ $delay = 0.0; // no delay for the first retry
+ }
+
+ if ($delay <= 0.0 && ($this->retryAfterOrigins[$job['origin']]['until'] ?? 0.0) <= $now) {
+ $this->restartJob($job, $url, $attributes);
+
+ return;
+ }
- $this->restartJob($job, $url, $attributes);
+ // sleeping here would stall every other transfer tick() drives, so the retry is scheduled instead
+ $this->delayedJobs[] = ['job' => $job, 'url' => $url, 'attributes' => $attributes, 'at' => $now + $delay];
+ }
+
+ /**
+ * Tells the user when an origin asked for requests to be retried later, once per origin and episode
+ *
+ * The wait can last up to MAX_RETRY_AFTER seconds, which without a word would look like a hang
+ */
+ private function announceRetryAfterWaits(): void
+ {
+ $now = microtime(true);
+ foreach ($this->retryAfterOrigins as $origin => $retryAfterOrigin) {
+ $pending = 0;
+ $lastDue = $now;
+ foreach ($this->delayedJobs as $delayedJob) {
+ if ($delayedJob['job']['origin'] === $origin) {
+ $pending++;
+ $lastDue = max($lastDue, $this->getDueAt($delayedJob));
+ }
+ }
+
+ if (0 === $pending) {
+ unset($this->retryAfterOrigins[$origin]);
+ continue;
+ }
+
+ if ($retryAfterOrigin['announced']) {
+ continue;
+ }
+
+ $reason = 429 === $retryAfterOrigin['statusCode'] ? 'is rate limiting requests' : 'responded with status code '.$retryAfterOrigin['statusCode'];
+ $this->io->writeError(''.$origin.' '.$reason.' ('.$pending.' pending), waiting '.(int) ceil($lastDue - $now).'s before retrying ');
+ $this->retryAfterOrigins[$origin]['announced'] = true;
+ }
}
/**
@@ -678,14 +958,36 @@ private function failResponse(array $job, Response $response, string $errorMessa
}
$details = '';
- // skip dumping the raw JSON body when outputWarnings already presented it cleanly, to avoid duplicate/messy output
- if (!$warningsOutput && in_array(strtolower((string) $response->getHeader('content-type')), ['application/json', 'application/json; charset=utf-8'], true)) {
+ // skip dumping the raw JSON body when ResponseWarnings::output() already presented it cleanly, to avoid duplicate/messy output
+ if (!$warningsOutput && self::isJsonResponse($response)) {
$details = ':'.PHP_EOL.substr($response->getBody(), 0, 200).(strlen($response->getBody()) > 200 ? '...' : '');
}
return new TransportException('The "'.Url::sanitize($job['url']).'" file could not be downloaded ('.$errorMessage.')' . $details, $response->getStatusCode());
}
+ /**
+ * Describes why a 4xx/5xx response failed the request
+ *
+ * A Retry-After which is longer than we wait for is spelled out, as it tells the user whether
+ * trying again straight away can help or whether to come back later.
+ */
+ private static function getStatusFailureMessage(Response $response): string
+ {
+ $message = (string) $response->getStatusMessage();
+ $retryAfter = self::getRetryAfterDelay($response);
+ if (null !== $retryAfter && $retryAfter > self::MAX_RETRY_AFTER) {
+ $message .= ', the server asked to retry in '.$retryAfter.'s which is longer than the '.self::MAX_RETRY_AFTER.'s Composer is willing to wait, try again later';
+ }
+
+ return $message;
+ }
+
+ private static function isJsonResponse(Response $response): bool
+ {
+ return in_array(strtolower((string) $response->getHeader('content-type')), ['application/json', 'application/json; charset=utf-8'], true);
+ }
+
/**
* @param Job $job
*/
diff --git a/core/vendor/composer/composer/src/Composer/Util/Http/ResponseWarnings.php b/core/vendor/composer/composer/src/Composer/Util/Http/ResponseWarnings.php
new file mode 100644
index 0000000000..064f1cde55
--- /dev/null
+++ b/core/vendor/composer/composer/src/Composer/Util/Http/ResponseWarnings.php
@@ -0,0 +1,127 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Util\Http;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Package\Version\VersionParser;
+use Composer\Pcre\Preg;
+use Composer\Semver\Constraint\Constraint;
+use Composer\Util\Url;
+
+/**
+ * Shows the warnings/infos which repositories can send in their responses
+ *
+ * @internal
+ */
+final class ResponseWarnings
+{
+ /**
+ * Warnings which were already shown, keyed by host and warning, see output()
+ *
+ * @var array
+ */
+ private static $shownWarnings = [];
+
+ /**
+ * Shows the warnings/infos of a response, unless the same host already showed the same ones
+ *
+ * @param mixed $data the decoded response body, expected to be array{warning?: string, info?: string, warning-versions?: string, info-versions?: string, warnings?: array, infos?: array}
+ *
+ * @return bool whether any warning/info was shown, now or before
+ */
+ public static function output(IOInterface $io, string $url, $data): bool
+ {
+ if (!is_array($data)) {
+ return false;
+ }
+
+ // the parts of the body which are not shown do not make a warning a different one
+ // key by host so callers passing an origin or a repository url share the same entry, see Url::getOrigin
+ $host = (string) parse_url($url, PHP_URL_HOST);
+ if ($host === '') {
+ $host = $url;
+ } elseif (is_int($port = parse_url($url, PHP_URL_PORT))) {
+ $host .= ':'.$port;
+ }
+ if ($host === 'repo.packagist.org') {
+ $host = 'packagist.org';
+ }
+ $shownKey = $host.':'.json_encode(array_intersect_key($data, array_flip(['warning', 'warning-versions', 'info', 'info-versions', 'warnings', 'infos'])));
+ if (isset(self::$shownWarnings[$shownKey])) {
+ return true;
+ }
+
+ $wrote = false;
+ $cleanMessage = static function ($msg) use ($io) {
+ if (!$io->isDecorated()) {
+ $msg = Preg::replace('{'.chr(27).'\\[[;\d]*m}u', '', $msg);
+ }
+
+ return $msg;
+ };
+
+ // legacy warning/info keys
+ foreach (['warning', 'info'] as $type) {
+ if (empty($data[$type])) {
+ continue;
+ }
+
+ if (!empty($data[$type . '-versions'])) {
+ $versionParser = new VersionParser();
+ $constraint = $versionParser->parseConstraints($data[$type . '-versions']);
+ $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion()));
+ if (!$constraint->matches($composer)) {
+ continue;
+ }
+ }
+
+ $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$cleanMessage($data[$type]).''.$type.'>');
+ $wrote = true;
+ }
+
+ // modern Composer 2.2+ format with support for multiple warning/info messages
+ foreach (['warnings', 'infos'] as $key) {
+ if (empty($data[$key])) {
+ continue;
+ }
+
+ $versionParser = new VersionParser();
+ foreach ($data[$key] as $spec) {
+ $type = substr($key, 0, -1);
+ $constraint = $versionParser->parseConstraints($spec['versions']);
+ $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion()));
+ if (!$constraint->matches($composer)) {
+ continue;
+ }
+
+ $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$cleanMessage($spec['message']).''.$type.'>');
+ $wrote = true;
+ }
+ }
+
+ if ($wrote) {
+ self::$shownWarnings[$shownKey] = true;
+ }
+
+ return $wrote;
+ }
+
+ /**
+ * Forgets which warnings were shown, so long-running processes can show them again in their next job
+ */
+ public static function reset(): void
+ {
+ self::$shownWarnings = [];
+ }
+}
diff --git a/core/vendor/composer/composer/src/Composer/Util/Http/TlsChainDenylist.php b/core/vendor/composer/composer/src/Composer/Util/Http/TlsChainDenylist.php
new file mode 100644
index 0000000000..6afb940b3d
--- /dev/null
+++ b/core/vendor/composer/composer/src/Composer/Util/Http/TlsChainDenylist.php
@@ -0,0 +1,71 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Util\Http;
+
+use Composer\CaBundle\CaBundle;
+use Composer\Exception\SecurityException;
+use Composer\Util\Url;
+
+/**
+ * Rejects TLS connections whose presented chain contains a distrusted (e.g. compromised) intermediate
+ *
+ * The system CA store may still trust the roots above them, so this cannot be left to the cafile alone.
+ *
+ * @internal
+ */
+final class TlsChainDenylist
+{
+ /**
+ * @param array $certInfo as returned by curl_getinfo($handle, CURLINFO_CERTINFO)
+ *
+ * @return string|null name of the first distrusted certificate found in the chain
+ */
+ public static function findDistrustedCertificateInCurlInfo(array $certInfo): ?string
+ {
+ return CaBundle::findDistrustedCertificate(array_column($certInfo, 'Cert'));
+ }
+
+ /**
+ * @return string|null host:port for https URLs, as that is what curl reuses connections by
+ */
+ public static function getHostKey(string $url): ?string
+ {
+ if (0 !== stripos($url, 'https://')) {
+ return null;
+ }
+
+ $host = parse_url($url, PHP_URL_HOST);
+ if (!is_string($host)) {
+ return null;
+ }
+
+ return strtolower($host).':'.(parse_url($url, PHP_URL_PORT) ?? 443);
+ }
+
+ /**
+ * Not a TransportException so that callers falling back on transport errors do not swallow it
+ *
+ * @param bool $requestSent whether the request (and any credentials in it) already went out before the chain could be checked
+ */
+ public static function createException(string $url, string $certName, bool $requestSent): SecurityException
+ {
+ $message = 'The TLS certificate chain presented for "'.Url::sanitize($url).'" contains the distrusted certificate "'.$certName.'", this may be a man-in-the-middle attack.';
+ if ($requestSent) {
+ $message .= ' The request was already sent before this could be detected (upgrade to PHP 8.4+ with the curl extension to detect it earlier), if it contained authentication credentials they may have been intercepted by an attacker and should be rotated.';
+ } else {
+ $message .= ' The request was aborted before it was sent.';
+ }
+
+ return new SecurityException($message);
+ }
+}
diff --git a/core/vendor/composer/composer/src/Composer/Util/HttpDownloader.php b/core/vendor/composer/composer/src/Composer/Util/HttpDownloader.php
index eae49f40c9..e0757b8b82 100644
--- a/core/vendor/composer/composer/src/Composer/Util/HttpDownloader.php
+++ b/core/vendor/composer/composer/src/Composer/Util/HttpDownloader.php
@@ -18,9 +18,7 @@
use Composer\Pcre\Preg;
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\Util\Http\ResponseWarnings;
use Composer\Exception\IrrecoverableDownloadException;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
@@ -298,7 +296,7 @@ private function addJob(array $request, bool $sync = false): array
});
$this->jobs[$job['id']] = &$job;
- if ($this->runningJobs < $this->maxJobs) {
+ if ($this->canStartJob($job)) {
$this->startJob($job['id']);
}
@@ -348,6 +346,21 @@ private function startJob(int $id): void
}
}
+ /**
+ * @param Job $job
+ */
+ private function canStartJob(array $job): bool
+ {
+ // a retry waiting for its Retry-After holds no transfer open, so its slot is free for another request meanwhile
+ $maxJobs = $this->maxJobs + (null !== $this->curl ? $this->curl->countDelayedJobs() : 0);
+ if ($this->runningJobs >= $maxJobs) {
+ return false;
+ }
+
+ // an origin which asked to be left alone gets no new request either, not just no retry
+ return null === $this->curl || !$this->curl->isOriginOnHold($job['origin']);
+ }
+
private function markJobDone(): void
{
$this->runningJobs--;
@@ -383,11 +396,9 @@ public function enableAsync(): void
*/
public function countActiveJobs(?int $index = null): int
{
- if ($this->runningJobs < $this->maxJobs) {
- foreach ($this->jobs as $job) {
- if ($job['status'] === self::STATUS_QUEUED && $this->runningJobs < $this->maxJobs) {
- $this->startJob($job['id']);
- }
+ foreach ($this->jobs as $job) {
+ if ($job['status'] === self::STATUS_QUEUED && $this->canStartJob($job)) {
+ $this->startJob($job['id']);
}
}
@@ -438,62 +449,13 @@ private function getResponse(int $index): Response
/**
* @internal
+ * @deprecated use ResponseWarnings::output() instead
*
- * @param array{warning?: string, info?: string, warning-versions?: string, info-versions?: string, warnings?: array, infos?: array} $data
- *
- * @return bool whether any warning/info was actually written to the output
+ * @param mixed $data
*/
public static function outputWarnings(IOInterface $io, string $url, $data): bool
{
- $wrote = false;
- $cleanMessage = static function ($msg) use ($io) {
- if (!$io->isDecorated()) {
- $msg = Preg::replace('{'.chr(27).'\\[[;\d]*m}u', '', $msg);
- }
-
- return $msg;
- };
-
- // legacy warning/info keys
- foreach (['warning', 'info'] as $type) {
- if (empty($data[$type])) {
- continue;
- }
-
- if (!empty($data[$type . '-versions'])) {
- $versionParser = new VersionParser();
- $constraint = $versionParser->parseConstraints($data[$type . '-versions']);
- $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion()));
- if (!$constraint->matches($composer)) {
- continue;
- }
- }
-
- $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$cleanMessage($data[$type]).''.$type.'>');
- $wrote = true;
- }
-
- // modern Composer 2.2+ format with support for multiple warning/info messages
- foreach (['warnings', 'infos'] as $key) {
- if (empty($data[$key])) {
- continue;
- }
-
- $versionParser = new VersionParser();
- foreach ($data[$key] as $spec) {
- $type = substr($key, 0, -1);
- $constraint = $versionParser->parseConstraints($spec['versions']);
- $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion()));
- if (!$constraint->matches($composer)) {
- continue;
- }
-
- $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$cleanMessage($spec['message']).''.$type.'>');
- $wrote = true;
- }
- }
-
- return $wrote;
+ return ResponseWarnings::output($io, $url, $data);
}
/**
diff --git a/core/vendor/composer/composer/src/Composer/Util/Loop.php b/core/vendor/composer/composer/src/Composer/Util/Loop.php
index 7cee5c8e66..58917d32f4 100644
--- a/core/vendor/composer/composer/src/Composer/Util/Loop.php
+++ b/core/vendor/composer/composer/src/Composer/Util/Loop.php
@@ -52,8 +52,9 @@ public function getProcessExecutor(): ?ProcessExecutor
/**
* @param array> $promises
+ * @param (callable(int): void)|null $onProgress
*/
- public function wait(array $promises, ?ProgressBar $progress = null): void
+ public function wait(array $promises, ?ProgressBar $progress = null, ?callable $onProgress = null): void
{
$uncaught = null;
@@ -70,6 +71,22 @@ static function (\Throwable $e) use (&$uncaught): void {
$waitIndex = $this->waitIndex++;
$this->currentPromises[$waitIndex] = $promises;
+ try {
+ $this->runJobs($progress, $onProgress);
+ } finally {
+ unset($this->currentPromises[$waitIndex]);
+ }
+
+ if (null !== $uncaught) {
+ throw $uncaught;
+ }
+ }
+
+ /**
+ * @param (callable(int): void)|null $onProgress
+ */
+ private function runJobs(?ProgressBar $progress, ?callable $onProgress): void
+ {
if ($progress) {
$totalJobs = 0;
$totalJobs += $this->httpDownloader->countActiveJobs();
@@ -91,6 +108,9 @@ static function (\Throwable $e) use (&$uncaught): void {
if ($progress && microtime(true) - $lastUpdate > 0.1) {
$lastUpdate = microtime(true);
$progress->setProgress($progress->getMaxSteps() - $activeJobs);
+ if ($onProgress !== null && $progress->getMaxSteps() > 0) {
+ $onProgress((int) floor(100 * $progress->getProgress() / $progress->getMaxSteps()));
+ }
}
if (!$activeJobs) {
@@ -101,11 +121,9 @@ static function (\Throwable $e) use (&$uncaught): void {
// as we skip progress updates if they are too quick, make sure we do one last one here at 100%
if ($progress) {
$progress->finish();
- }
-
- unset($this->currentPromises[$waitIndex]);
- if (null !== $uncaught) {
- throw $uncaught;
+ if ($onProgress !== null) {
+ $onProgress(100);
+ }
}
}
diff --git a/core/vendor/composer/composer/src/Composer/Util/Platform.php b/core/vendor/composer/composer/src/Composer/Util/Platform.php
index 6606531596..4e38fdb8ee 100644
--- a/core/vendor/composer/composer/src/Composer/Util/Platform.php
+++ b/core/vendor/composer/composer/src/Composer/Util/Platform.php
@@ -21,12 +21,138 @@
*/
class Platform
{
+ /**
+ * Matches OS metadata file names, Windows ones case-insensitively
+ *
+ * @internal
+ */
+ public const METADATA_NAME_REGEX = '{^(?:\._.*|\.DS_Store|__MACOSX|\.AppleDouble|\.fseventsd|\.Spotlight-V100|\.Trashes|(?i:thumbs\.db|desktop\.ini))$}';
+
+ /**
+ * Env vars which indicate that Composer is being run by an AI coding agent, mapped to the agent name
+ *
+ * Order matters as the first match wins. AI_AGENT is handled separately, see getAiAgent()
+ *
+ * Initially copied from https://github.com/laravel/agent-detector. Thanks!
+ *
+ * @internal
+ */
+ public const CODING_AGENT_ENV_VARS = [
+ // Amp, CodeBuddy and Cowork set/mirror Claude Code vars, so they must precede them
+ 'AMP_CURRENT_THREAD_ID' => 'amp',
+ 'CODEBUDDY' => 'codebuddy',
+ 'CLAUDE_CODE_IS_COWORK' => 'cowork',
+ 'CLAUDECODE' => 'claude-code',
+ 'CLAUDE_CODE' => 'claude-code',
+ 'CURSOR_AGENT' => 'cursor',
+ 'CURSOR_SANDBOX' => 'cursor-cli',
+ // Gemini CLI forks, must precede GEMINI_CLI
+ 'QWEN_CODE' => 'qwen-code',
+ 'VECLI_DIR' => 'vecli',
+ 'GEMINI_CLI' => 'gemini-cli',
+ 'CODEX_SANDBOX' => 'codex',
+ 'CODEX_CI' => 'codex',
+ 'CODEX_THREAD_ID' => 'codex',
+ 'AUGMENT_AGENT' => 'augment',
+ 'KILO' => 'kilo-code', // must precede OPENCODE* as Kilo inherits them
+ 'OPENCODE_CLIENT' => 'opencode',
+ 'OPENCODE' => 'opencode',
+ 'CLINE_ACTIVE' => 'cline',
+ 'CLINE_TASK_ID' => 'cline',
+ 'ROO_CODE_TASK_ID' => 'roo-code',
+ 'CRUSH' => 'crush',
+ 'IFLOW_CLI' => 'iflow-cli',
+ 'TRAE_AI_SHELL_ID' => 'trae',
+ 'GOOSE_TERMINAL' => 'goose',
+ 'OZ_RUN_ID' => 'warp',
+ 'FIREBENDER_TERMINAL' => 'firebender',
+ 'GROK_SESSION_ID' => 'grok-build',
+ 'JUNIE_SHIM_PATH' => 'junie',
+ 'REPL_ID' => 'replit',
+ 'COPILOT_MODEL' => 'github-copilot',
+ 'COPILOT_ALLOW_ALL' => 'github-copilot',
+ 'COPILOT_GITHUB_TOKEN' => 'github-copilot',
+ 'COPILOT_CLI' => 'github-copilot',
+ 'ANTIGRAVITY_AGENT' => 'antigravity',
+ 'PI_CODING_AGENT' => 'pi',
+ 'MATTERHORN_SESSION_ID' => 'junie',
+ 'KIRO_AGENT_PATH' => 'kiro',
+ ];
+
+ /**
+ * Known AI_AGENT values and aliases mapped to agent names
+ *
+ * Inspired by https://docs.rs/is-ai-agent
+ */
+ private const KNOWN_AI_AGENT_NAMES = [
+ 'amp' => 'amp',
+ 'amazonq' => 'amazon-q',
+ 'amazon-q' => 'amazon-q',
+ 'amazon-q-cli' => 'amazon-q',
+ 'antigravity' => 'antigravity',
+ 'augment' => 'augment',
+ 'augment-cli' => 'augment',
+ 'claude' => 'claude-code',
+ 'claude-code' => 'claude-code',
+ 'claudecode' => 'claude-code',
+ 'cline' => 'cline',
+ 'codebuddy' => 'codebuddy',
+ 'codex' => 'codex',
+ 'cowork' => 'cowork',
+ 'crush' => 'crush',
+ 'cursor' => 'cursor',
+ 'cursor-cli' => 'cursor-cli',
+ 'deepseek-harness' => 'deepseek-harness',
+ 'dsh' => 'deepseek-harness',
+ 'devin' => 'devin',
+ 'firebender' => 'firebender',
+ 'gemini' => 'gemini-cli',
+ 'gemini-cli' => 'gemini-cli',
+ 'github-copilot' => 'github-copilot',
+ 'github-copilot-cli' => 'github-copilot',
+ 'github_copilot_vscode_agent' => 'github-copilot',
+ 'github_copilot_app_agent' => 'github-copilot',
+ 'goose' => 'goose',
+ 'grok' => 'grok-cli',
+ 'grok-cli' => 'grok-cli',
+ 'grok-build' => 'grok-build',
+ 'hermes' => 'hermes',
+ 'hermes-agent' => 'hermes',
+ 'iflow' => 'iflow-cli',
+ 'iflow-cli' => 'iflow-cli',
+ 'junie' => 'junie',
+ 'kilo' => 'kilo-code',
+ 'kilo-code' => 'kilo-code',
+ 'kilocode' => 'kilo-code',
+ 'kiro' => 'kiro',
+ 'kiro-cli' => 'kiro',
+ 'openclaw' => 'openclaw',
+ 'opencode' => 'opencode',
+ 'openhands' => 'openhands',
+ 'pi' => 'pi',
+ 'qwen' => 'qwen-code',
+ 'qwen-code' => 'qwen-code',
+ 'qwencode' => 'qwen-code',
+ 'replit' => 'replit',
+ 'roo' => 'roo-code',
+ 'roo-code' => 'roo-code',
+ 'roocode' => 'roo-code',
+ 'trae' => 'trae',
+ 'v0' => 'v0',
+ 'vecli' => 'vecli',
+ 'vtcode' => 'vtcode',
+ 'warp' => 'warp',
+ 'oz' => 'warp',
+ ];
+
/** @var ?bool */
private static $isVirtualBoxGuest = null;
/** @var ?bool */
private static $isWindowsSubsystemForLinux = null;
/** @var ?bool */
private static $isDocker = null;
+ /** @var non-empty-string|false|null */
+ private static $aiAgent = null;
/**
* getcwd() equivalent which always returns a string
@@ -141,6 +267,10 @@ public static function putEnv(string $name, string $value): void
{
putenv($name . '=' . $value);
$_SERVER[$name] = $_ENV[$name] = $value;
+
+ if ($name === 'AI_AGENT' || isset(self::CODING_AGENT_ENV_VARS[$name])) {
+ self::$aiAgent = null;
+ }
}
/**
@@ -150,6 +280,10 @@ public static function clearEnv(string $name): void
{
putenv($name);
unset($_SERVER[$name], $_ENV[$name]);
+
+ if ($name === 'AI_AGENT' || isset(self::CODING_AGENT_ENV_VARS[$name])) {
+ self::$aiAgent = null;
+ }
}
/**
@@ -234,6 +368,57 @@ public static function isWindows(): bool
return \defined('PHP_WINDOWS_VERSION_BUILD');
}
+ /**
+ * @return ?non-empty-string The name of the AI coding agent running Composer, 'unknown' if it cannot be identified, or null if run by a human
+ */
+ public static function getAiAgent(): ?string
+ {
+ // the cache is reset by putEnv/clearEnv when AI_AGENT or one of the CODING_AGENT_ENV_VARS is modified
+ if (null !== self::$aiAgent) {
+ return self::$aiAgent === false ? null : self::$aiAgent;
+ }
+
+ // a known AI_AGENT name wins over other markers, an unknown one (sanitized) is only used as fallback
+ $fallback = false;
+ $value = strtolower(trim((string) self::getEnv('AI_AGENT')));
+ if (!in_array($value, ['', '0', 'false', 'no', 'off'], true)) {
+ $name = self::KNOWN_AI_AGENT_NAMES[$value] ?? null;
+ if ($name === null) {
+ // strip anything after a separator (e.g. "codex@1.2" or "claude-code v2") and a trailing version
+ $stripVersion = static function (string $value): string {
+ return Preg::replace('{-?v?\d+(\.\d+)*$}', '', $value);
+ };
+ $value = Preg::replace('{[\s@/:].*$}s', '', $value);
+ // also try the part before an underscore for values like "claude-code_2-1-201_agent"
+ $name = self::KNOWN_AI_AGENT_NAMES[$stripVersion(strtr($value, '_', '-'))]
+ ?? self::KNOWN_AI_AGENT_NAMES[$stripVersion(explode('_', $value)[0])]
+ ?? null;
+ $value = $stripVersion(strtr($value, '_', '-'));
+ }
+ if ($name !== null) {
+ return self::$aiAgent = $name;
+ }
+ $value = substr(trim(Preg::replace(['{[^a-z-]+}', '{-{2,}}'], ['', '-'], $value), '-'), 0, 20);
+ $fallback = $value !== '' ? $value : 'unknown';
+ }
+
+ foreach (self::CODING_AGENT_ENV_VARS as $envVar => $name) {
+ $value = self::getEnv($envVar);
+ if ($value !== false && $value !== '') {
+ return self::$aiAgent = $name;
+ }
+ }
+
+ // devin does not expose an env var but mounts this directory, cannot check it if open_basedir is set
+ if (!(bool) ini_get('open_basedir') && file_exists('/opt/.devin')) {
+ return self::$aiAgent = 'devin';
+ }
+
+ self::$aiAgent = $fallback;
+
+ return $fallback === false ? null : $fallback;
+ }
+
public static function isDocker(): bool
{
if (null !== self::$isDocker) {
@@ -398,4 +583,14 @@ public static function getDevNull(): string
return '/dev/null';
}
+
+ /**
+ * Whether the top-level segment of an archive entry path is OS metadata (resource forks, thumbnail caches, etc.)
+ */
+ public static function isMetadataPath(string $name): bool
+ {
+ $top = explode('/', ltrim(str_replace('\\', '/', $name), '/'))[0];
+
+ return Preg::isMatch(self::METADATA_NAME_REGEX, $top);
+ }
}
diff --git a/core/vendor/composer/composer/src/Composer/Util/ProcessExecutor.php b/core/vendor/composer/composer/src/Composer/Util/ProcessExecutor.php
index acd2b44163..85a7dd6bf9 100644
--- a/core/vendor/composer/composer/src/Composer/Util/ProcessExecutor.php
+++ b/core/vendor/composer/composer/src/Composer/Util/ProcessExecutor.php
@@ -12,6 +12,8 @@
namespace Composer\Util;
+use Composer\Console\ProgramStatus;
+use Composer\IO\ConsoleIO;
use Composer\IO\IOInterface;
use Composer\Pcre\Preg;
use Seld\Signal\SignalHandler;
@@ -175,6 +177,9 @@ function (string $signal) {
} catch (ProcessSignaledException $e) {
if ($signalHandler->isTriggered()) {
// exiting as we were signaled and the child process exited too due to the signal
+ if ($this->io instanceof ConsoleIO) {
+ $this->io->writeProgramStatus(ProgramStatus::idle());
+ }
$signalHandler->exitWithLastSignal();
}
} finally {
@@ -481,8 +486,19 @@ private function outputCommandRun($command, ?string $cwd, bool $async): void
return;
}
+ if (is_array($command)) {
+ foreach ($command as $index => $arg) {
+ if ($arg === '--password' && isset($command[$index + 1])) {
+ $command[$index + 1] = '***';
+ } elseif (str_starts_with($arg, '--password=')) {
+ $command[$index] = '--password=***';
+ }
+ }
+ }
+
$commandString = is_string($command) ? $command : implode(' ', array_map(self::class.'::escape', $command));
$safeCommand = Url::sanitize($commandString);
+ // array commands are redacted above, this covers string commands e.g. from plugins
$safeCommand = Preg::replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand);
$this->io->writeError('Executing'.($async ? ' async' : '').' command ('.($cwd ?: 'CWD').'): '.$safeCommand);
}
diff --git a/core/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php b/core/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php
index d7996ef042..3e83cf38e5 100644
--- a/core/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php
+++ b/core/vendor/composer/composer/src/Composer/Util/RemoteFilesystem.php
@@ -13,13 +13,16 @@
namespace Composer\Util;
use Closure;
+use Composer\CaBundle\CaBundle;
use Composer\Config;
use Composer\Downloader\MaxFileSizeExceededException;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
use Composer\Pcre\Preg;
use Composer\Util\Http\Response;
+use Composer\Util\Http\TlsChainDenylist;
use Composer\Util\Http\ProxyManager;
+use Composer\Util\Http\ResponseWarnings;
/**
* @internal
@@ -65,6 +68,8 @@ class RemoteFilesystem
private $redirects;
/** @var int */
private $maxRedirects = 20;
+ /** @var array hosts which presented a distrusted TLS chain, keyed by host:port */
+ private $distrustedHosts = [];
/**
* Constructor.
@@ -275,6 +280,14 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti
unset($options['max_file_size']);
}
+ $hostKey = TlsChainDenylist::getHostKey($fileUrl);
+ if (null !== $hostKey && isset($this->distrustedHosts[$hostKey])) {
+ throw TlsChainDenylist::createException($fileUrl, $this->distrustedHosts[$hostKey], false);
+ }
+ if (null !== $hostKey) {
+ $options['ssl']['capture_peer_cert_chain'] = true;
+ }
+
$ctx = StreamContextFactory::getContext($fileUrl, $options, ['notification' => Closure::fromCallable([$this, 'callbackGet'])]);
$proxy = ProxyManager::getInstance()->getProxyForRequest($fileUrl);
@@ -306,10 +319,21 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti
try {
$result = $this->getRemoteContents($originUrl, $fileUrl, $ctx, $http_response_header, $maxFileSize);
+ // streams offer no hook between the TLS handshake and sending the request, so this can only be checked afterwards
+ $peerChain = null !== $hostKey ? stream_context_get_options($ctx)['ssl']['peer_certificate_chain'] ?? null : null;
+ if (is_array($peerChain) && null !== ($distrusted = CaBundle::findDistrustedCertificate($peerChain))) {
+ $result = false;
+ $this->distrustedHosts[$hostKey] = $distrusted;
+ $e = TlsChainDenylist::createException($fileUrl, $distrusted, true);
+ // callers may swallow transport errors, but possibly leaked credentials must be reported
+ $this->io->writeError(''.$e->getMessage().' ');
+ throw $e;
+ }
+
if (!empty($http_response_header[0])) {
$statusCode = self::findStatusCode($http_response_header);
if ($statusCode >= 300 && Response::findHeaderValue($http_response_header, 'content-type') === 'application/json') {
- HttpDownloader::outputWarnings($this->io, $originUrl, json_decode($result, true));
+ ResponseWarnings::output($this->io, $originUrl, json_decode($result, true));
}
if (in_array($statusCode, [401, 403]) && $retryAuthFailure) {
diff --git a/core/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php b/core/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php
index 9f4b59c380..2e7d3c66a0 100644
--- a/core/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php
+++ b/core/vendor/composer/composer/src/Composer/Util/StreamContextFactory.php
@@ -124,8 +124,9 @@ public static function initOptions(string $url, array $options, bool $forCurl =
if ($operation !== null && $operation !== $runningCommand) {
$runningCommand = $runningCommand !== null ? $runningCommand.','.$operation : $operation;
}
+ $aiAgent = Platform::getAiAgent();
$options['http']['header'][] = sprintf(
- 'User-Agent: Composer/%s (%s; %s; %s; %s%s%s%s)',
+ 'User-Agent: Composer/%s (%s; %s; %s; %s%s%s%s%s)',
Composer::getVersion(),
function_exists('php_uname') ? php_uname('s') : 'Unknown',
function_exists('php_uname') ? php_uname('r') : 'Unknown',
@@ -133,6 +134,7 @@ function_exists('php_uname') ? php_uname('r') : 'Unknown',
$httpVersion,
$platformPhpVersion ? '; Platform-PHP '.$platformPhpVersion : '',
Platform::getEnv('CI') ? '; CI' : '',
+ $aiAgent !== null ? '; agent:'.$aiAgent : '',
$runningCommand !== null ? '; cmd:'.$runningCommand : ''
);
}
diff --git a/core/vendor/composer/composer/src/Composer/Util/Tar.php b/core/vendor/composer/composer/src/Composer/Util/Tar.php
index 8b724a83d0..774fabd750 100644
--- a/core/vendor/composer/composer/src/Composer/Util/Tar.php
+++ b/core/vendor/composer/composer/src/Composer/Util/Tar.php
@@ -44,6 +44,10 @@ private static function extractComposerJsonFromFolder(\PharData $phar): string
foreach ($phar as $folderFile) {
$name = $folderFile->getBasename();
+ if (Platform::isMetadataPath($name)) {
+ continue;
+ }
+
if ($folderFile->isDir()) {
$topLevelPaths[$name] = true;
if (\count($topLevelPaths) > 1) {
diff --git a/core/vendor/composer/composer/src/Composer/Util/Url.php b/core/vendor/composer/composer/src/Composer/Util/Url.php
index c6d0706f29..c166944b3b 100644
--- a/core/vendor/composer/composer/src/Composer/Util/Url.php
+++ b/core/vendor/composer/composer/src/Composer/Util/Url.php
@@ -135,7 +135,7 @@ public static function sanitize(string $url): string
// matches every scheme://user:pass@ in the string as URLs are usually embedded in a longer
// message, plus a scheme-less one at the very start for URLs passed in on their own
- $url = Preg::replaceCallback('{(?:(?P[a-z0-9][a-z0-9+.-]*://)|\A)(?P[^:/\s?#]*)(?::(?P[^\s/?#]+))?@}i', static function ($m): string {
+ $url = Preg::replaceCallback('{(?:(?P[a-z0-9][a-z0-9+.-]*://)|\A)(?P[^:/\s?#]*)(?::(?P[^\s/?#]*))?@}i', static function ($m): string {
$user = self::sanitizeUsername($m['user']);
if (($m['password'] ?? '') !== '') {
return $m['prefix'].$user.':***@';
@@ -154,7 +154,13 @@ public static function sanitize(string $url): string
*/
public static function stripCredentials(string $url): string
{
- return Preg::replace('{://[^/\s?#]+@}', '://', $url);
+ return Preg::replaceCallback('{(?P[a-z0-9][a-z0-9+.-]*://)(?P[^:/\s?#]*)(?::(?P[^\s/?#]*))?@}i', static function ($m): string {
+ if (($m['password'] ?? '') === '' && !Preg::isMatch('{^https?://$}i', $m['prefix'])) {
+ return $m['prefix'].$m['user'].'@';
+ }
+
+ return $m['prefix'];
+ }, $url);
}
/**
diff --git a/core/vendor/composer/composer/src/Composer/Util/Zip.php b/core/vendor/composer/composer/src/Composer/Util/Zip.php
index 9fd8f07857..08c6f58f06 100644
--- a/core/vendor/composer/composer/src/Composer/Util/Zip.php
+++ b/core/vendor/composer/composer/src/Composer/Util/Zip.php
@@ -67,10 +67,13 @@ private static function locateFile(\ZipArchive $zip, string $filename): int
$topLevelPaths = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
+ if (false === $name) {
+ continue;
+ }
$dirname = dirname($name);
- // ignore OSX specific resource fork folder
- if (strpos($name, '__MACOSX') !== false) {
+ // ignore well-known OS/editor metadata entries (resource forks, thumbnail caches, etc.)
+ if (Platform::isMetadataPath($name)) {
continue;
}
diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json
index 4edf62d6cc..87f3e93136 100644
--- a/core/vendor/composer/installed.json
+++ b/core/vendor/composer/installed.json
@@ -226,17 +226,17 @@
},
{
"name": "composer/ca-bundle",
- "version": "1.6.0",
- "version_normalized": "1.6.0.0",
+ "version": "1.6.1",
+ "version_normalized": "1.6.1.0",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "2c99cf2e92516985dea4914812b93d9daf01cdd6"
+ "reference": "0c64479fae6952ba4cf021a72c0ac530394c91d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/2c99cf2e92516985dea4914812b93d9daf01cdd6",
- "reference": "2c99cf2e92516985dea4914812b93d9daf01cdd6",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c64479fae6952ba4cf021a72c0ac530394c91d4",
+ "reference": "0c64479fae6952ba4cf021a72c0ac530394c91d4",
"shasum": ""
},
"require": {
@@ -250,7 +250,7 @@
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
- "time": "2026-10-20T20:14:05+00:00",
+ "time": "2026-10-29T14:10:42+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -285,7 +285,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
- "source": "https://github.com/composer/ca-bundle/tree/1.6.0"
+ "source": "https://github.com/composer/ca-bundle/tree/1.6.1"
},
"funding": [
{
@@ -373,22 +373,22 @@
},
{
"name": "composer/composer",
- "version": "2.10.3",
- "version_normalized": "2.10.3.0",
+ "version": "2.11.0",
+ "version_normalized": "2.11.0.0",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "f0de0bf90226853b841672f086d8b58b02332504"
+ "reference": "e52c65bed1de6aa272cdde4b7725ca6653d7b06a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/f0de0bf90226853b841672f086d8b58b02332504",
- "reference": "f0de0bf90226853b841672f086d8b58b02332504",
+ "url": "https://api.github.com/repos/composer/composer/zipball/e52c65bed1de6aa272cdde4b7725ca6653d7b06a",
+ "reference": "e52c65bed1de6aa272cdde4b7725ca6653d7b06a",
"shasum": ""
},
"require": {
- "composer/ca-bundle": "^1.5",
- "composer/class-map-generator": "^1.4.0",
+ "composer/ca-bundle": "^1.6",
+ "composer/class-map-generator": "^1.8.0",
"composer/metadata-minifier": "^1.0",
"composer/pcre": "^2.3 || ^3.3",
"composer/semver": "^3.3",
@@ -397,7 +397,7 @@
"ext-filter": "*",
"ext-hash": "*",
"ext-json": "*",
- "justinrainbow/json-schema": "^6.5.1",
+ "justinrainbow/json-schema": "^6.6.3",
"php": "^7.2.5 || ^8.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"react/promise": "^3.3",
@@ -414,7 +414,7 @@
"symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0"
},
"require-dev": {
- "phpstan/phpstan": "^1.11.8",
+ "phpstan/phpstan": "1.12.33",
"phpstan/phpstan-deprecation-rules": "^1.2.0",
"phpstan/phpstan-phpunit": "^1.4.0",
"phpstan/phpstan-strict-rules": "^1.6.0",
@@ -427,7 +427,7 @@
"ext-zip": "Allows direct extraction of ZIP archives (unzip/7z binaries will be used instead if available)",
"ext-zlib": "Enables gzip for HTTP requests"
},
- "time": "2026-08-27T11:34:23+00:00",
+ "time": "2026-10-29T09:23:20+00:00",
"bin": [
"bin/composer"
],
@@ -439,7 +439,7 @@
]
},
"branch-alias": {
- "dev-main": "2.10-dev"
+ "dev-main": "2.11-dev"
}
},
"installation-source": "dist",
@@ -475,7 +475,7 @@
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/composer/issues",
"security": "https://github.com/composer/composer/security/policy",
- "source": "https://github.com/composer/composer/tree/2.10.3"
+ "source": "https://github.com/composer/composer/tree/2.11.0"
},
"funding": [
{
@@ -638,17 +638,17 @@
},
{
"name": "composer/semver",
- "version": "3.5.1",
- "version_normalized": "3.5.1.0",
+ "version": "3.6.0",
+ "version_normalized": "3.6.0.0",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "528948210cc46f967a4b98e83b19e76f2b504993"
+ "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/528948210cc46f967a4b98e83b19e76f2b504993",
- "reference": "528948210cc46f967a4b98e83b19e76f2b504993",
+ "url": "https://api.github.com/repos/composer/semver/zipball/d92543b7ede9c4ec6b59853cefb061e39866d2cf",
+ "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf",
"shasum": ""
},
"require": {
@@ -658,7 +658,7 @@
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
- "time": "2026-10-05T13:12:12+00:00",
+ "time": "2026-10-29T09:13:34+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -702,7 +702,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
- "source": "https://github.com/composer/semver/tree/3.5.1"
+ "source": "https://github.com/composer/semver/tree/3.6.0"
},
"funding": [
{
@@ -1120,30 +1120,30 @@
},
{
"name": "doctrine/inflector",
- "version": "2.1.0",
- "version_normalized": "2.1.0.0",
+ "version": "2.1.1",
+ "version_normalized": "2.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
+ "reference": "46351b6812b7b6d52b82c3efa5dfb786a198ec88"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
- "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/46351b6812b7b6d52b82c3efa5dfb786a198ec88",
+ "reference": "46351b6812b7b6d52b82c3efa5dfb786a198ec88",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^12.0 || ^13.0",
+ "doctrine/coding-standard": "^12.0 || ^14.0",
"phpstan/phpstan": "^1.12 || ^2.0",
"phpstan/phpstan-phpunit": "^1.4 || ^2.0",
"phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
"phpunit/phpunit": "^8.5 || ^12.2"
},
- "time": "2025-08-10T19:31:58+00:00",
+ "time": "2026-10-26T08:14:14+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1193,7 +1193,7 @@
],
"support": {
"issues": "https://github.com/doctrine/inflector/issues",
- "source": "https://github.com/doctrine/inflector/tree/2.1.0"
+ "source": "https://github.com/doctrine/inflector/tree/2.1.1"
},
"funding": [
{
@@ -1645,17 +1645,17 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.16.0",
- "version_normalized": "7.16.0.0",
+ "version": "7.16.1",
+ "version_normalized": "7.16.1.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "ffb62ec787050ae4fa9449cdd6b80335db69bda2"
+ "reference": "81755d989b3007ca7f88a069857553cd08e7dc2e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ffb62ec787050ae4fa9449cdd6b80335db69bda2",
- "reference": "ffb62ec787050ae4fa9449cdd6b80335db69bda2",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/81755d989b3007ca7f88a069857553cd08e7dc2e",
+ "reference": "81755d989b3007ca7f88a069857553cd08e7dc2e",
"shasum": ""
},
"require": {
@@ -1684,7 +1684,7 @@
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
- "time": "2026-10-18T13:38:36+00:00",
+ "time": "2026-10-25T21:35:21+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
@@ -1756,7 +1756,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.16.0"
+ "source": "https://github.com/guzzle/guzzle/tree/7.16.1"
},
"funding": [
{
@@ -2074,8 +2074,8 @@
},
{
"name": "illuminate/bus",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/bus.git",
@@ -2130,8 +2130,8 @@
},
{
"name": "illuminate/cache",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/cache.git",
@@ -2195,8 +2195,8 @@
},
{
"name": "illuminate/collections",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
@@ -2258,8 +2258,8 @@
},
{
"name": "illuminate/conditionable",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@@ -2307,8 +2307,8 @@
},
{
"name": "illuminate/config",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/config.git",
@@ -2358,8 +2358,8 @@
},
{
"name": "illuminate/console",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/console.git",
@@ -2427,8 +2427,8 @@
},
{
"name": "illuminate/container",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
@@ -2492,8 +2492,8 @@
},
{
"name": "illuminate/contracts",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -2543,8 +2543,8 @@
},
{
"name": "illuminate/database",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
@@ -2619,8 +2619,8 @@
},
{
"name": "illuminate/events",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@@ -2677,8 +2677,8 @@
},
{
"name": "illuminate/filesystem",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
@@ -2747,8 +2747,8 @@
},
{
"name": "illuminate/http",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/http.git",
@@ -2812,8 +2812,8 @@
},
{
"name": "illuminate/log",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/log.git",
@@ -2868,8 +2868,8 @@
},
{
"name": "illuminate/macroable",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@@ -2917,8 +2917,8 @@
},
{
"name": "illuminate/pagination",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pagination.git",
@@ -2970,8 +2970,8 @@
},
{
"name": "illuminate/pipeline",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pipeline.git",
@@ -3025,8 +3025,8 @@
},
{
"name": "illuminate/queue",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/queue.git",
@@ -3095,8 +3095,8 @@
},
{
"name": "illuminate/redis",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/redis.git",
@@ -3152,8 +3152,8 @@
},
{
"name": "illuminate/reflection",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/reflection.git",
@@ -3206,8 +3206,8 @@
},
{
"name": "illuminate/routing",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/routing.git",
@@ -3275,8 +3275,8 @@
},
{
"name": "illuminate/session",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/session.git",
@@ -3335,17 +3335,17 @@
},
{
"name": "illuminate/support",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
- "reference": "9e9ff99fc4405664faa02f231a25aaf968c6f02b"
+ "reference": "6427b436b592d05050b506930566f47f85ecebf0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/support/zipball/9e9ff99fc4405664faa02f231a25aaf968c6f02b",
- "reference": "9e9ff99fc4405664faa02f231a25aaf968c6f02b",
+ "url": "https://api.github.com/repos/illuminate/support/zipball/6427b436b592d05050b506930566f47f85ecebf0",
+ "reference": "6427b436b592d05050b506930566f47f85ecebf0",
"shasum": ""
},
"require": {
@@ -3381,7 +3381,7 @@
"symfony/var-dumper": "Required to use the dd function (^7.2).",
"vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)."
},
- "time": "2026-10-06T13:17:38+00:00",
+ "time": "2026-10-24T14:38:23+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -3418,8 +3418,8 @@
},
{
"name": "illuminate/translation",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/translation.git",
@@ -3472,8 +3472,8 @@
},
{
"name": "illuminate/validation",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/validation.git",
@@ -3538,8 +3538,8 @@
},
{
"name": "illuminate/view",
- "version": "v12.70.0",
- "version_normalized": "12.70.0.0",
+ "version": "v12.70.2",
+ "version_normalized": "12.70.2.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
@@ -3655,17 +3655,17 @@
},
{
"name": "justinrainbow/json-schema",
- "version": "6.13.1",
- "version_normalized": "6.13.1.0",
+ "version": "6.14.0",
+ "version_normalized": "6.14.0.0",
"source": {
"type": "git",
"url": "https://github.com/jsonrainbow/json-schema.git",
- "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6"
+ "reference": "bae415e135dd68260c2d020ba392850231e9b1ab"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/e54aacc4c0657b3a4af4f628428d2702146828c6",
- "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6",
+ "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/bae415e135dd68260c2d020ba392850231e9b1ab",
+ "reference": "bae415e135dd68260c2d020ba392850231e9b1ab",
"shasum": ""
},
"require": {
@@ -3682,7 +3682,7 @@
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^8.5"
},
- "time": "2026-09-30T07:42:28+00:00",
+ "time": "2026-10-24T08:53:34+00:00",
"bin": [
"bin/validate-json"
],
@@ -3717,7 +3717,7 @@
],
"support": {
"issues": "https://github.com/jsonrainbow/json-schema/issues",
- "source": "https://github.com/jsonrainbow/json-schema/tree/6.13.1"
+ "source": "https://github.com/jsonrainbow/json-schema/tree/6.14.0"
},
"install-path": "../justinrainbow/json-schema"
},
@@ -4229,17 +4229,17 @@
},
{
"name": "nesbot/carbon",
- "version": "3.15.0",
- "version_normalized": "3.15.0.0",
+ "version": "3.15.1",
+ "version_normalized": "3.15.1.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "217d38553209bdb44e01590d9e9a2e4df8354cce"
+ "reference": "ce0e5636fae7d1e99856eb70cae4c719a284aa08"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/217d38553209bdb44e01590d9e9a2e4df8354cce",
- "reference": "217d38553209bdb44e01590d9e9a2e4df8354cce",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ce0e5636fae7d1e99856eb70cae4c719a284aa08",
+ "reference": "ce0e5636fae7d1e99856eb70cae4c719a284aa08",
"shasum": ""
},
"require": {
@@ -4266,7 +4266,7 @@
"phpunit/phpunit": "^10.5.53",
"squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0"
},
- "time": "2026-10-18T18:27:53+00:00",
+ "time": "2026-10-25T14:32:48+00:00",
"bin": [
"bin/carbon"
],
@@ -6237,17 +6237,17 @@
},
{
"name": "symfony/console",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "21f1bc2296f7ff6043241a8dcbc09559b3732820"
+ "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/21f1bc2296f7ff6043241a8dcbc09559b3732820",
- "reference": "21f1bc2296f7ff6043241a8dcbc09559b3732820",
+ "url": "https://api.github.com/repos/symfony/console/zipball/8012f842110b414fd1cc641adb0ed8423353b0bb",
+ "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb",
"shasum": ""
},
"require": {
@@ -6280,7 +6280,7 @@
"symfony/stopwatch": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0"
},
- "time": "2026-10-12T18:45:38+00:00",
+ "time": "2026-10-28T11:08:27+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -6314,7 +6314,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.21"
+ "source": "https://github.com/symfony/console/tree/v7.4.22"
},
"funding": [
{
@@ -6412,17 +6412,17 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.4.20",
- "version_normalized": "7.4.20.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0"
+ "reference": "545852cfe9c4979c82389155b4d8d05f8864e4f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0",
- "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/545852cfe9c4979c82389155b4d8d05f8864e4f1",
+ "reference": "545852cfe9c4979c82389155b4d8d05f8864e4f1",
"shasum": ""
},
"require": {
@@ -6442,7 +6442,7 @@
"symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/webpack-encore-bundle": "^1.0|^2.0"
},
- "time": "2026-09-22T18:56:40+00:00",
+ "time": "2026-10-25T14:40:16+00:00",
"bin": [
"Resources/bin/patch-type-declarations"
],
@@ -6473,7 +6473,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.4.20"
+ "source": "https://github.com/symfony/error-handler/tree/v7.4.22"
},
"funding": [
{
@@ -6741,17 +6741,17 @@
},
{
"name": "symfony/finder",
- "version": "v7.4.20",
- "version_normalized": "7.4.20.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e"
+ "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e",
- "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/e13420a8e0a76bdde1e7be09f923e62ad13739f2",
+ "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2",
"shasum": ""
},
"require": {
@@ -6760,7 +6760,7 @@
"require-dev": {
"symfony/filesystem": "^6.4|^7.0|^8.0"
},
- "time": "2026-09-26T06:31:10+00:00",
+ "time": "2026-10-19T16:56:54+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -6788,7 +6788,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.4.20"
+ "source": "https://github.com/symfony/finder/tree/v7.4.22"
},
"funding": [
{
@@ -6812,17 +6812,17 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "58c89144c7a36dd89c3d01917e9fa3a72d8aaf31"
+ "reference": "63eda1d0c4de229d6b3e825709e49dd5c149699a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/58c89144c7a36dd89c3d01917e9fa3a72d8aaf31",
- "reference": "58c89144c7a36dd89c3d01917e9fa3a72d8aaf31",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/63eda1d0c4de229d6b3e825709e49dd5c149699a",
+ "reference": "63eda1d0c4de229d6b3e825709e49dd5c149699a",
"shasum": ""
},
"require": {
@@ -6845,7 +6845,7 @@
"symfony/mime": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^6.4|^7.0|^8.0"
},
- "time": "2026-10-08T16:56:12+00:00",
+ "time": "2026-10-27T13:26:03+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -6873,7 +6873,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.4.21"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.22"
},
"funding": [
{
@@ -6897,17 +6897,17 @@
},
{
"name": "symfony/http-kernel",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "4779fce7dedacb8801b2b61c703ef99159502331"
+ "reference": "87e727b33983238d3f2ea50f9f62265499ca1a6d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4779fce7dedacb8801b2b61c703ef99159502331",
- "reference": "4779fce7dedacb8801b2b61c703ef99159502331",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/87e727b33983238d3f2ea50f9f62265499ca1a6d",
+ "reference": "87e727b33983238d3f2ea50f9f62265499ca1a6d",
"shasum": ""
},
"require": {
@@ -6967,7 +6967,7 @@
"symfony/var-exporter": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12|^4.0"
},
- "time": "2026-10-14T06:44:08+00:00",
+ "time": "2026-10-28T18:18:28+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -6995,7 +6995,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.4.21"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.4.22"
},
"funding": [
{
@@ -7019,17 +7019,17 @@
},
{
"name": "symfony/mime",
- "version": "v7.4.19",
- "version_normalized": "7.4.19.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693"
+ "reference": "94a4c017435bc515f5ac80421a2c555fb9ca3676"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693",
- "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/94a4c017435bc515f5ac80421a2c555fb9ca3676",
+ "reference": "94a4c017435bc515f5ac80421a2c555fb9ca3676",
"shasum": ""
},
"require": {
@@ -7055,7 +7055,7 @@
"symfony/property-info": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4.44|^7.4.17|^8.1.5"
},
- "time": "2026-09-04T10:45:44+00:00",
+ "time": "2026-10-28T17:57:58+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -7087,7 +7087,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.4.19"
+ "source": "https://github.com/symfony/mime/tree/v7.4.22"
},
"funding": [
{
@@ -7197,17 +7197,17 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.43.0",
- "version_normalized": "1.43.0.0",
+ "version": "v1.43.3",
+ "version_normalized": "1.43.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "5cac8f4db97208823aa353f59a01faad2da52c75"
+ "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75",
- "reference": "5cac8f4db97208823aa353f59a01faad2da52c75",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5a8f33c87aeecffce24e721b5064fab05f4becf8",
+ "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8",
"shasum": ""
},
"require": {
@@ -7216,7 +7216,7 @@
"suggest": {
"ext-intl": "For best performance"
},
- "time": "2026-09-25T15:46:47+00:00",
+ "time": "2026-10-15T09:18:53+00:00",
"type": "library",
"extra": {
"thanks": {
@@ -7258,7 +7258,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.3"
},
"funding": [
{
@@ -7460,17 +7460,17 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.43.0",
- "version_normalized": "1.43.0.0",
+ "version": "v1.43.3",
+ "version_normalized": "1.43.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72"
+ "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72",
- "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a",
+ "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a",
"shasum": ""
},
"require": {
@@ -7483,7 +7483,7 @@
"suggest": {
"ext-mbstring": "For best performance"
},
- "time": "2026-09-24T15:19:04+00:00",
+ "time": "2026-10-28T09:35:49+00:00",
"type": "library",
"extra": {
"thanks": {
@@ -7524,7 +7524,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.3"
},
"funding": [
{
@@ -7800,23 +7800,23 @@
},
{
"name": "symfony/process",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "e5247f4a5e0797d232ad8e8e6803aeb341843c83"
+ "reference": "a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/e5247f4a5e0797d232ad8e8e6803aeb341843c83",
- "reference": "e5247f4a5e0797d232ad8e8e6803aeb341843c83",
+ "url": "https://api.github.com/repos/symfony/process/zipball/a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a",
+ "reference": "a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
- "time": "2026-10-08T16:56:12+00:00",
+ "time": "2026-10-25T14:40:16+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -7844,7 +7844,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.4.21"
+ "source": "https://github.com/symfony/process/tree/v7.4.22"
},
"funding": [
{
@@ -7868,17 +7868,17 @@
},
{
"name": "symfony/routing",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "7a344cf871532d081875bfdd18f3dff8dca36c5b"
+ "reference": "f79ed6fbf60d9e32d358054f1915295f3b8a6f7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/7a344cf871532d081875bfdd18f3dff8dca36c5b",
- "reference": "7a344cf871532d081875bfdd18f3dff8dca36c5b",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/f79ed6fbf60d9e32d358054f1915295f3b8a6f7a",
+ "reference": "f79ed6fbf60d9e32d358054f1915295f3b8a6f7a",
"shasum": ""
},
"require": {
@@ -7898,7 +7898,7 @@
"symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
- "time": "2026-10-11T18:40:59+00:00",
+ "time": "2026-10-26T21:52:50+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -7932,7 +7932,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.4.21"
+ "source": "https://github.com/symfony/routing/tree/v7.4.22"
},
"funding": [
{
@@ -8046,17 +8046,17 @@
},
{
"name": "symfony/string",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "9bc35d074ad16a58c21cd52b18c5cacc4f4c3981"
+ "reference": "8dff7da3a56638f1d189c5368183294102de4ced"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/9bc35d074ad16a58c21cd52b18c5cacc4f4c3981",
- "reference": "9bc35d074ad16a58c21cd52b18c5cacc4f4c3981",
+ "url": "https://api.github.com/repos/symfony/string/zipball/8dff7da3a56638f1d189c5368183294102de4ced",
+ "reference": "8dff7da3a56638f1d189c5368183294102de4ced",
"shasum": ""
},
"require": {
@@ -8077,7 +8077,7 @@
"symfony/translation-contracts": "^2.5|^3.0",
"symfony/var-exporter": "^6.4|^7.0|^8.0"
},
- "time": "2026-10-03T15:15:30+00:00",
+ "time": "2026-10-23T16:51:52+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -8116,7 +8116,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.4.21"
+ "source": "https://github.com/symfony/string/tree/v7.4.22"
},
"funding": [
{
@@ -8140,17 +8140,17 @@
},
{
"name": "symfony/translation",
- "version": "v7.4.17",
- "version_normalized": "7.4.17.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b"
+ "reference": "a892cd1807bfaf0a10690359c47fdf95b162ef22"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b",
- "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/a892cd1807bfaf0a10690359c47fdf95b162ef22",
+ "reference": "a892cd1807bfaf0a10690359c47fdf95b162ef22",
"shasum": ""
},
"require": {
@@ -8188,7 +8188,7 @@
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0|^8.0"
},
- "time": "2026-08-21T17:40:08+00:00",
+ "time": "2026-10-25T14:40:16+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -8219,7 +8219,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.4.17"
+ "source": "https://github.com/symfony/translation/tree/v7.4.22"
},
"funding": [
{
@@ -8328,17 +8328,17 @@
},
{
"name": "symfony/var-dumper",
- "version": "v7.4.21",
- "version_normalized": "7.4.21.0",
+ "version": "v7.4.22",
+ "version_normalized": "7.4.22.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6"
+ "reference": "6e6ff33ea8cf5df27f00a5561ebd5ee9bd7f1618"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/55d06b02f49eca9bff61828548988e6f8763e8a6",
- "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e6ff33ea8cf5df27f00a5561ebd5ee9bd7f1618",
+ "reference": "6e6ff33ea8cf5df27f00a5561ebd5ee9bd7f1618",
"shasum": ""
},
"require": {
@@ -8356,7 +8356,7 @@
"symfony/uid": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12|^4.0"
},
- "time": "2026-10-13T05:48:33+00:00",
+ "time": "2026-10-25T21:04:04+00:00",
"bin": [
"Resources/bin/var-dump-server"
],
@@ -8394,7 +8394,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.4.21"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.22"
},
"funding": [
{
@@ -8418,23 +8418,23 @@
},
{
"name": "tracy/tracy",
- "version": "v2.12.1",
- "version_normalized": "2.12.1.0",
+ "version": "v2.12.2",
+ "version_normalized": "2.12.2.0",
"source": {
"type": "git",
"url": "https://github.com/nette/tracy.git",
- "reference": "90d24872cd97202f79281aac7eb09c27ec48bf69"
+ "reference": "2664bd9456cb745ff1fb0b31efa79fe2f411bef3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/tracy/zipball/90d24872cd97202f79281aac7eb09c27ec48bf69",
- "reference": "90d24872cd97202f79281aac7eb09c27ec48bf69",
+ "url": "https://api.github.com/repos/nette/tracy/zipball/2664bd9456cb745ff1fb0b31efa79fe2f411bef3",
+ "reference": "2664bd9456cb745ff1fb0b31efa79fe2f411bef3",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-session": "*",
- "php": "8.2 - 8.5"
+ "php": "8.2 - 8.6"
},
"conflict": {
"nette/di": "<3.0"
@@ -8448,10 +8448,10 @@
"nette/tester": "^2.6",
"nette/utils": "^3.0 || ^4.0",
"phpstan/extension-installer": "^1.4@stable",
- "phpstan/phpstan": "^2.1@stable",
+ "phpstan/phpstan": "^2.3@stable",
"psr/log": "^1.0 || ^2.0 || ^3.0"
},
- "time": "2026-08-18T10:56:11+00:00",
+ "time": "2026-10-21T05:14:18+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -8478,40 +8478,41 @@
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
- },
- {
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
}
],
- "description": "😎 Tracy: the addictive tool to ease debugging PHP code for cool developers. Friendly design, logging, profiler, advanced features like debugging AJAX calls or CLI support. You will love it.",
+ "description": "😎 Tracy: the addictive PHP debugger. Clear error pages with full context, a debug bar, readable dumps, AJAX and CLI support, and logging with e-mail alerts in production.",
"homepage": "https://tracy.nette.org",
"keywords": [
- "Xdebug",
- "debug",
+ "debug bar",
"debugger",
+ "debugging",
+ "dump",
+ "error handler",
+ "logging",
"nette",
- "profiler"
+ "tracy",
+ "var_dump",
+ "whoops"
],
"support": {
"issues": "https://github.com/nette/tracy/issues",
- "source": "https://github.com/nette/tracy/tree/v2.12.1"
+ "source": "https://github.com/nette/tracy/tree/v2.12.2"
},
"install-path": "../tracy/tracy"
},
{
"name": "vlucas/phpdotenv",
- "version": "v5.7.0",
- "version_normalized": "5.7.0.0",
+ "version": "v5.8.0",
+ "version_normalized": "5.8.0.0",
"source": {
"type": "git",
- "url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "301c07936b16d88628b126b01d082ba153cf4c40"
+ "url": "https://github.com/phpdotenv/phpdotenv.git",
+ "reference": "96149103e62ac67e915f6d2ef59b72dee2e011e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40",
- "reference": "301c07936b16d88628b126b01d082ba153cf4c40",
+ "url": "https://api.github.com/repos/phpdotenv/phpdotenv/zipball/96149103e62ac67e915f6d2ef59b72dee2e011e0",
+ "reference": "96149103e62ac67e915f6d2ef59b72dee2e011e0",
"shasum": ""
},
"require": {
@@ -8531,7 +8532,7 @@
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
- "time": "2026-08-24T18:07:49+00:00",
+ "time": "2026-10-25T16:26:19+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
@@ -8539,7 +8540,7 @@
"forward-command": false
},
"branch-alias": {
- "dev-master": "5.6-dev"
+ "dev-master": "5.8-dev"
}
},
"installation-source": "dist",
@@ -8571,8 +8572,8 @@
"environment"
],
"support": {
- "issues": "https://github.com/vlucas/phpdotenv/issues",
- "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0"
+ "issues": "https://github.com/phpdotenv/phpdotenv/issues",
+ "source": "https://github.com/phpdotenv/phpdotenv/tree/v5.8.0"
},
"funding": [
{
diff --git a/core/vendor/composer/installed.php b/core/vendor/composer/installed.php
index b5f6f7beeb..69b8a6b785 100644
--- a/core/vendor/composer/installed.php
+++ b/core/vendor/composer/installed.php
@@ -38,9 +38,9 @@
'dev_requirement' => false,
),
'composer/ca-bundle' => array(
- 'pretty_version' => '1.6.0',
- 'version' => '1.6.0.0',
- 'reference' => '2c99cf2e92516985dea4914812b93d9daf01cdd6',
+ 'pretty_version' => '1.6.1',
+ 'version' => '1.6.1.0',
+ 'reference' => '0c64479fae6952ba4cf021a72c0ac530394c91d4',
'type' => 'library',
'install_path' => __DIR__ . '/./ca-bundle',
'aliases' => array(),
@@ -56,9 +56,9 @@
'dev_requirement' => false,
),
'composer/composer' => array(
- 'pretty_version' => '2.10.3',
- 'version' => '2.10.3.0',
- 'reference' => 'f0de0bf90226853b841672f086d8b58b02332504',
+ 'pretty_version' => '2.11.0',
+ 'version' => '2.11.0.0',
+ 'reference' => 'e52c65bed1de6aa272cdde4b7725ca6653d7b06a',
'type' => 'library',
'install_path' => __DIR__ . '/./composer',
'aliases' => array(),
@@ -83,9 +83,9 @@
'dev_requirement' => false,
),
'composer/semver' => array(
- 'pretty_version' => '3.5.1',
- 'version' => '3.5.1.0',
- 'reference' => '528948210cc46f967a4b98e83b19e76f2b504993',
+ 'pretty_version' => '3.6.0',
+ 'version' => '3.6.0.0',
+ 'reference' => 'd92543b7ede9c4ec6b59853cefb061e39866d2cf',
'type' => 'library',
'install_path' => __DIR__ . '/./semver',
'aliases' => array(),
@@ -146,9 +146,9 @@
'dev_requirement' => false,
),
'doctrine/inflector' => array(
- 'pretty_version' => '2.1.0',
- 'version' => '2.1.0.0',
- 'reference' => '6d6c96277ea252fc1304627204c3d5e6e15faa3b',
+ 'pretty_version' => '2.1.1',
+ 'version' => '2.1.1.0',
+ 'reference' => '46351b6812b7b6d52b82c3efa5dfb786a198ec88',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/inflector',
'aliases' => array(),
@@ -227,9 +227,9 @@
'dev_requirement' => false,
),
'guzzlehttp/guzzle' => array(
- 'pretty_version' => '7.16.0',
- 'version' => '7.16.0.0',
- 'reference' => 'ffb62ec787050ae4fa9449cdd6b80335db69bda2',
+ 'pretty_version' => '7.16.1',
+ 'version' => '7.16.1.0',
+ 'reference' => '81755d989b3007ca7f88a069857553cd08e7dc2e',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
'aliases' => array(),
@@ -263,8 +263,8 @@
'dev_requirement' => false,
),
'illuminate/bus' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '9e70f409c744e0ba6b301fa608bb5b3c8c38669a',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/bus',
@@ -272,8 +272,8 @@
'dev_requirement' => false,
),
'illuminate/cache' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '2689356ab45bc53ffe796d1b1241babae59c69f6',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/cache',
@@ -281,8 +281,8 @@
'dev_requirement' => false,
),
'illuminate/collections' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '83313b009c4afb6f02dbc090bdb67809756eefa2',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/collections',
@@ -290,8 +290,8 @@
'dev_requirement' => false,
),
'illuminate/conditionable' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'ec677967c1f2faf90b8428919124d2184a4c9b49',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/conditionable',
@@ -299,8 +299,8 @@
'dev_requirement' => false,
),
'illuminate/config' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '7adbf5cc27081d4613e1fa2f4f53e2a4fc91ad53',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/config',
@@ -308,8 +308,8 @@
'dev_requirement' => false,
),
'illuminate/console' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '29f41bd7e07c14c04cb5acfaf9dc103adf2084aa',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/console',
@@ -317,8 +317,8 @@
'dev_requirement' => false,
),
'illuminate/container' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '9abe9dba6b3f5220205ce143069c975a601e4294',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/container',
@@ -326,8 +326,8 @@
'dev_requirement' => false,
),
'illuminate/contracts' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'c16fd7ba7d8e6b8f336639ceb6b7e1ff6ed0efb5',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/contracts',
@@ -335,8 +335,8 @@
'dev_requirement' => false,
),
'illuminate/database' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '24033c9216144db0d05470015007535f035688ce',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/database',
@@ -344,8 +344,8 @@
'dev_requirement' => false,
),
'illuminate/events' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'ff22aa6017bdbd90ace660f50638ed9c580ba555',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/events',
@@ -353,8 +353,8 @@
'dev_requirement' => false,
),
'illuminate/filesystem' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '4e2bff4d2ac439db7a2903b8a99bc8dc5c8d5de4',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/filesystem',
@@ -362,8 +362,8 @@
'dev_requirement' => false,
),
'illuminate/http' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'a91f70083082bd6d466ea229dbbdfc48c553621c',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/http',
@@ -371,8 +371,8 @@
'dev_requirement' => false,
),
'illuminate/log' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '0309c15e63d07c05218b622caa514f3ed6b226a6',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/log',
@@ -380,8 +380,8 @@
'dev_requirement' => false,
),
'illuminate/macroable' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'e295d62d89dcdb87e2b1bd70dd14d074a0ed73cc',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/macroable',
@@ -389,8 +389,8 @@
'dev_requirement' => false,
),
'illuminate/pagination' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '8327d828676654053906a771abf5eea5426354ec',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/pagination',
@@ -398,8 +398,8 @@
'dev_requirement' => false,
),
'illuminate/pipeline' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'b6a14c20d69a44bf0a6fba664a00d23ca71770ee',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/pipeline',
@@ -407,8 +407,8 @@
'dev_requirement' => false,
),
'illuminate/queue' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '6d6d4da3c6a1bb02f6ac16e64eb3a22bb58c1c32',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/queue',
@@ -416,8 +416,8 @@
'dev_requirement' => false,
),
'illuminate/redis' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'e46d4c3806d78d4d7733cd9b9bfb2fef30f4c8cb',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/redis',
@@ -425,8 +425,8 @@
'dev_requirement' => false,
),
'illuminate/reflection' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '348cf5da9de89b596d7723be6425fb048e2bf4bb',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/reflection',
@@ -434,8 +434,8 @@
'dev_requirement' => false,
),
'illuminate/routing' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '56b1a062bfd5153627e0d3bd328e7c8eafc37472',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/routing',
@@ -443,8 +443,8 @@
'dev_requirement' => false,
),
'illuminate/session' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/session',
@@ -452,17 +452,17 @@
'dev_requirement' => false,
),
'illuminate/support' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
- 'reference' => '9e9ff99fc4405664faa02f231a25aaf968c6f02b',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
+ 'reference' => '6427b436b592d05050b506930566f47f85ecebf0',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/support',
'aliases' => array(),
'dev_requirement' => false,
),
'illuminate/translation' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => '431ac000adf17a024d6ef3a8131c3c1fa950459f',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/translation',
@@ -470,8 +470,8 @@
'dev_requirement' => false,
),
'illuminate/validation' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'ce2bb8d019e2510d1892b0c87f9086ea802c28e4',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/validation',
@@ -479,8 +479,8 @@
'dev_requirement' => false,
),
'illuminate/view' => array(
- 'pretty_version' => 'v12.70.0',
- 'version' => '12.70.0.0',
+ 'pretty_version' => 'v12.70.2',
+ 'version' => '12.70.2.0',
'reference' => 'ea300ac93680690580927fda447fdd883034f263',
'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/view',
@@ -497,9 +497,9 @@
'dev_requirement' => false,
),
'justinrainbow/json-schema' => array(
- 'pretty_version' => '6.13.1',
- 'version' => '6.13.1.0',
- 'reference' => 'e54aacc4c0657b3a4af4f628428d2702146828c6',
+ 'pretty_version' => '6.14.0',
+ 'version' => '6.14.0.0',
+ 'reference' => 'bae415e135dd68260c2d020ba392850231e9b1ab',
'type' => 'library',
'install_path' => __DIR__ . '/../justinrainbow/json-schema',
'aliases' => array(),
@@ -575,9 +575,9 @@
),
),
'nesbot/carbon' => array(
- 'pretty_version' => '3.15.0',
- 'version' => '3.15.0.0',
- 'reference' => '217d38553209bdb44e01590d9e9a2e4df8354cce',
+ 'pretty_version' => '3.15.1',
+ 'version' => '3.15.1.0',
+ 'reference' => 'ce0e5636fae7d1e99856eb70cae4c719a284aa08',
'type' => 'library',
'install_path' => __DIR__ . '/../nesbot/carbon',
'aliases' => array(),
@@ -897,9 +897,9 @@
'dev_requirement' => false,
),
'symfony/console' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => '21f1bc2296f7ff6043241a8dcbc09559b3732820',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '8012f842110b414fd1cc641adb0ed8423353b0bb',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/console',
'aliases' => array(),
@@ -915,9 +915,9 @@
'dev_requirement' => false,
),
'symfony/error-handler' => array(
- 'pretty_version' => 'v7.4.20',
- 'version' => '7.4.20.0',
- 'reference' => '82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '545852cfe9c4979c82389155b4d8d05f8864e4f1',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/error-handler',
'aliases' => array(),
@@ -957,36 +957,36 @@
'dev_requirement' => false,
),
'symfony/finder' => array(
- 'pretty_version' => 'v7.4.20',
- 'version' => '7.4.20.0',
- 'reference' => '2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => 'e13420a8e0a76bdde1e7be09f923e62ad13739f2',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/finder',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-foundation' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => '58c89144c7a36dd89c3d01917e9fa3a72d8aaf31',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '63eda1d0c4de229d6b3e825709e49dd5c149699a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-foundation',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-kernel' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => '4779fce7dedacb8801b2b61c703ef99159502331',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '87e727b33983238d3f2ea50f9f62265499ca1a6d',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-kernel',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/mime' => array(
- 'pretty_version' => 'v7.4.19',
- 'version' => '7.4.19.0',
- 'reference' => 'f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '94a4c017435bc515f5ac80421a2c555fb9ca3676',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/mime',
'aliases' => array(),
@@ -1002,9 +1002,9 @@
'dev_requirement' => false,
),
'symfony/polyfill-intl-grapheme' => array(
- 'pretty_version' => 'v1.43.0',
- 'version' => '1.43.0.0',
- 'reference' => '5cac8f4db97208823aa353f59a01faad2da52c75',
+ 'pretty_version' => 'v1.43.3',
+ 'version' => '1.43.3.0',
+ 'reference' => '5a8f33c87aeecffce24e721b5064fab05f4becf8',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme',
'aliases' => array(),
@@ -1029,9 +1029,9 @@
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
- 'pretty_version' => 'v1.43.0',
- 'version' => '1.43.0.0',
- 'reference' => '4d5d331bf731e0aedbc6b2f5e61135b7b7996a72',
+ 'pretty_version' => 'v1.43.3',
+ 'version' => '1.43.3.0',
+ 'reference' => '122889ed24dc000165bcbfd77ec412f15cbdfc6a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
@@ -1095,18 +1095,18 @@
'dev_requirement' => false,
),
'symfony/process' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => 'e5247f4a5e0797d232ad8e8e6803aeb341843c83',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => 'a5a6912edbbfc4c98cd080bc4bbe37c8e7eeb20a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/process',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/routing' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => '7a344cf871532d081875bfdd18f3dff8dca36c5b',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => 'f79ed6fbf60d9e32d358054f1915295f3b8a6f7a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/routing',
'aliases' => array(),
@@ -1122,18 +1122,18 @@
'dev_requirement' => false,
),
'symfony/string' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => '9bc35d074ad16a58c21cd52b18c5cacc4f4c3981',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '8dff7da3a56638f1d189c5368183294102de4ced',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/string',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/translation' => array(
- 'pretty_version' => 'v7.4.17',
- 'version' => '7.4.17.0',
- 'reference' => '2ee1e4a3b32a528a642babe041ff7c440b213b4b',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => 'a892cd1807bfaf0a10690359c47fdf95b162ef22',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/translation',
'aliases' => array(),
@@ -1155,27 +1155,27 @@
),
),
'symfony/var-dumper' => array(
- 'pretty_version' => 'v7.4.21',
- 'version' => '7.4.21.0',
- 'reference' => '55d06b02f49eca9bff61828548988e6f8763e8a6',
+ 'pretty_version' => 'v7.4.22',
+ 'version' => '7.4.22.0',
+ 'reference' => '6e6ff33ea8cf5df27f00a5561ebd5ee9bd7f1618',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/var-dumper',
'aliases' => array(),
'dev_requirement' => false,
),
'tracy/tracy' => array(
- 'pretty_version' => 'v2.12.1',
- 'version' => '2.12.1.0',
- 'reference' => '90d24872cd97202f79281aac7eb09c27ec48bf69',
+ 'pretty_version' => 'v2.12.2',
+ 'version' => '2.12.2.0',
+ 'reference' => '2664bd9456cb745ff1fb0b31efa79fe2f411bef3',
'type' => 'library',
'install_path' => __DIR__ . '/../tracy/tracy',
'aliases' => array(),
'dev_requirement' => false,
),
'vlucas/phpdotenv' => array(
- 'pretty_version' => 'v5.7.0',
- 'version' => '5.7.0.0',
- 'reference' => '301c07936b16d88628b126b01d082ba153cf4c40',
+ 'pretty_version' => 'v5.8.0',
+ 'version' => '5.8.0.0',
+ 'reference' => '96149103e62ac67e915f6d2ef59b72dee2e011e0',
'type' => 'library',
'install_path' => __DIR__ . '/../vlucas/phpdotenv',
'aliases' => array(),
diff --git a/core/vendor/composer/semver/CHANGELOG.md b/core/vendor/composer/semver/CHANGELOG.md
index 09526fb31e..2def9cc82a 100644
--- a/core/vendor/composer/semver/CHANGELOG.md
+++ b/core/vendor/composer/semver/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+### [3.6.0] 2026-10-29
+
+ * Improved `Intervals::compactConstraint()` performance by caching results per constraint string, which speeds up callers that compact the same constraints repeatedly like Composer's pool optimizer, `Intervals::clear()` frees the cache (#194)
+
+### [3.5.2] 2026-10-22
+
+ * Fixed `MultiConstraint::getUpperBound()` picking the wrong inclusivity when two upper bounds share a version, e.g. `< 1.0 || == 1.0` reported an exclusive upper bound that excluded `1.0` (#193)
+
### [3.5.1] 2026-10-05
* Fixed `Intervals::isSubsetOf()` returning false for `< 0.0.0`, which now correctly matches nothing and so is a subset of any constraint (#192)
@@ -206,6 +214,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint`
* Changed: code style using php-cs-fixer.
+[3.6.0]: https://github.com/composer/semver/compare/3.5.2...3.6.0
+[3.5.2]: https://github.com/composer/semver/compare/3.5.1...3.5.2
[3.5.1]: https://github.com/composer/semver/compare/3.5.0...3.5.1
[3.5.0]: https://github.com/composer/semver/compare/3.4.4...3.5.0
[3.4.4]: https://github.com/composer/semver/compare/3.4.3...3.4.4
diff --git a/core/vendor/composer/semver/src/Constraint/MultiConstraint.php b/core/vendor/composer/semver/src/Constraint/MultiConstraint.php
index 1f4c006168..16e47ce439 100644
--- a/core/vendor/composer/semver/src/Constraint/MultiConstraint.php
+++ b/core/vendor/composer/semver/src/Constraint/MultiConstraint.php
@@ -317,8 +317,15 @@ private function extractBounds()
$this->lowerBound = $constraint->getLowerBound();
}
- if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) {
- $this->upperBound = $constraint->getUpperBound();
+ $upperBound = $constraint->getUpperBound();
+ $cmp = version_compare($upperBound->getVersion(), $this->upperBound->getVersion());
+ if (0 === $cmp) {
+ // same version: for upper bounds inclusive is higher (compareTo() assumes lower bounds)
+ if ($upperBound->isInclusive() === $this->isDisjunctive()) {
+ $this->upperBound = $upperBound;
+ }
+ } elseif ($cmp === ($this->isConjunctive() ? -1 : 1)) {
+ $this->upperBound = $upperBound;
}
}
}
diff --git a/core/vendor/composer/semver/src/Intervals.php b/core/vendor/composer/semver/src/Intervals.php
index 1329800dfd..80c6d07518 100644
--- a/core/vendor/composer/semver/src/Intervals.php
+++ b/core/vendor/composer/semver/src/Intervals.php
@@ -36,6 +36,11 @@ class Intervals
*/
private static $intervalsCache = array();
+ /**
+ * @phpstan-var array
+ */
+ private static $compactedCache = array();
+
/**
* @phpstan-var array
*/
@@ -54,6 +59,7 @@ class Intervals
public static function clear()
{
self::$intervalsCache = array();
+ self::$compactedCache = array();
}
/**
@@ -142,6 +148,19 @@ public static function compactConstraint(ConstraintInterface $constraint)
return $constraint;
}
+ $key = (string) $constraint;
+ if (!isset(self::$compactedCache[$key])) {
+ self::$compactedCache[$key] = self::doCompactConstraint($constraint);
+ }
+
+ return self::$compactedCache[$key];
+ }
+
+ /**
+ * @return ConstraintInterface
+ */
+ private static function doCompactConstraint(MultiConstraint $constraint)
+ {
$intervals = self::generateIntervals($constraint);
$constraints = array();
$hasNumericMatchAll = false;
diff --git a/core/vendor/doctrine/inflector/SECURITY.md b/core/vendor/doctrine/inflector/SECURITY.md
new file mode 100644
index 0000000000..4b4deb1137
--- /dev/null
+++ b/core/vendor/doctrine/inflector/SECURITY.md
@@ -0,0 +1,7 @@
+# Security
+
+If you find a Security bug in Doctrine, please follow our
+[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).
+
+Security vulnerabilities should be reported to security@doctrine-project.org
+and not posted on the GitHub issue tracker of the project.
diff --git a/core/vendor/doctrine/inflector/composer.json b/core/vendor/doctrine/inflector/composer.json
index 6102926f50..960111dc69 100644
--- a/core/vendor/doctrine/inflector/composer.json
+++ b/core/vendor/doctrine/inflector/composer.json
@@ -42,7 +42,7 @@
"php": "^7.2 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^12.0 || ^13.0",
+ "doctrine/coding-standard": "^12.0 || ^14.0",
"phpstan/phpstan": "^1.12 || ^2.0",
"phpstan/phpstan-phpunit": "^1.4 || ^2.0",
"phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
diff --git a/core/vendor/doctrine/inflector/docs/en/index.rst b/core/vendor/doctrine/inflector/docs/en/index.rst
index 1e094c8ecb..09ea90e87d 100644
--- a/core/vendor/doctrine/inflector/docs/en/index.rst
+++ b/core/vendor/doctrine/inflector/docs/en/index.rst
@@ -41,6 +41,7 @@ The supported languages are as follows:
- ``Language::ENGLISH``
- ``Language::ESPERANTO``
- ``Language::FRENCH``
+- ``Language::ITALIAN``
- ``Language::NORWEGIAN_BOKMAL``
- ``Language::PORTUGUESE``
- ``Language::SPANISH``
@@ -50,6 +51,7 @@ If you want to manually construct the inflector instead of using a factory, you
.. code-block:: php
+ use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\CachedWordInflector;
use Doctrine\Inflector\RulesetInflector;
use Doctrine\Inflector\Rules\English;
diff --git a/core/vendor/doctrine/inflector/src/Inflector.php b/core/vendor/doctrine/inflector/src/Inflector.php
index 610a4cf4d0..01d82706b8 100644
--- a/core/vendor/doctrine/inflector/src/Inflector.php
+++ b/core/vendor/doctrine/inflector/src/Inflector.php
@@ -210,6 +210,10 @@ class Inflector
'Ž' => 'Z',
'ž' => 'z',
'ſ' => 's',
+ 'Ё' => 'Е',
+ 'Ї' => 'І',
+ 'ё' => 'е',
+ 'ї' => 'і',
'€' => 'E',
'£' => '',
];
diff --git a/core/vendor/doctrine/inflector/src/Rules/Portuguese/Inflectible.php b/core/vendor/doctrine/inflector/src/Rules/Portuguese/Inflectible.php
index 0d41fe7e16..042cd48c51 100644
--- a/core/vendor/doctrine/inflector/src/Rules/Portuguese/Inflectible.php
+++ b/core/vendor/doctrine/inflector/src/Rules/Portuguese/Inflectible.php
@@ -71,7 +71,7 @@ public static function getIrregular(): iterable
yield new Substitution(new Word('chão'), new Word('chãos'));
yield new Substitution(new Word('charlatão'), new Word('charlatães'));
yield new Substitution(new Word('cidadão'), new Word('cidadãos'));
- yield new Substitution(new Word('consul'), new Word('consules'));
+ yield new Substitution(new Word('cônsul'), new Word('cônsules'));
yield new Substitution(new Word('cristão'), new Word('cristãos'));
yield new Substitution(new Word('difícil'), new Word('difíceis'));
yield new Substitution(new Word('email'), new Word('emails'));
@@ -85,6 +85,7 @@ public static function getIrregular(): iterable
yield new Substitution(new Word('liquens'), new Word('liquen'));
yield new Substitution(new Word('mal'), new Word('males'));
yield new Substitution(new Word('mão'), new Word('mãos'));
+ yield new Substitution(new Word('mês'), new Word('meses'));
yield new Substitution(new Word('orfão'), new Word('orfãos'));
yield new Substitution(new Word('país'), new Word('países'));
yield new Substitution(new Word('pai'), new Word('pais'));
@@ -94,5 +95,6 @@ public static function getIrregular(): iterable
yield new Substitution(new Word('sacristão'), new Word('sacristães'));
yield new Substitution(new Word('sotão'), new Word('sotãos'));
yield new Substitution(new Word('tabelião'), new Word('tabeliães'));
+ yield new Substitution(new Word('útil'), new Word('úteis'));
}
}
diff --git a/core/vendor/doctrine/inflector/src/Rules/Portuguese/Uninflected.php b/core/vendor/doctrine/inflector/src/Rules/Portuguese/Uninflected.php
index b8e988f8c6..5eb101fe69 100644
--- a/core/vendor/doctrine/inflector/src/Rules/Portuguese/Uninflected.php
+++ b/core/vendor/doctrine/inflector/src/Rules/Portuguese/Uninflected.php
@@ -23,10 +23,24 @@ public static function getPlural(): iterable
/** @return Pattern[] */
private static function getDefault(): iterable
{
- yield new Pattern('tórax');
- yield new Pattern('tênis');
- yield new Pattern('ônibus');
- yield new Pattern('lápis');
+ yield new Pattern('atlas');
+ yield new Pattern('bate-papo');
+ yield new Pattern('cais');
yield new Pattern('fênix');
+ yield new Pattern('guarda-chuva');
+ yield new Pattern('guarda-roupa');
+ yield new Pattern('lápis');
+ yield new Pattern('oásis');
+ yield new Pattern('ônibus');
+ yield new Pattern('ônus');
+ yield new Pattern('pára-brisa');
+ yield new Pattern('pára-choque');
+ yield new Pattern('pires');
+ yield new Pattern('porta-malas');
+ yield new Pattern('porta-voz');
+ yield new Pattern('sem-terra');
+ yield new Pattern('tênis');
+ yield new Pattern('tórax');
+ yield new Pattern('vírus');
}
}
diff --git a/core/vendor/guzzlehttp/guzzle/CHANGELOG.md b/core/vendor/guzzlehttp/guzzle/CHANGELOG.md
index 8b55e29e26..fbfccd9228 100644
--- a/core/vendor/guzzlehttp/guzzle/CHANGELOG.md
+++ b/core/vendor/guzzlehttp/guzzle/CHANGELOG.md
@@ -3,6 +3,14 @@
Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
+## 7.16.1 - 2026-10-25
+
+### Fixed
+
+- Fix `CookieJar` storing already expired cookies instead of removing the stored cookie
+- Fix `CookieJar` ignoring a same-value cookie that changes its expiry or its `Discard`, `Secure`, or `HttpOnly` flag
+
+
## 7.16.0 - 2026-10-18
### Changed
diff --git a/core/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php b/core/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
index ef11cc7d78..423eb34a70 100644
--- a/core/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
+++ b/core/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
@@ -175,7 +175,7 @@ public function setCookie(SetCookie $cookie): bool
}
$maxAge = $cookie->getMaxAge();
- if ($maxAge !== null && $maxAge <= 0) {
+ if (($maxAge !== null && $maxAge <= 0) || $cookie->isExpired()) {
if ($cookie->getDomain() !== null) {
$this->removeCookie($cookie);
}
@@ -195,22 +195,13 @@ public function setCookie(SetCookie $cookie): bool
continue;
}
- // The previously set cookie is a discard cookie and this one is
- // not so allow the new cookie to be set
- if (!$cookie->getDiscard() && $c->getDiscard()) {
- unset($this->cookies[$i]);
- continue;
- }
-
- // If the new cookie's expiration is further into the future, then
- // replace the old cookie
- if ($cookie->getExpires() > $c->getExpires()) {
- unset($this->cookies[$i]);
- continue;
- }
-
- // If the value has changed, we better change it
- if ($cookie->getValue() !== $c->getValue()) {
+ // Replace the old cookie when its value, expiry or flags changed
+ if ($cookie->getValue() !== $c->getValue()
+ || $cookie->getExpires() !== $c->getExpires()
+ || $cookie->getDiscard() !== $c->getDiscard()
+ || $cookie->getSecure() !== $c->getSecure()
+ || $cookie->getHttpOnly() !== $c->getHttpOnly()
+ ) {
unset($this->cookies[$i]);
continue;
}
diff --git a/core/vendor/illuminate/support/Facades/Artisan.php b/core/vendor/illuminate/support/Facades/Artisan.php
index 842cc7cfc7..a73ae795ce 100644
--- a/core/vendor/illuminate/support/Facades/Artisan.php
+++ b/core/vendor/illuminate/support/Facades/Artisan.php
@@ -14,6 +14,7 @@
* @method static void registerCommand(\Symfony\Component\Console\Command\Command $command)
* @method static int call(\Symfony\Component\Console\Command\Command|string $command, array $parameters = [], \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer = null)
* @method static \Illuminate\Foundation\Bus\PendingDispatch queue(string $command, array $parameters = [])
+ * @method static \Symfony\Component\Console\Command\Command|null findCommand(string $name)
* @method static array all()
* @method static string output()
* @method static void bootstrap()
diff --git a/core/vendor/illuminate/support/Facades/Exceptions.php b/core/vendor/illuminate/support/Facades/Exceptions.php
index 263b95bd04..1b6827706e 100644
--- a/core/vendor/illuminate/support/Facades/Exceptions.php
+++ b/core/vendor/illuminate/support/Facades/Exceptions.php
@@ -20,6 +20,7 @@
* @method static bool shouldReport(\Throwable $e)
* @method static \Illuminate\Foundation\Exceptions\Handler throttleUsing(callable $throttleUsing)
* @method static \Illuminate\Foundation\Exceptions\Handler stopIgnoring(array|string $exceptions)
+ * @method static array contextForException(\Throwable $e)
* @method static \Illuminate\Foundation\Exceptions\Handler buildContextUsing(\Closure $contextCallback)
* @method static \Symfony\Component\HttpFoundation\Response render(\Illuminate\Http\Request $request, \Throwable $e)
* @method static \Illuminate\Foundation\Exceptions\Handler respondUsing(callable $callback)
diff --git a/core/vendor/justinrainbow/json-schema/CHANGELOG.md b/core/vendor/justinrainbow/json-schema/CHANGELOG.md
index a62cb51673..1254730ddc 100644
--- a/core/vendor/justinrainbow/json-schema/CHANGELOG.md
+++ b/core/vendor/justinrainbow/json-schema/CHANGELOG.md
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [6.14.0] - 2026-10-24
+
+### Added
+- test: Remove JSON Schema Test Suite skips for cases that already pass ([#985](https://github.com/jsonrainbow/json-schema/pull/985))
+- chore: Enforce LF line endings via .gitattributes and .editorconfig ([#980](https://github.com/jsonrainbow/json-schema/pull/980))
+- test: Clean up JSON Schema Test Suite skip list ([#978](https://github.com/jsonrainbow/json-schema/pull/978))
+- feat: support unevaluatedProperties in Draft 2019-09 ([#931](https://github.com/jsonrainbow/json-schema/pull/931))
+- Allow bin/run-test-case to run a complete test file ([#966](https://github.com/jsonrainbow/json-schema/pull/966))
+
+### Fixed
+- Throw on an unresolvable $ref instead of passing silently ([#944](https://github.com/jsonrainbow/json-schema/pull/944))
+- test: Pass file URLs to the Curl retriever tests ([#989](https://github.com/jsonrainbow/json-schema/pull/989))
+- fix: Reject a trailing newline in idn-hostname ([#990](https://github.com/jsonrainbow/json-schema/pull/990))
+- fix: Report Draft-06/07 sub-schema errors at the property and item path ([#967](https://github.com/jsonrainbow/json-schema/pull/967))
+- fix: Report float limits in numeric error messages without casting to int ([#982](https://github.com/jsonrainbow/json-schema/pull/982))
+- fix: Respect CHECK_MODE_DISABLE_FORMAT in Draft-06, Draft-07 and Draft 2019-09 format constraints ([#972](https://github.com/jsonrainbow/json-schema/pull/972))
+- fix: Show the regex instead of the value in the pattern error message ([#974](https://github.com/jsonrainbow/json-schema/pull/974))
+- Apply the same ECMA to PCRE pattern replacements everywhere ([#943](https://github.com/jsonrainbow/json-schema/pull/943))
+- fix: Keep backslash sequences of the install path in package:// translation ([#958](https://github.com/jsonrainbow/json-schema/pull/958))
+- fix: Ignore sibling keywords of $ref in Draft-06 and Draft-07 ([#968](https://github.com/jsonrainbow/json-schema/pull/968))
+- fix: Handle invalid regex patterns from user input ([#969](https://github.com/jsonrainbow/json-schema/pull/969))
+
+### Removed
+- chore: Remove legacy Sphinx docs folder ([#981](https://github.com/jsonrainbow/json-schema/pull/981))
+
+
## [6.13.1] - 2026-09-30
### Fixed
diff --git a/core/vendor/justinrainbow/json-schema/README.md b/core/vendor/justinrainbow/json-schema/README.md
index 5f400b309b..938b77d9f8 100644
--- a/core/vendor/justinrainbow/json-schema/README.md
+++ b/core/vendor/justinrainbow/json-schema/README.md
@@ -11,6 +11,8 @@ Draft-4, Draft-6,Draft-7 or Draft 2019-09.
Features of newer Drafts might not be supported. See [Table of All Versions of Everything](https://json-schema.org/specification-links.html#table-of-all-versions-of-everything) to get an overview
of all existing Drafts. See [json-schema](http://json-schema.org/) for more details about the JSON Schema specification
+The documentation for this library is available at [jsonrainbow.github.io/docs](https://jsonrainbow.github.io/docs/).
+
# Compliance


diff --git a/core/vendor/justinrainbow/json-schema/bin/run-test-case b/core/vendor/justinrainbow/json-schema/bin/run-test-case
index 86b6dc38f6..1e50fa6bad 100644
--- a/core/vendor/justinrainbow/json-schema/bin/run-test-case
+++ b/core/vendor/justinrainbow/json-schema/bin/run-test-case
@@ -34,10 +34,10 @@ foreach ($argv as $arg) {
}
}
-if (count($arArgs) < 2 || isset($arOptions['--help']) || isset($arOptions['-h'])) {
+if (count($arArgs) < 1 || isset($arOptions['--help']) || isset($arOptions['-h'])) {
echo << "[/]"
+Usage: run-test-case ["[/]"]
Arguments:
test-file Path to a test JSON file. Resolved in order:
@@ -46,6 +46,7 @@ Arguments:
description "" or "/" — split on the first "/" only.
Exact match is tried first, then case-insensitive substring.
Omit the test part to run all tests in the group.
+ Omit the description to run every group in the file.
Options:
-v --verbose Also print the schema
@@ -53,7 +54,8 @@ Options:
Exit codes:
0 All matched tests passed
- 1 At least one test failed
+ 1 At least one test failed (or errored during validation when running
+ more than a single test)
2 Usage error, file not found, or group/test not found
HLP;
@@ -81,9 +83,14 @@ if (preg_match('/(draft[^\/]+)/i', str_replace('\\', '/', $filePath), $m)) {
}
// Parse "/" — split on first "/" only so slashes in test descriptions are preserved
-$descParts = explode('/', $arArgs[1], 2);
-$groupDesc = $descParts[0];
-$testDesc = $descParts[1] ?? null;
+$description = $arArgs[1] ?? null;
+$groupDesc = null;
+$testDesc = null;
+if ($description !== null) {
+ $descParts = explode('/', $description, 2);
+ $groupDesc = $descParts[0];
+ $testDesc = $descParts[1] ?? null;
+}
// Load test file
$contents = json_decode((string) file_get_contents($filePath), false);
@@ -98,55 +105,57 @@ $foundGroup = null;
$foundTest = null;
$matchedGroup = null;
-foreach ([true, false] as $exact) {
- foreach ($contents as $group) {
- $groupMatch = $exact
- ? $group->description === $groupDesc
- : stripos($group->description, $groupDesc) !== false;
+if ($groupDesc !== null) {
+ foreach ([true, false] as $exact) {
+ foreach ($contents as $group) {
+ $groupMatch = $exact
+ ? $group->description === $groupDesc
+ : stripos($group->description, $groupDesc) !== false;
- if (!$groupMatch) {
- continue;
- }
+ if (!$groupMatch) {
+ continue;
+ }
- if ($testDesc === null) {
- $foundGroup = $group;
- break 2;
- }
+ if ($testDesc === null) {
+ $foundGroup = $group;
+ break 2;
+ }
- $matchedGroup = $group;
+ $matchedGroup = $group;
- foreach ($group->tests as $test) {
- $testMatch = $exact
- ? $test->description === $testDesc
- : stripos($test->description, $testDesc) !== false;
+ foreach ($group->tests as $test) {
+ $testMatch = $exact
+ ? $test->description === $testDesc
+ : stripos($test->description, $testDesc) !== false;
- if ($testMatch) {
- $foundGroup = $group;
- $foundTest = $test;
- break 3;
+ if ($testMatch) {
+ $foundGroup = $group;
+ $foundTest = $test;
+ break 3;
+ }
}
}
}
-}
-if ($matchedGroup === null && $foundGroup === null) {
- echo "Error: no matching group found for: {$groupDesc}\n";
- echo "Available groups:\n";
- foreach ($contents as $group) {
- echo " - {$group->description}\n";
+ if ($matchedGroup === null && $foundGroup === null) {
+ echo "Error: no matching group found for: {$groupDesc}\n";
+ echo "Available groups:\n";
+ foreach ($contents as $group) {
+ echo " - {$group->description}\n";
+ }
+ exit(2);
}
- exit(2);
-}
-if ($testDesc !== null && $foundTest === null) {
- echo "Error: no matching test found for: {$testDesc}\n";
- $listGroup = $matchedGroup ?? $foundGroup;
- echo "Available tests in \"{$listGroup->description}\":\n";
- foreach ($listGroup->tests as $test) {
- $expect = $test->valid ? 'valid' : 'invalid';
- echo " [{$expect}] {$test->description}\n";
+ if ($testDesc !== null && $foundTest === null) {
+ echo "Error: no matching test found for: {$testDesc}\n";
+ $listGroup = $matchedGroup ?? $foundGroup;
+ echo "Available tests in \"{$listGroup->description}\":\n";
+ foreach ($listGroup->tests as $test) {
+ $expect = $test->valid ? 'valid' : 'invalid';
+ echo " [{$expect}] {$test->description}\n";
+ }
+ exit(2);
}
- exit(2);
}
// Determine check mode (mirrors JsonSchemaTestSuiteTest::getCheckModeForDraft)
@@ -160,15 +169,8 @@ try {
$draftIdentifier = DraftIdentifiers::DRAFT_4();
}
-// Set up validator (mirrors JsonSchemaTestSuiteTest::testTestCaseValidatesCorrectly)
-$schema = $foundGroup->schema;
-$schemaId = is_object($schema) && property_exists($schema, 'id')
- ? $schema->id
- : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI;
-
-$schemaStorage = new SchemaStorage();
-$schemaStorage->addSchema($schemaId, $schema);
-
+// Load remote schemas once; they are registered for every group
+$remotes = [];
if (is_dir($remotesDir)) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($remotesDir));
foreach ($iterator as $info) {
@@ -176,15 +178,29 @@ if (is_dir($remotesDir)) {
continue;
}
$remoteId = str_replace($remotesDir, 'http://localhost:1234', $info->getPathname());
- $schemaStorage->addSchema($remoteId, json_decode((string) file_get_contents($info->getPathname()), false));
+ $remotes[$remoteId] = json_decode((string) file_get_contents($info->getPathname()), false);
}
}
-$factory = new Factory($schemaStorage);
-$factory->setDefaultDialect($draftIdentifier->getValue());
+// Set up a factory for a group's schema (mirrors JsonSchemaTestSuiteTest::testTestCaseValidatesCorrectly)
+$createFactory = static function ($schema) use ($remotes, $draftIdentifier): Factory {
+ $schemaId = is_object($schema) && property_exists($schema, 'id')
+ ? $schema->id
+ : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI;
-/** @param object $test */
-$runTest = static function (object $test) use ($factory, $schema, $checkMode): array {
+ $schemaStorage = new SchemaStorage();
+ $schemaStorage->addSchema($schemaId, $schema);
+ foreach ($remotes as $remoteId => $remote) {
+ $schemaStorage->addSchema($remoteId, $remote);
+ }
+
+ $factory = new Factory($schemaStorage);
+ $factory->setDefaultDialect($draftIdentifier->getValue());
+
+ return $factory;
+};
+
+$runTest = static function (Factory $factory, $schema, object $test) use ($checkMode): array {
$validator = new Validator($factory);
try {
$validator->validate($test->data, $schema, $checkMode);
@@ -201,16 +217,17 @@ $runTest = static function (object $test) use ($factory, $schema, $checkMode): a
$verbose = isset($arOptions['--verbose']) || isset($arOptions['-v']);
-// Run all tests in the group when no specific test was requested
-if ($foundTest === null) {
- echo "Group: {$foundGroup->description}\n";
+// Run all tests in a group, returning [passCount, failCount]
+$runGroup = static function (object $group) use ($createFactory, $runTest, $verbose): array {
+ echo "Group: {$group->description}\n";
if ($verbose) {
- echo "Schema: " . json_encode($schema, JSON_PRETTY_PRINT) . "\n";
+ echo "Schema: " . json_encode($group->schema, JSON_PRETTY_PRINT) . "\n";
}
+ $factory = $createFactory($group->schema);
$passCount = 0;
$failCount = 0;
- foreach ($foundGroup->tests as $test) {
- $result = $runTest($test);
+ foreach ($group->tests as $test) {
+ $result = $runTest($factory, $group->schema, $test);
if (isset($result['error'])) {
echo sprintf("[ERROR] %s\n", $test->description);
echo " Error during validation: {$result['error']}\n";
@@ -233,11 +250,35 @@ if ($foundTest === null) {
$passCount++;
}
}
+
+ return [$passCount, $failCount];
+};
+
+// Run every group in the file when no description was given
+if ($groupDesc === null) {
+ $passCount = 0;
+ $failCount = 0;
+ foreach ($contents as $index => $group) {
+ if ($index > 0) {
+ echo "\n";
+ }
+ [$groupPass, $groupFail] = $runGroup($group);
+ $passCount += $groupPass;
+ $failCount += $groupFail;
+ }
+ echo sprintf("\n%d groups: %d passed, %d failed.\n", count($contents), $passCount, $failCount);
+ exit($failCount > 0 ? 1 : 0);
+}
+
+// Run all tests in the group when no specific test was requested
+if ($foundTest === null) {
+ [$passCount, $failCount] = $runGroup($foundGroup);
echo "\n{$passCount} passed, {$failCount} failed.\n";
exit($failCount > 0 ? 1 : 0);
}
-$result = $runTest($foundTest);
+$schema = $foundGroup->schema;
+$result = $runTest($createFactory($schema), $schema, $foundTest);
if (isset($result['error'])) {
echo "Error during validation: {$result['error']}\n";
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php
index 0934292c35..95145617fb 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php
@@ -62,6 +62,7 @@ class ConstraintError extends Enum
public const PROPERTY_NAMES = 'propertyNames';
public const TYPE = 'type';
public const UNIQUE_ITEMS = 'uniqueItems';
+ public const UNEVALUATED_PROPERTIES = 'unevaluatedProperties';
public const CONTENT_MEDIA_TYPE = 'contentMediaType';
public const CONTENT_ENCODING = 'contentEncoding';
@@ -78,12 +79,12 @@ public function getMessage()
self::ANY_OF => 'Failed to match at least one schema',
self::DEPENDENCIES => '%s depends on %s, which is missing',
self::DISALLOW => 'Disallowed value was matched',
- self::DIVISIBLE_BY => 'Is not divisible by %d',
+ self::DIVISIBLE_BY => 'Is not divisible by %s',
self::ENUM => 'Does not have a value in the enumeration %s',
self::CONSTANT => 'Does not have a value equal to %s',
self::CONTAINS => 'Does not have a value valid to contains schema',
- self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d',
- self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d',
+ self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %s',
+ self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %s',
self::FALSE => 'Boolean schema false',
self::FORMAT_COLOR => 'Invalid color',
self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD',
@@ -107,10 +108,10 @@ public function getMessage()
self::LENGTH_MIN => 'Must be at least %d characters long',
self::MAX_CONTAINS => 'There must be a maximum of %d valid items in the array, %d found',
self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found',
- self::MAXIMUM => 'Must have a maximum value less than or equal to %d',
+ self::MAXIMUM => 'Must have a maximum value less than or equal to %s',
self::MIN_CONTAINS => 'There must be a minimum of %d valid items in the array, %d found',
self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found',
- self::MINIMUM => 'Must have a minimum value greater than or equal to %d',
+ self::MINIMUM => 'Must have a minimum value greater than or equal to %s',
self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum',
self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum',
/*self::MISSING_ERROR => 'Used for tests; this error is deliberately commented out',*/
@@ -126,6 +127,7 @@ public function getMessage()
self::PROPERTY_NAMES => 'Property name %s is invalid',
self::TYPE => '%s value found, but %s is required',
self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array',
+ self::UNEVALUATED_PROPERTIES => 'The property %s is not evaluated and the definition does not allow unevaluated properties',
self::CONTENT_MEDIA_TYPE => 'Value is not valid with content media type',
self::CONTENT_ENCODING => 'Value is not valid with content encoding',
];
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalItemsConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalItemsConstraint.php
index d1adbce8f5..8b7cc69920 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalItemsConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalItemsConstraint.php
@@ -46,16 +46,19 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
$additionalItems = array_diff_key($value, property_exists($schema, 'items') ? $schema->items : []);
+ $basePath = $path ?? new JsonPointer('');
foreach ($additionalItems as $propertyName => $propertyValue) {
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
$schemaConstraint = $this->factory->createInstanceFor('schema');
- $schemaConstraint->check($propertyValue, $schema->additionalItems, $path, $i);
+ $schemaConstraint->check($propertyValue, $schema->additionalItems, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
- $this->addError(ConstraintError::ADDITIONAL_ITEMS(), $path, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]);
+ $this->addError(ConstraintError::ADDITIONAL_ITEMS(), $incrementedPath, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]);
}
}
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php
index 0f78b32b5a..05c7dd4e90 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php
@@ -8,6 +8,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class AdditionalPropertiesConstraint implements ConstraintInterface
{
@@ -47,7 +48,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
foreach ($additionalProperties as $key => $_) {
foreach ($patterns as $pattern) {
- if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) {
+ if (preg_match(EcmaPatternConverter::toPcre($pattern), (string) $key)) {
unset($additionalProperties[$key]);
break;
}
@@ -74,25 +75,4 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), ($path ?? new JsonPointer(''))->withAppendedPath($key), ['found' => $key]);
}
}
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
-// '\D' => '[^0-9]',
-// '\d' => '[0-9]',
- '\p{digit}' => '\p{Nd}',
-// '\w' => '[A-Za-z0-9_]',
-// '\W' => '[^A-Za-z0-9_]',
-// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space,
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
- }
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php
index bd6efac66c..9d8afe8af4 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php
@@ -33,6 +33,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
// Apply defaults
$this->checkForKeyword('ref', $value, $schema, $path, $i);
+ if (property_exists($schema, '$ref')) {
+ // All other keywords in a schema containing $ref are ignored
+ return;
+ }
+
$this->checkForKeyword('required', $value, $schema, $path, $i);
$this->checkForKeyword('contains', $value, $schema, $path, $i);
$this->checkForKeyword('properties', $value, $schema, $path, $i);
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php
index d4e74c75fe..b268d640a6 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php
@@ -5,6 +5,7 @@
namespace JsonSchema\Constraints\Drafts\Draft06;
use JsonSchema\ConstraintError;
+use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Constraints\Factory;
use JsonSchema\Entity\ErrorBagProxy;
@@ -17,14 +18,18 @@ class FormatConstraint implements ConstraintInterface
{
use ErrorBagProxy;
+ /** @var Factory */
+ private $factory;
+
public function __construct(?Factory $factory = null)
{
- $this->initialiseErrorBag($factory ?: new Factory());
+ $this->factory = $factory ?: new Factory();
+ $this->initialiseErrorBag($this->factory);
}
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
- if (!property_exists($schema, 'format')) {
+ if (!property_exists($schema, 'format') || $this->factory->getConfig(Constraint::CHECK_MODE_DISABLE_FORMAT)) {
return;
}
@@ -140,7 +145,7 @@ private function validateDateTime(string $datetime, string $format): bool
private function validateRegex(string $regex): bool
{
- return preg_match(self::jsonPatternToPhpRegex($regex), '') !== false;
+ return @preg_match(self::jsonPatternToPhpRegex($regex), '') !== false;
}
/**
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/ItemsConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/ItemsConstraint.php
index 21259e1a82..3b52465aa7 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/ItemsConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/ItemsConstraint.php
@@ -31,6 +31,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
+ $basePath = $path ?? new JsonPointer('');
foreach ($value as $propertyName => $propertyValue) {
$itemSchema = $schema->items;
if (is_array($itemSchema)) {
@@ -40,8 +41,9 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$itemSchema = $itemSchema[$propertyName];
}
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
$schemaConstraint = $this->factory->createInstanceFor('schema');
- $schemaConstraint->check($propertyValue, $itemSchema, $path, $i);
+ $schemaConstraint->check($propertyValue, $itemSchema, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternConstraint.php
index 5c705f4ae1..fde48bb342 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternConstraint.php
@@ -8,6 +8,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class PatternConstraint implements ConstraintInterface
{
@@ -32,32 +33,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
- $matchPattern = $this->createPregMatchPattern($schema->pattern);
+ $matchPattern = EcmaPatternConverter::toPcre($schema->pattern);
if (preg_match($matchPattern, $value) === 1) {
return;
}
- $this->addError(ConstraintError::PATTERN(), $path, ['found' => $value, 'pattern' => $schema->pattern]);
- }
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
- '\D' => '[^0-9]',
- '\d' => '[0-9]',
- '\p{digit}' => '[0-9]',
- '\w' => '[A-Za-z0-9_]',
- '\W' => '[^A-Za-z0-9_]',
- '\s' => '[\s\x{200B}]', // Explicitly include zero width white space
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
+ $this->addError(ConstraintError::PATTERN(), $path, ['pattern' => $schema->pattern, 'found' => $value]);
}
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternPropertiesConstraint.php
index 969736bfbe..fab4aa1ea6 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternPropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PatternPropertiesConstraint.php
@@ -7,6 +7,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class PatternPropertiesConstraint implements ConstraintInterface
{
@@ -33,12 +34,15 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$properties = get_object_vars($value);
+ $basePath = $path ?? new JsonPointer('');
foreach ($properties as $propertyName => $propertyValue) {
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
foreach ($schema->patternProperties as $patternPropertyRegex => $patternPropertySchema) {
- $matchPattern = $this->createPregMatchPattern($patternPropertyRegex);
+ $matchPattern = EcmaPatternConverter::toPcre($patternPropertyRegex);
if (preg_match($matchPattern, (string) $propertyName)) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
- $schemaConstraint->check($propertyValue, $patternPropertySchema, $path, $i);
+ $schemaConstraint->check($propertyValue, $patternPropertySchema, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
@@ -48,25 +52,4 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
}
}
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
-// '\D' => '[^0-9]',
- '\d' => '[0-9]',
- '\p{digit}' => '[0-9]',
-// '\w' => '[A-Za-z0-9_]',
-// '\W' => '[^A-Za-z0-9_]',
-// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
- }
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesConstraint.php
index 4a9c480818..403b05b27c 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesConstraint.php
@@ -31,13 +31,16 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
+ $basePath = $path ?? new JsonPointer('');
foreach ($schema->properties as $propertyName => $propertySchema) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
if (!property_exists($value, $propertyName)) {
continue;
}
- $schemaConstraint->check($value->{$propertyName}, $propertySchema, $path, $i);
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
+ $schemaConstraint->check($value->{$propertyName}, $propertySchema, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php
index f52bcda0a2..d3f5ccf1bd 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php
@@ -27,11 +27,9 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
- try {
- $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
- } catch (\Exception $e) {
- return;
- }
+ // A $ref that cannot be resolved is a broken schema, not invalid data: the exception
+ // is left to propagate rather than letting the value pass as if the $ref was absent.
+ $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($value, $refSchema, $path, $i);
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php
index 8444d05359..216681860b 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php
@@ -46,16 +46,19 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
$additionalItems = array_diff_key($value, property_exists($schema, 'items') ? $schema->items : []);
+ $basePath = $path ?? new JsonPointer('');
foreach ($additionalItems as $propertyName => $propertyValue) {
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
$schemaConstraint = $this->factory->createInstanceFor('schema');
- $schemaConstraint->check($propertyValue, $schema->additionalItems, $path, $i);
+ $schemaConstraint->check($propertyValue, $schema->additionalItems, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
- $this->addError(ConstraintError::ADDITIONAL_ITEMS(), $path, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]);
+ $this->addError(ConstraintError::ADDITIONAL_ITEMS(), $incrementedPath, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]);
}
}
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php
index f94827607f..1cf0f167e6 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php
@@ -8,6 +8,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class AdditionalPropertiesConstraint implements ConstraintInterface
{
@@ -47,7 +48,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
foreach ($additionalProperties as $key => $_) {
foreach ($patterns as $pattern) {
- if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) {
+ if (preg_match(EcmaPatternConverter::toPcre($pattern), (string) $key)) {
unset($additionalProperties[$key]);
break;
}
@@ -74,25 +75,4 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), ($path ?? new JsonPointer(''))->withAppendedPath($key), ['found' => $key]);
}
}
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
-// '\D' => '[^0-9]',
-// '\d' => '[0-9]',
- '\p{digit}' => '\p{Nd}',
-// '\w' => '[A-Za-z0-9_]',
-// '\W' => '[^A-Za-z0-9_]',
-// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space,
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
- }
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php
index 084ce96cc5..1e88b6faf9 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php
@@ -33,6 +33,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
// Apply defaults
$this->checkForKeyword('ref', $value, $schema, $path, $i);
+ if (property_exists($schema, '$ref')) {
+ // All other keywords in a schema containing $ref are ignored
+ return;
+ }
+
$this->checkForKeyword('required', $value, $schema, $path, $i);
$this->checkForKeyword('contains', $value, $schema, $path, $i);
$this->checkForKeyword('properties', $value, $schema, $path, $i);
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php
index a79e891ff5..4d26fe5f62 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php
@@ -6,6 +6,7 @@
use DateTimeZone;
use JsonSchema\ConstraintError;
+use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Constraints\Factory;
use JsonSchema\Entity\ErrorBagProxy;
@@ -18,14 +19,18 @@ class FormatConstraint implements ConstraintInterface
{
use ErrorBagProxy;
+ /** @var Factory */
+ private $factory;
+
public function __construct(?Factory $factory = null)
{
- $this->initialiseErrorBag($factory ?: new Factory());
+ $this->factory = $factory ?: new Factory();
+ $this->initialiseErrorBag($this->factory);
}
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
- if (!property_exists($schema, 'format')) {
+ if (!property_exists($schema, 'format') || $this->factory->getConfig(Constraint::CHECK_MODE_DISABLE_FORMAT)) {
return;
}
@@ -209,7 +214,7 @@ private function validateDateTime(string $datetime, string $format): bool
private function validateRegex(string $regex): bool
{
- return preg_match(self::jsonPatternToPhpRegex($regex), '') !== false;
+ return @preg_match(self::jsonPatternToPhpRegex($regex), '') !== false;
}
/**
@@ -351,7 +356,7 @@ private function validateInternationalizedHostname(string $host): bool
return false;
}
// LDH rule (after IDNA)
- if (!preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i', $ascii)) {
+ if (!preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/i', $ascii)) {
return false;
}
$asciiLabels[] = $ascii;
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php
index 16abae3285..5b94ab4f2f 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php
@@ -31,6 +31,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
+ $basePath = $path ?? new JsonPointer('');
foreach ($value as $propertyName => $propertyValue) {
$itemSchema = $schema->items;
if (is_array($itemSchema)) {
@@ -40,8 +41,9 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$itemSchema = $itemSchema[$propertyName];
}
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
$schemaConstraint = $this->factory->createInstanceFor('schema');
- $schemaConstraint->check($propertyValue, $itemSchema, $path, $i);
+ $schemaConstraint->check($propertyValue, $itemSchema, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php
index 062b61d3f0..9afe22f6aa 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php
@@ -8,6 +8,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class PatternConstraint implements ConstraintInterface
{
@@ -32,32 +33,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
- $matchPattern = $this->createPregMatchPattern($schema->pattern);
+ $matchPattern = EcmaPatternConverter::toPcre($schema->pattern);
if (preg_match($matchPattern, $value) === 1) {
return;
}
- $this->addError(ConstraintError::PATTERN(), $path, ['found' => $value, 'pattern' => $schema->pattern]);
- }
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
- '\D' => '[^0-9]',
- '\d' => '[0-9]',
- '\p{digit}' => '[0-9]',
- '\w' => '[A-Za-z0-9_]',
- '\W' => '[^A-Za-z0-9_]',
- '\s' => '[\s\x{200B}]', // Explicitly include zero width white space
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
+ $this->addError(ConstraintError::PATTERN(), $path, ['pattern' => $schema->pattern, 'found' => $value]);
}
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php
index d821abf184..98a7760096 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php
@@ -7,6 +7,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class PatternPropertiesConstraint implements ConstraintInterface
{
@@ -33,12 +34,15 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$properties = get_object_vars($value);
+ $basePath = $path ?? new JsonPointer('');
foreach ($properties as $propertyName => $propertyValue) {
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
foreach ($schema->patternProperties as $patternPropertyRegex => $patternPropertySchema) {
- $matchPattern = $this->createPregMatchPattern($patternPropertyRegex);
+ $matchPattern = EcmaPatternConverter::toPcre($patternPropertyRegex);
if (preg_match($matchPattern, (string) $propertyName)) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
- $schemaConstraint->check($propertyValue, $patternPropertySchema, $path, $i);
+ $schemaConstraint->check($propertyValue, $patternPropertySchema, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
@@ -48,25 +52,4 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
}
}
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
-// '\D' => '[^0-9]',
- '\d' => '[0-9]',
- '\p{digit}' => '[0-9]',
-// '\w' => '[A-Za-z0-9_]',
-// '\W' => '[^A-Za-z0-9_]',
-// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
- }
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php
index 25c55e6660..2abad178ec 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php
@@ -31,13 +31,16 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
+ $basePath = $path ?? new JsonPointer('');
foreach ($schema->properties as $propertyName => $propertySchema) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
if (!property_exists($value, $propertyName)) {
continue;
}
- $schemaConstraint->check($value->{$propertyName}, $propertySchema, $path, $i);
+ $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
+ $schemaConstraint->check($value->{$propertyName}, $propertySchema, $incrementedPath, $i);
if ($schemaConstraint->isValid()) {
continue;
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php
index 9ec6efdac1..270da3d469 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php
@@ -27,11 +27,9 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
- try {
- $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
- } catch (\Exception $e) {
- return;
- }
+ // A $ref that cannot be resolved is a broken schema, not invalid data: the exception
+ // is left to propagate rather than letting the value pass as if the $ref was absent.
+ $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($value, $refSchema, $path, $i);
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php
index cdabc08103..373c0e8e5e 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php
@@ -8,6 +8,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class AdditionalPropertiesConstraint implements ConstraintInterface
{
@@ -47,7 +48,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
foreach ($additionalProperties as $key => $_) {
foreach ($patterns as $pattern) {
- if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) {
+ if (preg_match(EcmaPatternConverter::toPcre($pattern), (string) $key)) {
unset($additionalProperties[$key]);
break;
}
@@ -74,25 +75,4 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), ($path ?? new JsonPointer(''))->withAppendedPath($key), ['found' => $key]);
}
}
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
-// '\D' => '[^0-9]',
-// '\d' => '[0-9]',
- '\p{digit}' => '\p{Nd}',
-// '\w' => '[A-Za-z0-9_]',
-// '\W' => '[^A-Za-z0-9_]',
-// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space,
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
- }
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php
index 0b003b5461..da459bb7a1 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php
@@ -46,6 +46,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$this->checkForKeyword('anyOf', $value, $schema, $path, $i);
$this->checkForKeyword('oneOf', $value, $schema, $path, $i);
$this->checkForKeyword('ifThenElse', $value, $schema, $path, $i);
+ $this->checkForKeyword('unevaluatedProperties', $value, $schema, $path, $i);
$this->checkForKeyword('additionalProperties', $value, $schema, $path, $i);
$this->checkForKeyword('items', $value, $schema, $path, $i);
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php
index cc490225e7..0c8e9f17f4 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php
@@ -12,6 +12,7 @@ class Factory extends \JsonSchema\Constraints\Factory
protected $constraintMap = [
'schema' => Draft2019Constraint::class,
'additionalProperties' => AdditionalPropertiesConstraint::class,
+ 'unevaluatedProperties' => UnevaluatedPropertiesConstraint::class,
'additionalItems' => AdditionalItemsConstraint::class,
'dependentSchemas' => DependentSchemasConstraint::class,
'dependentRequired' => DependentRequiredConstraint::class,
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php
index 75554c4183..94706da812 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php
@@ -6,6 +6,7 @@
use DateTimeZone;
use JsonSchema\ConstraintError;
+use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Constraints\Factory;
use JsonSchema\Entity\ErrorBagProxy;
@@ -18,14 +19,18 @@ class FormatConstraint implements ConstraintInterface
{
use ErrorBagProxy;
+ /** @var Factory */
+ private $factory;
+
public function __construct(?Factory $factory = null)
{
- $this->initialiseErrorBag($factory ?: new Factory());
+ $this->factory = $factory ?: new Factory();
+ $this->initialiseErrorBag($this->factory);
}
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
- if (!property_exists($schema, 'format')) {
+ if (!property_exists($schema, 'format') || $this->factory->getConfig(Constraint::CHECK_MODE_DISABLE_FORMAT)) {
return;
}
@@ -218,7 +223,7 @@ private function validateDateTime(string $datetime, string $format): bool
private function validateRegex(string $regex): bool
{
- return preg_match(self::jsonPatternToPhpRegex($regex), '') !== false;
+ return @preg_match(self::jsonPatternToPhpRegex($regex), '') !== false;
}
/**
@@ -347,7 +352,7 @@ private function validateInternationalizedHostname(string $host): bool
return false;
}
- $ascii = idn_to_ascii($label);
+ $ascii = idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
if ($ascii === false) {
return false;
}
@@ -356,7 +361,7 @@ private function validateInternationalizedHostname(string $host): bool
return false;
}
// LDH rule (after IDNA)
- if (!preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i', $ascii)) {
+ if (!preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/i', $ascii)) {
return false;
}
$asciiLabels[] = $ascii;
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php
index b36ec026b3..29d3966586 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php
@@ -8,6 +8,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class PatternConstraint implements ConstraintInterface
{
@@ -32,32 +33,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
- $matchPattern = $this->createPregMatchPattern($schema->pattern);
+ $matchPattern = EcmaPatternConverter::toPcre($schema->pattern);
if (preg_match($matchPattern, $value) === 1) {
return;
}
- $this->addError(ConstraintError::PATTERN(), $path, ['found' => $value, 'pattern' => $schema->pattern]);
- }
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
- '\D' => '[^0-9]',
- '\d' => '[0-9]',
- '\p{digit}' => '[0-9]',
- '\w' => '[A-Za-z0-9_]',
- '\W' => '[^A-Za-z0-9_]',
- '\s' => '[\s\x{200B}]', // Explicitly include zero width white space
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
+ $this->addError(ConstraintError::PATTERN(), $path, ['pattern' => $schema->pattern, 'found' => $value]);
}
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php
index 4415615401..8c25011f6f 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php
@@ -7,6 +7,7 @@
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
+use JsonSchema\Tool\EcmaPatternConverter;
class PatternPropertiesConstraint implements ConstraintInterface
{
@@ -38,7 +39,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
$incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
foreach ($schema->patternProperties as $patternPropertyRegex => $patternPropertySchema) {
- $matchPattern = $this->createPregMatchPattern($patternPropertyRegex);
+ $matchPattern = EcmaPatternConverter::toPcre($patternPropertyRegex);
if (preg_match($matchPattern, (string) $propertyName)) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($propertyValue, $patternPropertySchema, $incrementedPath, $i);
@@ -51,25 +52,4 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
}
}
-
- private function createPregMatchPattern(string $pattern): string
- {
- $replacements = [
-// '\D' => '[^0-9]',
- '\d' => '[0-9]',
- '\p{digit}' => '[0-9]',
-// '\w' => '[A-Za-z0-9_]',
-// '\W' => '[^A-Za-z0-9_]',
-// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space
- '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
- ];
-
- $pattern = str_replace(
- array_keys($replacements),
- array_values($replacements),
- $pattern
- );
-
- return '/' . str_replace('/', '\/', $pattern) . '/u';
- }
}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php
index 989b1f0399..18d9c5f938 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php
@@ -27,11 +27,9 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
return;
}
- try {
- $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
- } catch (\Exception $e) {
- return;
- }
+ // A $ref that cannot be resolved is a broken schema, not invalid data: the exception
+ // is left to propagate rather than letting the value pass as if the $ref was absent.
+ $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($value, $refSchema, $path, $i);
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php
new file mode 100644
index 0000000000..809cbce57e
--- /dev/null
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php
@@ -0,0 +1,221 @@
+factory = $factory ?: new Factory();
+ $this->initialiseErrorBag($this->factory);
+ }
+
+ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
+ {
+ if (!is_object($schema) || !property_exists($schema, 'unevaluatedProperties') || !is_object($value)) {
+ return;
+ }
+
+ if ($schema->unevaluatedProperties === true) {
+ return;
+ }
+
+ $evaluated = $this->collectEvaluatedProperties($schema, $value, $path);
+ $unevaluated = array_diff_key(get_object_vars($value), array_flip($evaluated));
+ if (!$unevaluated) {
+ return;
+ }
+
+ $basePath = $path ?? new JsonPointer('');
+ foreach ($unevaluated as $propertyName => $propertyValue) {
+ $propertyPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+
+ if (is_object($schema->unevaluatedProperties)) {
+ $propertyConstraint = $this->factory->createInstanceFor('schema');
+ $propertyConstraint->check($propertyValue, $schema->unevaluatedProperties, $propertyPath, $i);
+ if ($propertyConstraint->isValid()) {
+ continue;
+ }
+
+ $this->addErrors($propertyConstraint->getErrors());
+ continue;
+ }
+
+ $this->addError(ConstraintError::UNEVALUATED_PROPERTIES(), $propertyPath, ['found' => $propertyName]);
+ }
+ }
+
+ /**
+ * The validator does not propagate annotations between applicators, so this
+ * constraint derives the evaluated property names from the supported schema branches.
+ *
+ * @param array $visitedRefs
+ *
+ * @return array
+ */
+ private function collectEvaluatedProperties(object $schema, object $value, ?JsonPointer $path = null, array $visitedRefs = []): array
+ {
+ $evaluated = [];
+ if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) {
+ $reference = $schema->{'$ref'};
+ if (in_array($reference, $visitedRefs, true)) {
+ return [];
+ }
+
+ try {
+ $visitedRefs[] = $reference;
+ $resolvedSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema);
+ if (is_object($resolvedSchema)) {
+ $evaluated = array_merge(
+ $evaluated,
+ $this->collectEvaluatedProperties($resolvedSchema, $value, $path, $visitedRefs)
+ );
+ }
+ } catch (\Exception $e) {
+ // Let the normal reference validation report resolution errors.
+ }
+ }
+
+ $properties = get_object_vars($value);
+
+ if (property_exists($schema, 'unevaluatedProperties') && $schema->unevaluatedProperties === true) {
+ $evaluated = array_merge($evaluated, array_keys($properties));
+ }
+
+ if (isset($schema->properties) && is_object($schema->properties)) {
+ $evaluated = array_merge(
+ $evaluated,
+ array_intersect(array_keys(get_object_vars($schema->properties)), array_keys($properties))
+ );
+ }
+
+ if (isset($schema->patternProperties) && is_object($schema->patternProperties)) {
+ foreach ($properties as $propertyName => $_) {
+ foreach (array_keys(get_object_vars($schema->patternProperties)) as $pattern) {
+ if (preg_match($this->createPregMatchPattern($pattern), (string) $propertyName)) {
+ $evaluated[] = $propertyName;
+ break;
+ }
+ }
+ }
+ }
+
+ if (property_exists($schema, 'additionalProperties')) {
+ if ($schema->additionalProperties === true) {
+ $evaluated = array_merge($evaluated, array_keys($properties));
+ } elseif (is_object($schema->additionalProperties)) {
+ foreach (array_diff(array_keys($properties), $evaluated) as $propertyName) {
+ $propertyPath = $this->propertyPath($path, $propertyName);
+ if ($this->schemaIsValid($schema->additionalProperties, $properties[$propertyName], $propertyPath)) {
+ $evaluated[] = $propertyName;
+ }
+ }
+ }
+ }
+
+ if (isset($schema->allOf) && is_array($schema->allOf)) {
+ foreach ($schema->allOf as $branch) {
+ if (!is_object($branch) || !$this->schemaIsValid($branch, $value, $path)) {
+ continue;
+ }
+
+ $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs));
+ }
+ }
+
+ if (isset($schema->anyOf) && is_array($schema->anyOf)) {
+ foreach ($schema->anyOf as $branch) {
+ if (!is_object($branch) || !$this->schemaIsValid($branch, $value, $path)) {
+ continue;
+ }
+
+ $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs));
+ }
+ }
+
+ if (isset($schema->oneOf) && is_array($schema->oneOf)) {
+ $validBranchCount = 0;
+ $validBranches = [];
+ foreach ($schema->oneOf as $branch) {
+ if ($this->schemaIsValid($branch, $value, $path)) {
+ ++$validBranchCount;
+ if (is_object($branch)) {
+ $validBranches[] = $branch;
+ }
+ }
+ }
+
+ if ($validBranchCount === 1 && count($validBranches) === 1) {
+ $evaluated = array_merge(
+ $evaluated,
+ $this->collectEvaluatedProperties($validBranches[0], $value, $path, $visitedRefs)
+ );
+ }
+ }
+
+ if (property_exists($schema, 'if')) {
+ $ifMatches = $this->schemaIsValid($schema->if, $value, $path);
+ if ($ifMatches) {
+ if (is_object($schema->if)) {
+ $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->if, $value, $path, $visitedRefs));
+ }
+ if (property_exists($schema, 'then') && is_object($schema->then) && $this->schemaIsValid($schema->then, $value, $path)) {
+ $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->then, $value, $path, $visitedRefs));
+ }
+ } elseif (property_exists($schema, 'else') && is_object($schema->else) && $this->schemaIsValid($schema->else, $value, $path)) {
+ $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->else, $value, $path, $visitedRefs));
+ }
+ }
+
+ if (isset($schema->dependentSchemas) && is_object($schema->dependentSchemas)) {
+ foreach (get_object_vars($schema->dependentSchemas) as $propertyName => $dependentSchema) {
+ if (!array_key_exists($propertyName, $properties) || !is_object($dependentSchema) || !$this->schemaIsValid($dependentSchema, $value, $path)) {
+ continue;
+ }
+
+ $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($dependentSchema, $value, $path, $visitedRefs));
+ }
+ }
+
+ return array_values(array_unique($evaluated));
+ }
+
+ /**
+ * @param mixed $schema
+ * @param mixed $value
+ */
+ private function schemaIsValid($schema, $value, ?JsonPointer $path = null): bool
+ {
+ $schemaConstraint = $this->factory->createInstanceFor('schema');
+ $schemaConstraint->check($value, $schema, $path);
+
+ return $schemaConstraint->isValid();
+ }
+
+ private function propertyPath(?JsonPointer $path, string $propertyName): JsonPointer
+ {
+ $basePath = $path ?? new JsonPointer('');
+
+ return $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));
+ }
+
+ private function createPregMatchPattern(string $pattern): string
+ {
+ $pattern = str_replace('\\p{digit}', '\\p{Nd}', $pattern);
+ $pattern = str_replace('\\p{Letter}', '\\p{L}', $pattern);
+
+ return '/' . str_replace('/', '\\/', $pattern) . '/u';
+ }
+}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Tool/EcmaPatternConverter.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Tool/EcmaPatternConverter.php
new file mode 100644
index 0000000000..96b0bafdc5
--- /dev/null
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Tool/EcmaPatternConverter.php
@@ -0,0 +1,84 @@
+ '0-9',
+ 'D' => '\x{0}-\x{2F}\x{3A}-\x{10FFFF}',
+ 'w' => 'A-Za-z0-9_',
+ 'W' => '\x{0}-\x{2F}\x{3A}-\x{40}\x{5B}-\x{5E}\x{60}\x{7B}-\x{10FFFF}',
+ 's' => '\s\x{200B}', // Explicitly include zero width white space
+ ];
+
+ /**
+ * PCRE rejects the ECMA long property names, so they are mapped to its abbreviations.
+ */
+ private const PROPERTY_NAMES = [
+ '\p{digit}' => '\p{Nd}',
+ '\p{Letter}' => '\p{L}',
+ ];
+
+ public static function toPcre(string $pattern): string
+ {
+ $regex = '';
+ $inClass = false;
+ $length = strlen($pattern);
+
+ for ($i = 0; $i < $length; $i++) {
+ $char = $pattern[$i];
+
+ if ($char === '\\' && $i + 1 < $length) {
+ $next = $pattern[$i + 1];
+
+ if (isset(self::SHORTHAND_CLASSES[$next])) {
+ $regex .= $inClass ? self::SHORTHAND_CLASSES[$next] : '[' . self::SHORTHAND_CLASSES[$next] . ']';
+ $i++;
+ continue;
+ }
+
+ foreach (self::PROPERTY_NAMES as $ecma => $pcre) {
+ if (substr_compare($pattern, $ecma, $i, strlen($ecma)) === 0) {
+ $regex .= $pcre;
+ $i += strlen($ecma) - 1;
+ continue 2;
+ }
+ }
+
+ // Any other escape, an escaped backslash or delimiter included, is kept as is.
+ $regex .= $char . $next;
+ $i++;
+ continue;
+ }
+
+ if ($char === '[' && !$inClass) {
+ $inClass = true;
+ } elseif ($char === ']' && $inClass) {
+ $inClass = false;
+ } elseif ($char === '/') {
+ $char = '\/';
+ }
+
+ $regex .= $char;
+ }
+
+ return '/' . $regex . '/u';
+ }
+}
diff --git a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php
index 76f1c7723e..781b3d5919 100644
--- a/core/vendor/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php
+++ b/core/vendor/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php
@@ -27,6 +27,8 @@
*/
class UriRetriever implements BaseUriRetrieverInterface
{
+ private const PACKAGE_SCHEME = 'package://';
+
/**
* @var array Map of URL translations
*/
@@ -352,7 +354,11 @@ public function translate($uri)
}
// translate references to local files within the json-schema package
- $uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri);
+ // a plain replacement, because the package path is data: a Windows path such as
+ // C:\Users\4223627 would otherwise have its \4 read as a backreference
+ if (0 === strpos($uri, self::PACKAGE_SCHEME)) {
+ $uri = sprintf('file://%s/', realpath(__DIR__ . '/../../..')) . substr($uri, strlen(self::PACKAGE_SCHEME));
+ }
return $uri;
}
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar.php
index 9f73a04e16..8454cb0790 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar.php
@@ -34,20 +34,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'a_year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'a_month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'a_week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'a_day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'a_hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'a_minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
- 'a_second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
+ 'year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'a_year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'a_month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'a_week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'a_day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'a_hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'a_minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
+ 'a_second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
'ago' => 'منذ :time',
'from_now' => ':time من الآن',
'after' => 'بعد :time',
@@ -61,7 +61,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+عند)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_DZ.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_DZ.php
index 01b800b0d8..297a687362 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_DZ.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_DZ.php
@@ -34,20 +34,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'a_year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'a_month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'a_week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'a_day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'a_hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'a_minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
- 'a_second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
+ 'year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'a_year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'a_month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'a_week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'a_day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'a_hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'a_minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
+ 'a_second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
'ago' => 'منذ :time',
'from_now' => 'في :time',
'after' => 'بعد :time',
@@ -61,7 +61,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+على)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_KW.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_KW.php
index 04cfafc2df..2e1dd5f339 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_KW.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_KW.php
@@ -35,20 +35,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'a_year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'a_month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'a_week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'a_day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'a_hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'a_minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
- 'a_second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
+ 'year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'a_year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'a_month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'a_week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'a_day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'a_hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'a_minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
+ 'a_second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
'ago' => 'منذ :time',
'from_now' => 'في :time',
'after' => 'بعد :time',
@@ -62,7 +62,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+على)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_MA.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_MA.php
index ac5392d77f..2ce193af24 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_MA.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_MA.php
@@ -33,20 +33,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'a_year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'a_month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'a_week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'a_day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'a_hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'a_minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
- 'a_second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
+ 'year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'a_year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'a_month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'a_week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'a_day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'a_hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'a_minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
+ 'a_second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
'ago' => 'منذ :time',
'from_now' => 'في :time',
'after' => 'بعد :time',
@@ -60,7 +60,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+على)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_SA.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_SA.php
index 7967c161e5..44d396e9fc 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_SA.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_SA.php
@@ -34,20 +34,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'a_year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'a_month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'a_week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'a_day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'a_hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'a_minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
- 'a_second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
+ 'year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'a_year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'a_month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'a_week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'a_day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'a_hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'a_minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
+ 'a_second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
'ago' => 'منذ :time',
'from_now' => 'في :time',
'after' => 'بعد :time',
@@ -61,7 +61,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+على)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_Shakl.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_Shakl.php
index 3ab8f23864..b5d2b9c2a3 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_Shakl.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_Shakl.php
@@ -34,20 +34,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سَنَة', '{1}سَنَة', '{2}سَنَتَيْن', ']2,11[:count سَنَوَات', ']10,Inf[:count سَنَة']),
- 'a_year' => implode('|', ['{0}:count سَنَة', '{1}سَنَة', '{2}سَنَتَيْن', ']2,11[:count سَنَوَات', ']10,Inf[:count سَنَة']),
- 'month' => implode('|', ['{0}:count شَهْرَ', '{1}شَهْرَ', '{2}شَهْرَيْن', ']2,11[:count أَشْهُر', ']10,Inf[:count شَهْرَ']),
- 'a_month' => implode('|', ['{0}:count شَهْرَ', '{1}شَهْرَ', '{2}شَهْرَيْن', ']2,11[:count أَشْهُر', ']10,Inf[:count شَهْرَ']),
- 'week' => implode('|', ['{0}:count أُسْبُوع', '{1}أُسْبُوع', '{2}أُسْبُوعَيْن', ']2,11[:count أَسَابِيع', ']10,Inf[:count أُسْبُوع']),
- 'a_week' => implode('|', ['{0}:count أُسْبُوع', '{1}أُسْبُوع', '{2}أُسْبُوعَيْن', ']2,11[:count أَسَابِيع', ']10,Inf[:count أُسْبُوع']),
- 'day' => implode('|', ['{0}:count يَوْم', '{1}يَوْم', '{2}يَوْمَيْن', ']2,11[:count أَيَّام', ']10,Inf[:count يَوْم']),
- 'a_day' => implode('|', ['{0}:count يَوْم', '{1}يَوْم', '{2}يَوْمَيْن', ']2,11[:count أَيَّام', ']10,Inf[:count يَوْم']),
- 'hour' => implode('|', ['{0}:count سَاعَة', '{1}سَاعَة', '{2}سَاعَتَيْن', ']2,11[:count سَاعَات', ']10,Inf[:count سَاعَة']),
- 'a_hour' => implode('|', ['{0}:count سَاعَة', '{1}سَاعَة', '{2}سَاعَتَيْن', ']2,11[:count سَاعَات', ']10,Inf[:count سَاعَة']),
- 'minute' => implode('|', ['{0}:count دَقِيقَة', '{1}دَقِيقَة', '{2}دَقِيقَتَيْن', ']2,11[:count دَقَائِق', ']10,Inf[:count دَقِيقَة']),
- 'a_minute' => implode('|', ['{0}:count دَقِيقَة', '{1}دَقِيقَة', '{2}دَقِيقَتَيْن', ']2,11[:count دَقَائِق', ']10,Inf[:count دَقِيقَة']),
- 'second' => implode('|', ['{0}:count ثَانِيَة', '{1}ثَانِيَة', '{2}ثَانِيَتَيْن', ']2,11[:count ثَوَان', ']10,Inf[:count ثَانِيَة']),
- 'a_second' => implode('|', ['{0}:count ثَانِيَة', '{1}ثَانِيَة', '{2}ثَانِيَتَيْن', ']2,11[:count ثَوَان', ']10,Inf[:count ثَانِيَة']),
+ 'year' => implode('|', [':count سَنَة', 'سَنَة', 'سَنَتَيْن', ':count سَنَوَات', ':count سَنَة', ':count سَنَة']),
+ 'a_year' => implode('|', [':count سَنَة', 'سَنَة', 'سَنَتَيْن', ':count سَنَوَات', ':count سَنَة', ':count سَنَة']),
+ 'month' => implode('|', [':count شَهْرَ', 'شَهْرَ', 'شَهْرَيْن', ':count أَشْهُر', ':count شَهْرَ', ':count شَهْرَ']),
+ 'a_month' => implode('|', [':count شَهْرَ', 'شَهْرَ', 'شَهْرَيْن', ':count أَشْهُر', ':count شَهْرَ', ':count شَهْرَ']),
+ 'week' => implode('|', [':count أُسْبُوع', 'أُسْبُوع', 'أُسْبُوعَيْن', ':count أَسَابِيع', ':count أُسْبُوع', ':count أُسْبُوع']),
+ 'a_week' => implode('|', [':count أُسْبُوع', 'أُسْبُوع', 'أُسْبُوعَيْن', ':count أَسَابِيع', ':count أُسْبُوع', ':count أُسْبُوع']),
+ 'day' => implode('|', [':count يَوْم', 'يَوْم', 'يَوْمَيْن', ':count أَيَّام', ':count يَوْم', ':count يَوْم']),
+ 'a_day' => implode('|', [':count يَوْم', 'يَوْم', 'يَوْمَيْن', ':count أَيَّام', ':count يَوْم', ':count يَوْم']),
+ 'hour' => implode('|', [':count سَاعَة', 'سَاعَة', 'سَاعَتَيْن', ':count سَاعَات', ':count سَاعَة', ':count سَاعَة']),
+ 'a_hour' => implode('|', [':count سَاعَة', 'سَاعَة', 'سَاعَتَيْن', ':count سَاعَات', ':count سَاعَة', ':count سَاعَة']),
+ 'minute' => implode('|', [':count دَقِيقَة', 'دَقِيقَة', 'دَقِيقَتَيْن', ':count دَقَائِق', ':count دَقِيقَة', ':count دَقِيقَة']),
+ 'a_minute' => implode('|', [':count دَقِيقَة', 'دَقِيقَة', 'دَقِيقَتَيْن', ':count دَقَائِق', ':count دَقِيقَة', ':count دَقِيقَة']),
+ 'second' => implode('|', [':count ثَانِيَة', 'ثَانِيَة', 'ثَانِيَتَيْن', ':count ثَوَان', ':count ثَانِيَة', ':count ثَانِيَة']),
+ 'a_second' => implode('|', [':count ثَانِيَة', 'ثَانِيَة', 'ثَانِيَتَيْن', ':count ثَوَان', ':count ثَانِيَة', ':count ثَانِيَة']),
'ago' => 'مُنْذُ :time',
'from_now' => 'مِنَ الْآن :time',
'after' => 'بَعْدَ :time',
@@ -63,7 +63,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+عند)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_TN.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_TN.php
index be04f4ef86..5d805223c4 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_TN.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ar_TN.php
@@ -32,20 +32,20 @@
];
return [
- 'year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'a_year' => implode('|', ['{0}:count سنة', '{1}سنة', '{2}سنتين', ']2,11[:count سنوات', ']10,Inf[:count سنة']),
- 'month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'a_month' => implode('|', ['{0}:count شهر', '{1}شهر', '{2}شهرين', ']2,11[:count أشهر', ']10,Inf[:count شهر']),
- 'week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'a_week' => implode('|', ['{0}:count أسبوع', '{1}أسبوع', '{2}أسبوعين', ']2,11[:count أسابيع', ']10,Inf[:count أسبوع']),
- 'day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'a_day' => implode('|', ['{0}:count يوم', '{1}يوم', '{2}يومين', ']2,11[:count أيام', ']10,Inf[:count يوم']),
- 'hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'a_hour' => implode('|', ['{0}:count ساعة', '{1}ساعة', '{2}ساعتين', ']2,11[:count ساعات', ']10,Inf[:count ساعة']),
- 'minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'a_minute' => implode('|', ['{0}:count دقيقة', '{1}دقيقة', '{2}دقيقتين', ']2,11[:count دقائق', ']10,Inf[:count دقيقة']),
- 'second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
- 'a_second' => implode('|', ['{0}:count ثانية', '{1}ثانية', '{2}ثانيتين', ']2,11[:count ثواني', ']10,Inf[:count ثانية']),
+ 'year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'a_year' => implode('|', [':count سنة', 'سنة', 'سنتين', ':count سنوات', ':count سنة', ':count سنة']),
+ 'month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'a_month' => implode('|', [':count شهر', 'شهر', 'شهرين', ':count أشهر', ':count شهر', ':count شهر']),
+ 'week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'a_week' => implode('|', [':count أسبوع', 'أسبوع', 'أسبوعين', ':count أسابيع', ':count أسبوع', ':count أسبوع']),
+ 'day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'a_day' => implode('|', [':count يوم', 'يوم', 'يومين', ':count أيام', ':count يوم', ':count يوم']),
+ 'hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'a_hour' => implode('|', [':count ساعة', 'ساعة', 'ساعتين', ':count ساعات', ':count ساعة', ':count ساعة']),
+ 'minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'a_minute' => implode('|', [':count دقيقة', 'دقيقة', 'دقيقتين', ':count دقائق', ':count دقيقة', ':count دقيقة']),
+ 'second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
+ 'a_second' => implode('|', [':count ثانية', 'ثانية', 'ثانيتين', ':count ثواني', ':count ثانية', ':count ثانية']),
'ago' => 'منذ :time',
'from_now' => 'في :time',
'after' => 'بعد :time',
@@ -59,7 +59,7 @@
'diff_tomorrow_regexp' => 'غدً?اً?(?:\\s+على)?(?:\\s+الساعة)?',
'diff_before_yesterday' => 'قبل الأمس',
'diff_after_tomorrow' => 'بعد غد',
- 'period_recurrences' => implode('|', ['{0}:count مرة', '{1}مرة', '{2}مرتين', ']2,11[:count مرات', ']10,Inf[:count مرة']),
+ 'period_recurrences' => implode('|', [':count مرة', 'مرة', 'مرتين', ':count مرات', ':count مرة', ':count مرة']),
'period_interval' => 'كل :interval',
'period_start_date' => 'من :date',
'period_end_date' => 'إلى :date',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Lang/ru.php b/core/vendor/nesbot/carbon/src/Carbon/Lang/ru.php
index fe9607f46f..320bdb4e76 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Lang/ru.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Lang/ru.php
@@ -67,10 +67,10 @@
'second' => ':count секунда|:count секунды|:count секунд',
's' => ':count сек.',
'a_second' => '{1}несколько секунд|:count секунду|:count секунды|:count секунд',
- 'millisecond' => '{1}:count миллисекунда|:count миллисекунды|:count миллисекунд',
+ 'millisecond' => ':count миллисекунда|:count миллисекунды|:count миллисекунд',
'a_millisecond' => '{1}миллисекунда|:count миллисекунда|:count миллисекунды|:count миллисекунд',
'ms' => ':count мс',
- 'microsecond' => '{1}:count микросекунда|:count микросекунды|:count микросекунд',
+ 'microsecond' => ':count микросекунда|:count микросекунды|:count микросекунд',
'a_microsecond' => '{1}микросекунда|:count микросекунда|:count микросекунды|:count микросекунд',
'ago' => static fn (string $time) => $transformDiff($time).' назад',
'from_now' => static fn (string $time) => 'через '.$transformDiff($time),
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Traits/Eras.php b/core/vendor/nesbot/carbon/src/Carbon/Traits/Eras.php
index e124a19187..a7ca76cd8f 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Traits/Eras.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Traits/Eras.php
@@ -200,9 +200,9 @@ private static function resolveIsoFormatEras(string $format, string $time, ?stri
foreach ($tokens as $index => $token) {
if ($isEraToken($token)) {
$eraTokens[$index] = $token;
- $eraPattern = $token[0] === 'N' ? self::getEraNamesPattern($eras, $token) : ($token === 'yo' ? '\\d+\\p{L}*' : '\\d+');
+ $eraPattern = $token[0] === 'N' ? self::getEraNamesPattern($eras, $token) : ($token === 'yo' ? '[0-9]+\\p{L}*' : '[0-9]+');
$pattern .= "($eraPattern)";
- $generic .= $token[0] === 'N' ? '(.+?)' : '(\\d+\\p{L}*)';
+ $generic .= $token[0] === 'N' ? '(.+?)' : '([0-9]+\\p{L}*)';
continue;
}
@@ -335,10 +335,10 @@ private static function getIsoTokenPattern(string $token): string
}
return match (true) {
- $token === 'YY' => '\d{2}',
- (bool) preg_match('/^(?:Y+|x|X)$/', $token) => '[+-]?\d+',
- (bool) preg_match('/^(?:DDD[Do]?|[Hh]mm(?:ss)?|S+|O[YMDHhms])$/', $token) => '\d+',
- (bool) preg_match('/^(?:[DMHhkms]{1,2}|[MD]o)$/', $token) => '\d{1,2}',
+ $token === 'YY' => '[0-9]{2}',
+ (bool) preg_match('/^(?:Y+|x|X)$/', $token) => '[+-]?[0-9]+',
+ (bool) preg_match('/^(?:DDD[Do]?|[Hh]mm(?:ss)?|S+|O[YMDHhms])$/', $token) => '[0-9]+',
+ (bool) preg_match('/^(?:[DMHhkms]{1,2}|[MD]o)$/', $token) => '[0-9]{1,2}',
$token === ' ' => '\s',
$token === '?' => '.',
$token === '!' || $token === '|' => '',
diff --git a/core/vendor/nesbot/carbon/src/Carbon/Traits/Week.php b/core/vendor/nesbot/carbon/src/Carbon/Traits/Week.php
index 0101d055eb..3f928b528d 100644
--- a/core/vendor/nesbot/carbon/src/Carbon/Traits/Week.php
+++ b/core/vendor/nesbot/carbon/src/Carbon/Traits/Week.php
@@ -85,6 +85,7 @@ public function weekYear($year = null, $dayOfWeek = null, $dayOfYear = null)
$week = $this->week(null, $dayOfWeek, $dayOfYear);
$day = $this->dayOfWeek;
+ $time = $this->avoidMutation();
$date = $this->year($year);
$date = match ($date->weekYear(null, $dayOfWeek, $dayOfYear) - $year) {
@@ -97,11 +98,11 @@ public function weekYear($year = null, $dayOfWeek = null, $dayOfYear = null)
->addWeeks($week - $date->week(null, $dayOfWeek, $dayOfYear))
->startOfWeek($dayOfWeek);
- if ($date->dayOfWeek === $day) {
- return $date;
+ if ($date->dayOfWeek !== $day) {
+ $date = $date->next($day);
}
- return $date->next($day);
+ return $date->setTimeFrom($time);
}
$year = $this->year;
diff --git a/core/vendor/symfony/console/Application.php b/core/vendor/symfony/console/Application.php
index 68acadf57e..41ddc95b7e 100644
--- a/core/vendor/symfony/console/Application.php
+++ b/core/vendor/symfony/console/Application.php
@@ -1045,7 +1045,9 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
// We register application signals, so that we can dispatch the event
foreach ($this->signalsToDispatchEvent as $signal) {
- $signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal);
+ // When this application runs inside another application that handles the signal, the outer application decides if the process exits
+ $previousHandler = pcntl_signal_get_handler($signal);
+ $signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal, \is_array($previousHandler) && $previousHandler[0] instanceof SignalRegistry ? false : 0);
$alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null;
$signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) {
diff --git a/core/vendor/symfony/console/Command/CompleteCommand.php b/core/vendor/symfony/console/Command/CompleteCommand.php
index 44d23cc638..bdc3a8c53e 100644
--- a/core/vendor/symfony/console/Command/CompleteCommand.php
+++ b/core/vendor/symfony/console/Command/CompleteCommand.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Command;
+use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
@@ -20,6 +21,7 @@
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
+use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -35,6 +37,7 @@ final class CompleteCommand extends Command
public const COMPLETION_API_VERSION = '1';
private array $completionOutputs;
+ private ?InputDefinition $fullDefinition = null;
private bool $isDebug = false;
/**
@@ -52,6 +55,53 @@ public function __construct(array $completionOutputs = [])
parent::__construct();
}
+ public function setApplication(?Application $application): void
+ {
+ $this->fullDefinition = null;
+
+ parent::setApplication($application);
+ }
+
+ public function setDefinition(array|InputDefinition $definition): static
+ {
+ $this->fullDefinition = null;
+
+ return parent::setDefinition($definition);
+ }
+
+ /**
+ * @internal
+ */
+ public function mergeApplicationDefinition(bool $mergeArgs = true): void
+ {
+ if (!$application = $this->getApplication()) {
+ return;
+ }
+
+ $definition = $this->getNativeDefinition();
+ $this->fullDefinition = new InputDefinition();
+ $this->fullDefinition->setOptions($definition->getOptions());
+
+ foreach ($application->getDefinition()->getOptions() as $option) {
+ // the completion scripts rely on the options of this command, they win over the conflicting application ones
+ if ($definition->hasOption($option->getName()) || array_filter(explode('|', $option->getShortcut() ?? ''), $definition->hasShortcut(...))) {
+ continue;
+ }
+
+ $this->fullDefinition->addOption($option);
+ }
+
+ if ($mergeArgs) {
+ $this->fullDefinition->setArguments($application->getDefinition()->getArguments());
+ }
+ $this->fullDefinition->addArguments($definition->getArguments());
+ }
+
+ public function getDefinition(): InputDefinition
+ {
+ return $this->fullDefinition ?? parent::getDefinition();
+ }
+
protected function configure(): void
{
$this
diff --git a/core/vendor/symfony/console/Command/InvokableCommand.php b/core/vendor/symfony/console/Command/InvokableCommand.php
index 016da5a018..808083578d 100644
--- a/core/vendor/symfony/console/Command/InvokableCommand.php
+++ b/core/vendor/symfony/console/Command/InvokableCommand.php
@@ -35,6 +35,11 @@
*/
class InvokableCommand implements SignalableCommandInterface
{
+ /**
+ * @var list
+ */
+ private static array $running = [];
+
private readonly ?SignalableCommandInterface $signalableCommand;
private readonly \ReflectionFunction $invokable;
/**
@@ -58,7 +63,13 @@ public function __construct(
*/
public function __invoke(InputInterface $input, OutputInterface $output): int
{
- $statusCode = $this->invokable->invoke(...$this->getParameters($this->invokable, $input, $output));
+ self::$running[] = [$this->invokable->getClosureThis(), $this->command];
+
+ try {
+ $statusCode = $this->invokable->invoke(...$this->getParameters($this->invokable, $input, $output));
+ } finally {
+ array_pop(self::$running);
+ }
if (!\is_int($statusCode)) {
if ($this->triggerDeprecations) {
@@ -114,6 +125,20 @@ public function getCode(): callable
return $this->code;
}
+ /**
+ * Returns the command that is currently running the given object, if any.
+ */
+ public static function getRunningCommand(object $code): ?Command
+ {
+ for ($i = \count(self::$running) - 1; 0 <= $i; --$i) {
+ if ($code === self::$running[$i][0]) {
+ return self::$running[$i][1];
+ }
+ }
+
+ return null;
+ }
+
private function getClosure(callable $code): \Closure
{
if (!$code instanceof \Closure) {
diff --git a/core/vendor/symfony/console/Command/LockableTrait.php b/core/vendor/symfony/console/Command/LockableTrait.php
index b7abd2fdc5..25261a7552 100644
--- a/core/vendor/symfony/console/Command/LockableTrait.php
+++ b/core/vendor/symfony/console/Command/LockableTrait.php
@@ -55,20 +55,22 @@ private function lock(?string $name = null, bool $blocking = false): bool
if (!$name) {
if ($this instanceof Command) {
$name = $this->getName();
+ } elseif ($command = InvokableCommand::getRunningCommand($this)) {
+ $name = $command->getName();
} elseif ($attribute = (new \ReflectionClass($this::class))->getAttributes(AsCommand::class)) {
- $name = $attribute[0]->newInstance()->name;
+ $name = explode('|', ltrim($attribute[0]->newInstance()->name, '|'), 2)[0];
} else {
throw new LogicException(\sprintf('Lock name missing: provide it via "%s()", #[AsCommand] attribute, or by extending Command class.', __METHOD__));
}
}
- $this->lock = $this->lockFactory->createLock($name);
- if (!$this->lock->acquire($blocking)) {
- $this->lock = null;
-
+ $lock = $this->lockFactory->createLock($name);
+ if (!$lock->acquire($blocking)) {
return false;
}
+ $this->lock = $lock;
+
return true;
}
@@ -77,9 +79,9 @@ private function lock(?string $name = null, bool $blocking = false): bool
*/
private function release(): void
{
- if ($this->lock) {
- $this->lock->release();
+ if ($lock = $this->lock) {
$this->lock = null;
+ $lock->release();
}
}
}
diff --git a/core/vendor/symfony/console/Formatter/OutputFormatter.php b/core/vendor/symfony/console/Formatter/OutputFormatter.php
index 1e757183e1..21800ebe6c 100644
--- a/core/vendor/symfony/console/Formatter/OutputFormatter.php
+++ b/core/vendor/symfony/console/Formatter/OutputFormatter.php
@@ -24,8 +24,12 @@
*/
class OutputFormatter implements WrappableOutputFormatterInterface
{
+ private const ANSI_PATTERN = '\\x1B(?:\\[[0-9;:]*m|\\]8;[^;]*;[^\\x1B]*\\x1B\\\\)';
+ private const ANSI_REGEX = '/'.self::ANSI_PATTERN.'/';
+
private array $styles = [];
private OutputFormatterStyleStack $styleStack;
+ private array $charWidths = [];
public function __clone()
{
@@ -128,6 +132,7 @@ public function formatAndWrap(?string $message, int $width): string
$openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
$closeTagRegex = '[a-z][^<>]*+';
$currentLineLength = 0;
+ $lineBreak = false;
preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $i => $match) {
$pos = $match[1];
@@ -139,7 +144,7 @@ public function formatAndWrap(?string $message, int $width): string
// tags are delimited by "<" and ">", which are ASCII, so a tag offset can never
// fall inside a multibyte sequence: slicing by bytes is safe here
- $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
+ $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $width, $currentLineLength, $lineBreak);
$offset = $pos + \strlen($text);
// opening tag?
@@ -153,7 +158,7 @@ public function formatAndWrap(?string $message, int $width): string
// >
$this->styleStack->pop();
} elseif (null === $style = $this->createStyleFromString($tag)) {
- $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength);
+ $output .= $this->applyCurrentStyle($text, $width, $currentLineLength, $lineBreak);
} elseif ($open) {
$this->styleStack->push($style);
} else {
@@ -161,9 +166,18 @@ public function formatAndWrap(?string $message, int $width): string
}
}
- $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
+ $output .= $this->applyCurrentStyle(substr($message, $offset), $width, $currentLineLength, $lineBreak);
+
+ if ($lineBreak) {
+ $output .= "\n";
+ }
+
+ if ($width && str_contains($output, "\r")) {
+ // keep "\r\n" line breaks together when ANSI codes end the line
+ $output = preg_replace('/\r((?:'.self::ANSI_PATTERN.')++)(?=\n|$)/', "\$1\r", $output);
+ }
- return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
+ return str_replace("\0", '\\', $output);
}
public function getStyleStack(): OutputFormatterStyleStack
@@ -176,8 +190,8 @@ public function getStyleStack(): OutputFormatterStyleStack
*/
private function createStyleFromString(string $string): ?OutputFormatterStyleInterface
{
- if (isset($this->styles[$string])) {
- return $this->styles[$string];
+ if (isset($this->styles[$lowerString = strtolower($string)])) {
+ return $this->styles[$lowerString];
}
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
@@ -213,58 +227,108 @@ private function createStyleFromString(string $string): ?OutputFormatterStyleInt
/**
* Applies current style from stack to text, if must be applied.
*/
- private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string
+ private function applyCurrentStyle(string $text, int $width, int &$currentLineLength, bool &$lineBreak): string
{
if ('' === $text) {
return '';
}
- if (!$width) {
+ if (!$width || str_contains($text, "\e") && '' === preg_replace(self::ANSI_REGEX, '', $text)) {
+ $text = str_replace(['\\<', '\\>'], ['<', '>'], $text);
+
return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text;
}
- if (!$currentLineLength && '' !== $current) {
- $text = ltrim($text);
+ $text = str_replace(['\\<', '\\>'], ['<', '>'], $text);
+ $prefix = '';
+ $visibleText = str_contains($text, "\e") ? preg_replace(self::ANSI_REGEX, '', $text) : $text;
+
+ if ($currentLineLength >= $width) {
+ // the current line is full: drop the whitespace that follows and break the line before the next character
+ if (str_contains($text, "\e")) {
+ $text = preg_replace_callback('/^(?:[\\x00\\x09\\x0B\\x0D\\x20]|'.self::ANSI_PATTERN.')++/', static function ($match) {
+ preg_match_all(self::ANSI_REGEX, $match[0], $ansi);
+
+ return implode('', $ansi[0]);
+ }, $text);
+ $visibleText = ltrim($visibleText, " \t\r\0\x0B");
+ } else {
+ $text = $visibleText = ltrim($text, " \t\r\0\x0B");
+ }
+
+ // whitespace at the end of the message still breaks the line
+ if ($lineBreak = '' === $visibleText) {
+ return '' === $text || !$this->isDecorated() ? $text : $this->styleStack->getCurrent()->apply($text);
+ }
+
+ if ("\n" !== $visibleText[0]) {
+ $prefix = "\n";
+ }
+
+ $currentLineLength = 0;
}
+ $trailingNewlines = substr($visibleText, \strlen(rtrim($visibleText, "\r\n")));
+
if ($currentLineLength) {
$lines = explode("\n", $text, 2);
- $prefix = Helper::substr($lines[0], 0, $i = $width - $currentLineLength)."\n";
- $text = Helper::substr($lines[0], $i);
+ $eol = "\n";
+ if (isset($lines[1]) && str_ends_with($lines[0], "\r")) {
+ $lines[0] = substr($lines[0], 0, -1);
+ $eol = "\r\n";
+ }
+ $i = $width - $currentLineLength;
+ if ($widths = $this->getWidths($lines[0])) {
+ $room = $i;
+ $i = \count($widths);
+ foreach ($widths as $j => $charWidth) {
+ if (0 > $room -= $charWidth) {
+ $i = $j;
+ break;
+ }
+ }
+ }
+ $prefix = ($i ? $this->substr($lines[0], 0, $i) : '')."\n";
+ $text = $i ? $this->substr($lines[0], $i) : $lines[0];
if (isset($lines[1])) {
// $prefix may contain the full first line in which the \n is already a part of $prefix.
if ('' !== $text) {
- $text .= "\n";
+ $text .= $eol;
+ } else {
+ $prefix = substr($prefix, 0, -1).$eol;
}
$text .= $lines[1];
}
- } else {
- $prefix = '';
}
- preg_match('~(\\n)$~', $text, $matches);
$text = $prefix.$this->addLineBreaks($text, $width);
- $text = rtrim($text, "\n").($matches[1] ?? '');
-
- if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) {
- $text = "\n".$text;
+ if (str_contains($text, "\e")) {
+ $parts = preg_split('/('.self::ANSI_PATTERN.')/', $text, -1, \PREG_SPLIT_DELIM_CAPTURE);
+ $sequences = '';
+ for ($i = \count($parts) - 1; $i >= 0; --$i) {
+ if ($i % 2) {
+ $sequences = $parts[$i].$sequences;
+ } elseif ('' !== $parts[$i] = rtrim($parts[$i], "\r\n")) {
+ break;
+ }
+ }
+ $text = implode('', \array_slice($parts, 0, $i + 1)).$sequences;
+ } else {
+ $text = rtrim($text, "\r\n");
}
+ $text .= $trailingNewlines;
$lines = explode("\n", $text);
foreach ($lines as $i => $line) {
- $currentLineLength = 0 === $i ? $currentLineLength + Helper::length($line) : Helper::length($line);
- if ($width <= $currentLineLength) {
- $currentLineLength = 0;
- }
+ $lineLength = array_sum($this->getWidths($line)) ?: Helper::length(str_contains($line, "\e") ? preg_replace(self::ANSI_REGEX, '', $line) : $line);
+ $currentLineLength = 0 === $i ? $currentLineLength + $lineLength : $lineLength;
}
- if ($this->isDecorated()) {
- foreach ($lines as $i => $line) {
- $lines[$i] = $this->styleStack->getCurrent()->apply($line);
- }
+ foreach ($lines as $i => $line) {
+ $lines[$i] = $this->isDecorated() ? $this->styleStack->getCurrent()->apply($line) : $line;
}
return implode("\n", $lines);
@@ -273,7 +337,156 @@ private function applyCurrentStyle(string $text, string $current, int $width, in
private function addLineBreaks(string $text, int $width): string
{
$encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';
+ $widths = $this->getWidths($text);
+
+ if (!$widths && !str_contains($text, "\e") && !str_contains($text, "\r\n")) {
+ return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
+ }
+
+ $chars = [];
+ $mask = $pending = '';
+ foreach ($this->getTokens($text) as $token) {
+ if (preg_match(self::ANSI_REGEX, $token)) {
+ $pending .= $token;
+ continue;
+ }
+ $chars[] = [$pending, $token];
+ $pending = '';
+ $mask .= "\n" === $token || "\r\n" === $token ? '#' : (' ' === $token ? ' ' : '?');
+ }
+
+ if (!$chars) {
+ return $pending;
+ }
+
+ $wrapped = rtrim($mask, '#');
+ $mask = ($widths ? $this->wordwrap($wrapped, $width, $widths) : wordwrap($wrapped, $width, '#', true)).substr($mask, \strlen($wrapped));
+ $result = '';
+ $j = 0;
+ foreach (str_split($mask) as $char) {
+ if ('#' === $char) {
+ $eol = "\n";
+ if (isset($chars[$j]) && ("\n" === $chars[$j][1] || "\r\n" === $chars[$j][1] || ' ' === $chars[$j][1])) {
+ $eol = ' ' === $chars[$j][1] ? "\n" : $chars[$j][1];
+ $result .= $chars[$j++][0];
+ }
+ $result .= $eol;
+ } else {
+ $result .= implode('', $chars[$j++]);
+ }
+ }
+
+ return $result.$pending;
+ }
+
+ private function substr(string $text, int $from, ?int $length = null): string
+ {
+ if (!str_contains($text, "\e")) {
+ return Helper::substr($text, $from, $length);
+ }
+
+ $result = '';
+ $position = 0;
+ foreach ($this->getTokens($text) as $token) {
+ $isAnsi = preg_match(self::ANSI_REGEX, $token);
+ if ($position >= $from && (null === $length || $position < $from + $length || $isAnsi && $position === $from + $length)
+ && (!$isAnsi || !$from || $position > $from)
+ ) {
+ $result .= $token;
+ }
+ if (!$isAnsi) {
+ ++$position;
+ }
+ }
+
+ return $result;
+ }
+
+ private function getTokens(string $text): iterable
+ {
+ $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';
+ foreach (preg_split('/('.self::ANSI_PATTERN.')/', $text, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $part) {
+ if (preg_match(self::ANSI_REGEX, $part)) {
+ yield $part;
+ continue;
+ }
+ foreach (b($part)->toUnicodeString($encoding)->chunk() as $char) {
+ yield (string) $char->toByteString($encoding);
+ }
+ }
+ }
+
+ /**
+ * Returns the number of columns of each visible character, or an empty array when none uses more than one column.
+ *
+ * @return list
+ */
+ private function getWidths(string $text): array
+ {
+ if (!preg_match('/[^\x00-\x{10FF}]/u', $text)) {
+ return [];
+ }
+
+ $widths = [];
+ $wide = false;
+ foreach ($this->getTokens($text) as $token) {
+ if (!preg_match(self::ANSI_REGEX, $token)) {
+ $widths[] = $charWidth = 2 < \strlen($token) ? ($this->charWidths[$token] ??= Helper::width($token)) : 1;
+ $wide = $wide || 1 < $charWidth;
+ }
+ }
+
+ return $wide ? $widths : [];
+ }
+
+ /**
+ * Wraps a mask like wordwrap() does when cutting long words, but using the number of columns of each character.
+ *
+ * @param list $widths
+ */
+ private function wordwrap(string $mask, int $width, array $widths): string
+ {
+ $result = '';
+ $start = $lastSpace = $lineWidth = 0;
+
+ for ($i = 0, $length = \strlen($mask); $i < $length; ++$i) {
+ if ('#' === $mask[$i]) {
+ $result .= substr($mask, $start, $i + 1 - $start);
+ $start = $lastSpace = $i + 1;
+ $lineWidth = 0;
+
+ continue;
+ }
+
+ if (' ' === $mask[$i]) {
+ if ($lineWidth >= $width) {
+ $result .= substr($mask, $start, $i - $start).'#';
+ $start = $i + 1;
+ $lineWidth = 0;
+ } else {
+ ++$lineWidth;
+ }
+ $lastSpace = $i;
+
+ continue;
+ }
+
+ if ($lineWidth + $widths[$i] > $width && $start < $lastSpace) {
+ $result .= substr($mask, $start, $lastSpace - $start).'#';
+ $start = $lastSpace = $lastSpace + 1;
+ $lineWidth = array_sum(\array_slice($widths, $start, $i - $start));
+ }
+
+ // cut long words, and also before a wide character that still doesn't fit after breaking at the last space
+ if ($lineWidth && $lineWidth + $widths[$i] > $width) {
+ $result .= substr($mask, $start, $i - $start).'#';
+ $start = $lastSpace = $i;
+ $lineWidth = 0;
+ }
+
+ $lineWidth += $widths[$i];
+ }
- return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
+ return $result.substr($mask, $start);
}
}
diff --git a/core/vendor/symfony/console/Formatter/OutputFormatterStyle.php b/core/vendor/symfony/console/Formatter/OutputFormatterStyle.php
index 20a65b517c..de800f98b9 100644
--- a/core/vendor/symfony/console/Formatter/OutputFormatterStyle.php
+++ b/core/vendor/symfony/console/Formatter/OutputFormatterStyle.php
@@ -40,12 +40,14 @@ public function __construct(?string $foreground = null, ?string $background = nu
public function setForeground(?string $color): void
{
- $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
+ $this->color = new Color($foreground = $color ?: '', $this->background, $this->options);
+ $this->foreground = $foreground;
}
public function setBackground(?string $color): void
{
- $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
+ $this->color = new Color($this->foreground, $background = $color ?: '', $this->options);
+ $this->background = $background;
}
public function setHref(string $url): void
@@ -55,23 +57,20 @@ public function setHref(string $url): void
public function setOption(string $option): void
{
+ $this->color = new Color($this->foreground, $this->background, [...$this->options, $option]);
$this->options[] = $option;
- $this->color = new Color($this->foreground, $this->background, $this->options);
}
public function unsetOption(string $option): void
{
- $pos = array_search($option, $this->options);
- if (false !== $pos) {
- unset($this->options[$pos]);
- }
-
+ $this->options = array_diff($this->options, [$option]);
$this->color = new Color($this->foreground, $this->background, $this->options);
}
public function setOptions(array $options): void
{
- $this->color = new Color($this->foreground, $this->background, $this->options = $options);
+ $this->color = new Color($this->foreground, $this->background, $options);
+ $this->options = $options;
}
public function apply(string $text): string
diff --git a/core/vendor/symfony/console/Helper/ProgressBar.php b/core/vendor/symfony/console/Helper/ProgressBar.php
index 0ce4986439..3982a3a468 100644
--- a/core/vendor/symfony/console/Helper/ProgressBar.php
+++ b/core/vendor/symfony/console/Helper/ProgressBar.php
@@ -521,13 +521,6 @@ private function overwrite(string $message): void
--$lineCount;
}
- foreach ($messageLines as $messageLine) {
- $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
- if ($messageLineLength > $this->terminal->getWidth()) {
- $lineCount += floor($messageLineLength / $this->terminal->getWidth());
- }
- }
-
$this->output->clear($lineCount);
} else {
$lineCount = substr_count($this->previousMessage, "\n");
diff --git a/core/vendor/symfony/console/Helper/Table.php b/core/vendor/symfony/console/Helper/Table.php
index 80d7ed6ee0..aef14b9858 100644
--- a/core/vendor/symfony/console/Helper/Table.php
+++ b/core/vendor/symfony/console/Helper/Table.php
@@ -15,6 +15,7 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
+use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
@@ -50,7 +51,9 @@ class Table
private array $columnWidths = [];
private array $configuredColumnWidths = [];
private array $columnMaxWidths = [];
- private bool $rendered = false;
+ private array $renderedLines = [];
+ private array $tagStyles = [];
+ private ?\WeakMap $rowspanLines = null;
private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT;
private static array $styles;
@@ -235,16 +238,40 @@ public function addRow(TableSeparator|array $row): static
*/
public function appendRow(TableSeparator|array $row): static
{
- if (!$this->output instanceof ConsoleSectionOutput) {
+ $output = $this->output;
+ if (!$output instanceof ConsoleSectionOutput) {
throw new RuntimeException(\sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
}
- if ($this->rendered) {
- $this->output->clear($this->calculateRowCount());
+ if (!$output->isDecorated()) {
+ $this->addRow($row);
+ $this->render();
+
+ return $this;
+ }
+
+ $previous = $this->renderedLines;
+ if ($previous && !str_ends_with($output->getContent(), implode('', $previous))) {
+ // the section was cleared or written to since the table was rendered: keep its content and render the whole table again
+ $previous = [];
}
$this->addRow($row);
- $this->render();
+ $lines = $this->renderLines();
+
+ // Only rewrite the lines after the part both renderings have in common.
+ // While no column gets wider, that is just the bottom border, so lines that scrolled out of the terminal, which can't be cleared, are not touched.
+ $common = 0;
+ $max = min(\count($previous), \count($lines));
+ while ($common < $max && $previous[$common] === $lines[$common]) {
+ ++$common;
+ }
+
+ if (\count($previous) > $common) {
+ $output->clear(\count($previous) - $common);
+ }
+ $output->write(implode('', \array_slice($lines, $common)), false, OutputInterface::OUTPUT_RAW);
+ $this->renderedLines = $lines;
return $this;
}
@@ -314,6 +341,15 @@ public function setVertical(bool $vertical = true): static
*/
public function render(): void
{
+ if ($this->output instanceof ConsoleSectionOutput && $this->output->isDecorated()) {
+ // remember the lines on screen, so that appendRow() can rewrite only the ones that change
+ $lines = $this->renderLines();
+ $this->output->write(implode('', $lines), false, OutputInterface::OUTPUT_RAW);
+ $this->renderedLines = $lines;
+
+ return;
+ }
+
$divider = new TableSeparator();
$isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2;
@@ -323,13 +359,13 @@ public function render(): void
$rows = [];
if ($horizontal) {
foreach ($this->headers[0] ?? [] as $i => $header) {
- $rows[$i] = [$header];
+ $rows[$i] = [$this->closeStyles($header)];
foreach ($this->rows as $row) {
if ($row instanceof TableSeparator) {
continue;
}
if (isset($row[$i])) {
- $rows[$i][] = $row[$i];
+ $rows[$i][] = $this->closeStyles($row[$i]);
} elseif ($isCellWithColspan($rows[$i][0])) {
// Noop, there is a "title"
} else {
@@ -339,7 +375,8 @@ public function render(): void
}
} elseif ($vertical) {
$formatter = $this->output->getFormatter();
- $maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0);
+ $headers = array_map($this->closeStyles(...), $this->headers[0] ?? []);
+ $maxHeaderLength = array_reduce($headers, static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0);
foreach ($this->rows as $row) {
if ($row instanceof TableSeparator) {
@@ -357,13 +394,12 @@ public function render(): void
}
}
- $headers = $this->headers[0] ?? [];
$maxRows = max(\count($headers), \count($row));
for ($i = 0; $i < $maxRows; ++$i) {
- $cell = (string) ($row[$i] ?? '');
+ $cell = (string) $this->closeStyles($row[$i] ?? '');
$eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
- $parts = explode($eol, $cell);
+ $parts = explode($eol, $this->closeStyles($cell, $eol));
foreach ($parts as $idx => $part) {
if ($headers && !$containsColspan) {
if (0 === $idx) {
@@ -387,7 +423,8 @@ public function render(): void
}
}
} else {
- $rows = array_merge($this->headers, [$divider], $this->rows);
+ $closeStyles = $this->closeStyles(...);
+ $rows = array_map(static fn ($row) => $row instanceof TableSeparator ? $row : array_map($closeStyles, $row), array_merge($this->headers, [$divider], $this->rows));
}
$this->calculateNumberOfColumns($rows);
@@ -395,9 +432,13 @@ public function render(): void
$rowGroups = $this->buildTableRows($rows);
$this->calculateColumnsWidth($rowGroups);
+ $isMarkdown = self::$styles['markdown'] === $this->style;
+ // a markdown table must start with a header row, so one is added when the table has none
+ $needsMarkdownHeader = $isMarkdown && ($horizontal || $vertical || !array_filter($this->headers));
$isHeader = !$horizontal;
$isFirstRow = $horizontal;
- $hasTitle = (bool) $this->headerTitle;
+ // a title would break the delimiter row of a markdown table
+ $hasTitle = (bool) $this->headerTitle && !$isMarkdown;
$previousRow = null;
$pendingSeparators = 0;
@@ -407,7 +448,8 @@ public function render(): void
foreach ($rowGroup as $row) {
if ($divider === $row) {
$isHeader = false;
- $isFirstRow = true;
+ // the delimiter row of a markdown table is written after its first header row
+ $isFirstRow = !$isMarkdown;
continue;
}
@@ -423,6 +465,13 @@ public function render(): void
continue;
}
+ if ($needsMarkdownHeader) {
+ $this->renderRow(array_fill(0, $this->numberOfColumns, ''), $this->style->getCellHeaderFormat());
+ $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
+ $needsMarkdownHeader = false;
+ $isFirstRow = false;
+ }
+
for (; $pendingSeparators > 0; --$pendingSeparators) {
$this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow, $row);
}
@@ -462,6 +511,12 @@ public function render(): void
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
}
+ if ($isMarkdown && $isHeader) {
+ // a markdown table has a single header row, so the next header rows are rendered as body rows
+ $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
+ $isHeader = false;
+ }
+
$previousRow = $row;
}
}
@@ -475,7 +530,6 @@ public function render(): void
}
$this->cleanup();
- $this->rendered = true;
}
/**
@@ -604,34 +658,25 @@ private function renderCell(array $row, int $column, string $cellFormat): string
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
- $width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0");
+ $width += \strlen($cell) - Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell));
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
return \sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
}
- $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
$content = \sprintf($style->getCellRowContentFormat(), $cell);
$padType = $style->getPadType();
if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
- $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
+ // the lines of a cell that spans rows get the style of the cell, even when a tag wraps them
+ $isNotStyledByTag = isset($this->rowspanLines[$cell]) || !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
if ($isNotStyledByTag) {
$cellFormat = $cell->getStyle()->getCellFormat();
if (!\is_string($cellFormat)) {
$tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';');
$cellFormat = '<'.$tag.'>%s>';
}
-
- if (str_contains($content, '>')) {
- $content = str_replace('>', '', $content);
- $width -= 3;
- }
- if (str_contains($content, '')) {
- $content = str_replace('', '', $content);
- $width -= \strlen('');
- }
}
$padType = $cell->getStyle()->getPadByAlign();
@@ -661,6 +706,7 @@ private function buildTableRows(array $rows): TableRows
{
/** @var WrappableOutputFormatterInterface $formatter */
$formatter = $this->output->getFormatter();
+ $isMarkdown = self::$styles['markdown'] === $this->style;
$unmergedRows = [];
for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
$rows = $this->fillNextRows($rows, $rowKey);
@@ -690,8 +736,9 @@ private function buildTableRows(array $rows): TableRows
}
$cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell));
- if ($minWrappedWidth && $cellWidth > $minWrappedWidth) {
- $cell = $formatter->formatAndWrap($cell, $minWrappedWidth);
+ if ($wrapped = $minWrappedWidth && $cellWidth > $minWrappedWidth) {
+ // the wrapped cell is already formatted and gets formatted again when rendered
+ $cell = str_replace(['<', '>'], ['\\<', '\\>'], $formatter->formatAndWrap($cell, $minWrappedWidth));
}
// update minimal columnWidths for spanned columns
if ($colspan > 1 && $minWrappedWidth > 0) {
@@ -717,13 +764,19 @@ private function buildTableRows(array $rows): TableRows
$this->columnWidths[$i] = max($this->configuredColumnWidths[$i] ?? 0, $cellWidth + $lengthColumnBorder);
}
}
- if (!str_contains($cell ?? '', "\n")) {
+ if ($isMarkdown && !$cell instanceof TableSeparator) {
+ // double the backslashes before a so that markdown does not escape it, and add one that the output formatter removes
+ $escaped = preg_replace(['/\\\\++(?=[\r\n])/', '/\r\n?|\n/'], ['$0$0\\\\', ' '], str_replace('|', '\|', $cell ?? ''));
+ $rows[$rowKey][$column] = $cell instanceof TableCell || $colspan > 1 ? new TableCell($escaped, ['colspan' => $colspan, 'style' => $cell instanceof TableCell ? $cell->getStyle() : null]) : $escaped;
+
continue;
}
- $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
- $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell)));
+ if (!$wrapped && !str_contains($cell ?? '', "\n")) {
+ continue;
+ }
+ $escaped = implode("\n", array_map(OutputFormatter::escapeTrailingBackslash(...), preg_split('/\r?\n/', $wrapped ? $cell : $this->closeStyles(str_replace("\r\n", "\n", $cell), "\n"))));
$cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
- $lines = explode($eol, str_replace($eol, '>'.$eol, $cell));
+ $lines = explode("\n", str_replace("\n", '>'."\n", $cell));
foreach ($lines as $lineKey => $line) {
if ($colspan > 1) {
$line = new TableCell($line, ['colspan' => $colspan]);
@@ -754,21 +807,6 @@ private function buildTableRows(array $rows): TableRows
});
}
- private function calculateRowCount(): int
- {
- $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));
-
- if ($this->headers) {
- ++$numberOfRows; // Add row for header separator
- }
-
- if ($this->rows) {
- ++$numberOfRows; // Add row for footer separator
- }
-
- return $numberOfRows;
- }
-
/**
* fill rows that contains rowspan > 1.
*
@@ -776,6 +814,7 @@ private function calculateRowCount(): int
*/
private function fillNextRows(array $rows, int $line): array
{
+ $this->rowspanLines ??= new \WeakMap();
$unmergedRows = [];
foreach ($rows[$line] as $column => $cell) {
if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) {
@@ -786,10 +825,11 @@ private function fillNextRows(array $rows, int $line): array
$lines = [$cell];
if (str_contains($cell, "\n")) {
$eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
- $lines = explode($eol, str_replace($eol, ''.$eol.'>', $cell));
+ $lines = explode($eol, $this->closeStyles($cell, $eol));
$nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines;
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
+ $this->rowspanLines[$rows[$line][$column]] = true;
unset($lines[0]);
}
@@ -798,6 +838,7 @@ private function fillNextRows(array $rows, int $line): array
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
$value = $lines[$unmergedRowKey - $line] ?? '';
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
+ $this->rowspanLines[$unmergedRows[$unmergedRowKey][$column]] = true;
if ($nbLines === $unmergedRowKey - $line) {
break;
}
@@ -836,6 +877,10 @@ private function fillCells(iterable $row): iterable
foreach ($row as $column => $cell) {
$newRow[] = $cell;
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
+ if (self::$styles['markdown'] === $this->style) {
+ // markdown has no colspan and needs one cell per column, so the cell only fills the first column it spans
+ $newRow[array_key_last($newRow)] = new TableCell((string) $cell, ['style' => $cell->getStyle()]);
+ }
foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
// insert empty value at column position
$newRow[] = '';
@@ -859,6 +904,77 @@ private function copyRow(array $rows, int $line): array
return $row;
}
+ /**
+ * Closes the styles that a cell leaves open at its end.
+ *
+ * With $eol, closes the styles that span a line break at the end of each line, and reopens them on the next line.
+ */
+ private function closeStyles(mixed $cell, ?string $eol = null): mixed
+ {
+ if (!\is_string($cell) && !$cell instanceof \Stringable || !str_contains($value = (string) $cell, '<') || null !== $eol && !str_contains($value, $eol) || !($formatter = $this->output->getFormatter()) instanceof OutputFormatter) {
+ return $cell;
+ }
+
+ $lines = null === $eol ? [$value] : explode($eol, $value);
+ // the tags that are open at the start of each line, then at the end of the last one, with the codes of their style and their closing tag
+ $openTags = [[]];
+ foreach ($lines as $i => $line) {
+ $tags = $openTags[$i];
+ preg_match_all('#<(?:[a-z](?:[^\\\\<>]*+|\\\\.)*|/(?:[a-z][^<>]*+)?)>#i', $line, $matches, \PREG_OFFSET_CAPTURE);
+
+ foreach ($matches[0] as [$tag, $offset]) {
+ if ($offset && '\\' === $line[$offset - 1]) {
+ continue;
+ }
+
+ if ('>' !== $tag) {
+ // the formatter pushes the style of a style tag, and prints any other tag as is
+ $name = ltrim(substr($tag, 1), '/');
+ $this->tagStyles[$name] ??= '' === $formatter->format('<'.$name) ? $formatter->getStyleStack()->pop()->apply('') : false;
+ if (false === $style = $this->tagStyles[$name]) {
+ continue;
+ }
+
+ if ('/' !== $tag[1]) {
+ // a closing tag cannot contain an escaped "<" or ">"
+ $tags[] = [$tag, $style, strpbrk(substr($tag, 1, -1), '<>') ? '>' : ''.substr($tag, 1)];
+ continue;
+ }
+ }
+
+ if (!$tags) {
+ continue;
+ } elseif ('>' === $tag) {
+ array_pop($tags);
+ } else {
+ // like the formatter, close the last tag that has the same style, and the ones opened after it
+ for ($j = \count($tags) - 1; $j >= 0; --$j) {
+ if ($style === $tags[$j][1]) {
+ array_splice($tags, $j);
+ break;
+ }
+ }
+ }
+ }
+
+ $openTags[] = $tags;
+ }
+
+ if (!array_filter(\array_slice($openTags, 1, null === $eol ? null : -1))) {
+ return $cell;
+ }
+
+ foreach ($lines as $i => $line) {
+ // closing tags go before the trailing backslashes of the line, which would escape them otherwise
+ $text = rtrim($line, '\\');
+ $line = $text.implode('', array_reverse(array_column($openTags[$i + 1], 2))).substr($line, \strlen($text));
+ $lines[$i] = implode('', array_column($openTags[$i], 0)).$line;
+ }
+ $value = implode($eol ?? '', $lines);
+
+ return $cell instanceof TableCell ? new TableCell($value, ['rowspan' => $cell->getRowspan(), 'colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]) : $value;
+ }
+
/**
* Gets number of columns by row.
*/
@@ -908,7 +1024,7 @@ private function calculateColumnsWidth(iterable $groups): void
if ($textLength > 0) {
$contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan()));
foreach ($contentColumns as $position => $content) {
- $row[$i + $position] = $content;
+ $row[$i + $position] = OutputFormatter::escape($content);
}
}
}
@@ -939,7 +1055,7 @@ private function getCellWidth(array $row, int $column): int
$columnWidth = $this->columnWidths[$column] ?? 0;
$cellWidth = max($cellWidth, $columnWidth);
- return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
+ return isset($this->columnMaxWidths[$column]) && self::$styles['markdown'] !== $this->style ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
}
/**
@@ -948,6 +1064,8 @@ private function getCellWidth(array $row, int $column): int
private function cleanup(): void
{
$this->effectiveColumnWidths = [];
+ $this->tagStyles = [];
+ $this->rowspanLines = null;
unset($this->numberOfColumns);
}
@@ -1016,4 +1134,22 @@ private function resolveStyle(TableStyle|string $name): TableStyle
return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
}
+
+ /**
+ * @return string[]
+ */
+ private function renderLines(): array
+ {
+ $output = $this->output;
+ $buffer = new BufferedOutput($output->getVerbosity(), true, $output->getFormatter());
+ $this->output = $buffer;
+
+ try {
+ $this->render();
+ } finally {
+ $this->output = $output;
+ }
+
+ return preg_split('/(?<=\n)/', $buffer->fetch(), -1, \PREG_SPLIT_NO_EMPTY) ?: [];
+ }
}
diff --git a/core/vendor/symfony/console/Helper/TerminalInputHelper.php b/core/vendor/symfony/console/Helper/TerminalInputHelper.php
index d0bc120ae9..fe6d695ee7 100644
--- a/core/vendor/symfony/console/Helper/TerminalInputHelper.php
+++ b/core/vendor/symfony/console/Helper/TerminalInputHelper.php
@@ -102,7 +102,8 @@ public function finish(): void
// and shell_exec() gives no way to detect that failure. Try the exact restore first so
// any custom terminal settings survive, but always fall back to "stty sane" so the
// terminal is left in a usable state even when the exact restore silently fails.
- shell_exec('stty '.$this->initialState.' 2>/dev/null || stty sane');
+ $nullDevice = '\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null';
+ shell_exec('stty '.$this->initialState.' 2>'.$nullDevice.' || stty sane');
$this->signalToKill = 0;
foreach ($this->signalHandlers as $signal => $originalHandler) {
diff --git a/core/vendor/symfony/console/Input/ArgvInput.php b/core/vendor/symfony/console/Input/ArgvInput.php
index 74f3fca21a..b919667b07 100644
--- a/core/vendor/symfony/console/Input/ArgvInput.php
+++ b/core/vendor/symfony/console/Input/ArgvInput.php
@@ -270,6 +270,13 @@ private function addLongOption(string $name, mixed $value): void
}
public function getFirstArgument(): ?string
+ {
+ $position = $this->getFirstArgumentPosition();
+
+ return null === $position ? null : $this->tokens[$position];
+ }
+
+ private function getFirstArgumentPosition(): ?int
{
$isOption = false;
foreach ($this->tokens as $i => $token) {
@@ -295,7 +302,7 @@ public function getFirstArgument(): ?string
continue;
}
- return $token;
+ return $i;
}
return null;
@@ -364,20 +371,9 @@ public function getRawTokens(bool $strip = false): array
return $this->tokens;
}
- $parameters = [];
- $keep = false;
- foreach ($this->tokens as $value) {
- if (!$keep && $value === $this->getFirstArgument()) {
- $keep = true;
-
- continue;
- }
- if ($keep) {
- $parameters[] = $value;
- }
- }
+ $position = $this->getFirstArgumentPosition();
- return $parameters;
+ return null === $position ? [] : \array_slice($this->tokens, $position + 1);
}
/**
diff --git a/core/vendor/symfony/console/Input/StringInput.php b/core/vendor/symfony/console/Input/StringInput.php
index a70f048f98..1392bef2d2 100644
--- a/core/vendor/symfony/console/Input/StringInput.php
+++ b/core/vendor/symfony/console/Input/StringInput.php
@@ -63,7 +63,7 @@ private function tokenize(string $input): array
$token = null;
}
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) {
- $token .= $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1)));
+ $token .= $match[1].$match[2].stripcslashes(preg_replace('/'.self::REGEX_QUOTED_STRING.'/', '$1$2', $match[3]));
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
$token .= stripcslashes(substr($match[0], 1, -1));
} elseif (preg_match('/'.self::REGEX_UNQUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
diff --git a/core/vendor/symfony/console/Output/ConsoleSectionOutput.php b/core/vendor/symfony/console/Output/ConsoleSectionOutput.php
index a45d177a10..b86ca7f424 100644
--- a/core/vendor/symfony/console/Output/ConsoleSectionOutput.php
+++ b/core/vendor/symfony/console/Output/ConsoleSectionOutput.php
@@ -52,7 +52,8 @@ public function setMaxHeight(int $maxHeight): void
$this->maxHeight = $maxHeight;
$existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines);
- parent::doWrite($this->getVisibleContent(), false);
+ $visibleContent = $this->getVisibleContent();
+ parent::doWrite($visibleContent, '' !== $visibleContent && !str_ends_with($visibleContent, \PHP_EOL));
parent::doWrite($existingContent, false);
}
@@ -68,17 +69,31 @@ public function clear(?int $lines = null): void
}
if ($lines) {
- array_splice($this->content, -$lines);
+ $width = $this->terminal->getWidth();
+ foreach (array_splice($this->content, -$lines) as $line) {
+ $lines += $this->getDisplayHeight($line, $width) - 1;
+ }
} else {
$lines = $this->lines;
$this->content = [];
}
+ $previousLines = $this->lines;
+
// callers may ask to clear more lines than this section tracks (e.g. ProgressBar
// counts "\n"-separated lines while addContent() splits on PHP_EOL), so keep the
// counter from going negative, which would break the max-height bookkeeping
$this->lines = max(0, $this->lines - $lines);
+ if ($this->maxHeight && $previousLines > $this->maxHeight) {
+ // the section was scrolled: redraw it to show the lines that are visible again
+ $erasedContent = $this->popStreamContentUntilCurrentSection($this->maxHeight);
+ parent::doWrite($this->getVisibleContent(), false);
+ parent::doWrite($erasedContent, false);
+
+ return;
+ }
+
parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false);
}
@@ -157,7 +172,7 @@ public function addContent(string $input, bool $newline = true): int
&& !str_ends_with($lastLine, \PHP_EOL)
) {
// deduct the line count of the previous line
- $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1;
+ $this->lines -= $this->getDisplayHeight($lastLine, $width);
// concatenate previous and new line
$lineContent = $lastLine.$lineContent;
// replace last entry of `$this->content` with the new expanded line
@@ -167,7 +182,7 @@ public function addContent(string $input, bool $newline = true): int
$this->content[] = $lineContent;
}
- $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
+ $linesAdded += $this->getDisplayHeight($lineContent, $width);
}
$this->lines += $linesAdded;
@@ -198,25 +213,30 @@ protected function doWrite(string $message, bool $newline): void
return;
}
+ if ('' === $message && !$newline) {
+ return;
+ }
+
// Check if the previous line (last entry of `$this->content`) needs to be continued
// (i.e. does not end with a line break). In which case, it needs to be erased first.
- $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0;
+ $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL);
+ $linesToClear = $deleteLastLine ? $this->getDisplayHeight($lastLine, $this->terminal->getWidth()) : 0;
+ $previousLines = $this->lines;
+
+ $this->addContent($message, $newline);
- $linesAdded = $this->addContent($message, $newline);
+ if ($this->maxHeight > 0 && $this->lines > $this->maxHeight) {
+ // on overflow, clear the lines displayed so far and redraw the section (to remove the first lines)
+ $erasedContent = $this->popStreamContentUntilCurrentSection(min($this->maxHeight, $previousLines));
+ $visibleContent = $this->getVisibleContent();
+ parent::doWrite($visibleContent, !str_ends_with($visibleContent, \PHP_EOL));
+ parent::doWrite($erasedContent, false);
- if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) {
- // on overflow, clear the whole section and redraw again (to remove the first lines)
- $linesToClear = $this->maxHeight;
+ return;
}
$erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear);
- if ($lineOverflow) {
- // redraw existing lines of the section
- $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded);
- parent::doWrite(implode('', $previousLinesOfSection), false);
- }
-
// if the last line was removed, re-print its content together with the new content.
// otherwise, just print the new content.
parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true);
@@ -256,8 +276,45 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr
return implode('', array_reverse($erasedContent));
}
- private function getDisplayLength(string $text): int
+ private function getDisplayHeight(string $line, int $width): int
{
- return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text)));
+ if ($width <= 0) {
+ return 1;
+ }
+
+ // the line break must not count as a column, or a line as wide as the terminal would count as two
+ if (str_ends_with($line, \PHP_EOL)) {
+ $line = substr($line, 0, -\strlen(\PHP_EOL));
+ }
+
+ // formatting the line again would remove literal tags, so only the ANSI sequences are removed
+ $line = preg_replace(['/\e\[[^m]*m/', '/\e]8;[^;]*;[^\e]*\e\\\\/'], '', $line);
+ $lineWidth = Helper::width(str_replace("\t", ' ', $line));
+
+ if ($lineWidth <= $width || !preg_match('/[\t\x80-\xFF]/', $line) || !preg_match('//u', $line)) {
+ return (int) ceil($lineWidth / $width) ?: 1;
+ }
+
+ // a wide character that does not fit at the end of a row is moved to the next one
+ $rows = 1;
+ $column = 0;
+ $offset = 0;
+ $widths = [];
+ while (false !== $char = grapheme_extract($line, 1, \GRAPHEME_EXTR_COUNT, $offset, $offset)) {
+ if ("\t" === $char) {
+ // a tab moves the cursor to the next multiple of 8 columns, but never past the last column
+ $column = min($column - $column % 8 + 8, $width - 1);
+ continue;
+ }
+
+ $charWidth = $widths[$char] ??= Helper::width($char);
+ if ($column + $charWidth > $width) {
+ ++$rows;
+ $column = 0;
+ }
+ $column += $charWidth;
+ }
+
+ return $rows;
}
}
diff --git a/core/vendor/symfony/error-handler/DebugClassLoader.php b/core/vendor/symfony/error-handler/DebugClassLoader.php
index 3fbb526652..311da36619 100644
--- a/core/vendor/symfony/error-handler/DebugClassLoader.php
+++ b/core/vendor/symfony/error-handler/DebugClassLoader.php
@@ -406,11 +406,11 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
$parent = get_parent_class($class) ?: null;
self::$returnTypes[$class] = [];
- $classIsTemplate = false;
+ $classTemplates = [];
// Detect annotations on the class
if ($doc = $this->parsePhpDoc($refl)) {
- $classIsTemplate = isset($doc['template']) || isset($doc['template-covariant']);
+ $classTemplates = [...$doc['template'] ?? [], ...$doc['template-covariant'] ?? []];
foreach (['final', 'deprecated', 'internal'] as $annotation) {
if (null !== $description = $doc[$annotation][0] ?? null) {
@@ -431,7 +431,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
self::$method[$class][] = [$class, $static, $returnType, $name.$signature, $description];
if ('' !== $returnType) {
- $this->setReturnType($returnType, $refl->name, $name, $refl->getFileName(), $parent);
+ $this->setReturnType($returnType, $refl->name, $name, $refl->getFileName(), $parent, null, $classTemplates);
}
}
}
@@ -575,8 +575,9 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
// To read method annotations
$doc = $this->parsePhpDoc($method);
+ $templates = isset($doc['template']) || isset($doc['template-covariant']) ? [...$classTemplates, ...$doc['template'] ?? [], ...$doc['template-covariant'] ?? []] : $classTemplates;
- if (($classIsTemplate || isset($doc['template']) || isset($doc['template-covariant'])) && $method->hasReturnType()) {
+ if ($templates && $method->hasReturnType()) {
unset($doc['return']);
}
@@ -636,7 +637,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
}
if (isset($doc['return'])) {
- $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType());
+ $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType(), $templates);
if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
$this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
@@ -837,7 +838,7 @@ private function getOwnInterfaces(string $class, ?string $parent): array
return $ownInterfaces;
}
- private function setReturnType(string $types, string $class, string $method, string $filename, ?string $parent, ?\ReflectionType $returnType = null): void
+ private function setReturnType(string $types, string $class, string $method, string $filename, ?string $parent, ?\ReflectionType $returnType = null, array $templates = []): void
{
if ('__construct' === $method) {
return;
@@ -858,6 +859,18 @@ private function setReturnType(string $types, string $class, string $method, str
$typesMap = [];
$glue = str_contains($types, '&') ? '&' : '|';
foreach (explode($glue, $types) as $t) {
+ if (\in_array($t, $templates, true)) {
+ // there is no native type for a template, but $class remains the declaring class for its child classes
+ if (isset(self::$returnTypes[$class][$method])) {
+ self::$returnTypes[$class][$method][2] = $class;
+ } else {
+ // a null entry hides the types that child classes get from their own interfaces
+ self::$returnTypes[$class][$method] = null;
+ }
+
+ return;
+ }
+
$t = self::SPECIAL_RETURN_TYPES[strtolower($t)] ?? $t;
$typesMap[$this->normalizeType($t, $class, $parent, $returnType)][$t] = $t;
}
@@ -1329,6 +1342,12 @@ private function parsePhpDoc(\Reflector $reflector): array
$tags['param'][$param] = $type;
}
+ foreach (['template', 'template-covariant'] as $k) {
+ if ($templates = [...$tags[$k] ?? [], ...$tags['phpstan-'.$k] ?? [], ...$tags['psalm-'.$k] ?? []]) {
+ $tags[$k] = array_map(static fn ($v) => substr($v, 0, strpos($v, ' ') ?: \strlen($v)), $templates);
+ }
+ }
+
foreach (['var', 'return'] as $k) {
if (null === $v = $tags[$k][0] ?? null) {
continue;
diff --git a/core/vendor/symfony/error-handler/ErrorHandler.php b/core/vendor/symfony/error-handler/ErrorHandler.php
index 0b95b8922b..78d772105f 100644
--- a/core/vendor/symfony/error-handler/ErrorHandler.php
+++ b/core/vendor/symfony/error-handler/ErrorHandler.php
@@ -93,6 +93,7 @@ class ErrorHandler
private bool $isRecursive = false;
private bool $isRoot = false;
+ private bool $customExceptionHandlerCalled = false;
/** @var callable|null */
private $exceptionHandler;
private ?BufferingLogger $bootstrappingLogger = null;
@@ -529,6 +530,25 @@ public function handleException(\Throwable $exception): void
} catch (\Throwable $handlerException) {
$loggerFailed = true;
}
+ } elseif ([$this, 'renderException'] === $this->exceptionHandler && !$this->customExceptionHandlerCalled && !isset($this->loggers[$type][0]) && !$exception instanceof FatalError && (error_reporting() & \E_ERROR) && filter_var(\ini_get('log_errors'), \FILTER_VALIDATE_BOOL)) {
+ // renderException() replaces PHP's native handler, so log the exception like PHP does. PHP already logged fatal errors.
+ // In CLI, PHP logs to STDERR when error_log is empty, and renderException() already displays the exception.
+ // PHP logs this line with the error priority. error_log() uses the notice one, except with the SAPI logger.
+ if (($errorLog = \ini_get('error_log')) || !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
+ try {
+ $message = \sprintf("PHP Fatal error: Uncaught %s\n thrown in %s on line %d", $this->parseAnonymousClass((string) $exception), $exception->getFile(), $exception->getLine());
+
+ if ('syslog' === $errorLog && \function_exists('openlog') && \function_exists('syslog')) {
+ $facility = \ini_get('syslog.facility');
+ $facility = 'security' === $facility ? 'LOG_AUTH' : (str_starts_with($facility, 'LOG_') ? $facility : 'LOG_'.strtoupper($facility));
+ openlog(\ini_get('syslog.ident'), 0, \defined($facility) ? \constant($facility) : \LOG_USER);
+ syslog(\LOG_ERR, $message);
+ } else {
+ error_log($message, $errorLog ? 0 : 4);
+ }
+ } catch (\Throwable) {
+ }
+ }
}
$exception = $this->enhanceError($exception);
@@ -538,6 +558,9 @@ public function handleException(\Throwable $exception): void
if (null === $exceptionHandler || $exceptionHandler === $this->exceptionHandler) {
$this->exceptionHandler = null;
+ } else {
+ // renderException() is now the fallback of a custom handler, which is in charge of logging
+ $this->customExceptionHandlerCalled = true;
}
try {
diff --git a/core/vendor/symfony/finder/Gitignore.php b/core/vendor/symfony/finder/Gitignore.php
index 7764588983..97e4ddc606 100644
--- a/core/vendor/symfony/finder/Gitignore.php
+++ b/core/vendor/symfony/finder/Gitignore.php
@@ -46,7 +46,7 @@ private static function buildRegex(string $gitignoreFileContent, bool $inverted)
continue;
}
- $line = preg_replace('~(? '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', $regex);
+ $parts = preg_split('~\\\\(.)~s', $gitignoreLine, -1, \PREG_SPLIT_DELIM_CAPTURE);
+
+ if (str_ends_with(end($parts), '\\')) {
+ return '$f'; // a trailing backslash never matches
+ }
+
+ $regex = '';
+ foreach ($parts as $i => $part) {
+ // escaped characters are written in hex so that the replacements below leave them alone
+ $regex .= $i % 2 ? \sprintf('\x%02X', \ord($part)) : preg_quote($part, '~');
+ }
+ $regex = preg_replace_callback('~\\\\\[((?:\\\\[!^])?+)((?:\\\\\])?+[^\]]*)\\\\\]~', static fn (array $matches): string => ('' !== $matches[1] ? '(?!/)[^' : '[').self::rangesToRegex($matches[2]).']', $regex);
$regex = preg_replace('~(?:(?:\\\\\*){2,}(/?))+~', '(?:(?:(?!//).(? 4 === \strlen($c) ? hexdec(substr($c, 2)) : \ord($c[-1]), [$m[1], $m[2]]);
+
+ return $from <= $to ? $m[1].'-'.$m[2] : $m[1];
+ }, $class) ?? $class;
+ }
}
diff --git a/core/vendor/symfony/finder/Glob.php b/core/vendor/symfony/finder/Glob.php
index ac11e8e0de..e0d2e24564 100644
--- a/core/vendor/symfony/finder/Glob.php
+++ b/core/vendor/symfony/finder/Glob.php
@@ -42,7 +42,8 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool
{
$firstByte = true;
$escaping = false;
- $inCurlies = 0;
+ $curlies = [];
+ $bracketScanEnd = 0;
$regex = '';
if ($unanchored = str_starts_with($glob, '**/')) {
$glob = '/'.$glob;
@@ -76,6 +77,8 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool
if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) {
$regex .= "\\$car";
+ } elseif ('[' === $car && !$escaping && null !== $class = self::bracketExpressionToRegex($glob, $i, $delimiter, $bracketScanEnd)) {
+ $regex .= $class;
} elseif ('*' === $car) {
if ($escaping || $strictWildcardSlash || !str_ends_with($regex, '.*')) {
// ".*.*" matches the same as ".*" but backtracks quadratically
@@ -84,16 +87,19 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool
} elseif ('?' === $car) {
$regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.');
} elseif ('{' === $car) {
- $regex .= $escaping ? '\\{' : '(';
if (!$escaping) {
- ++$inCurlies;
+ $curlies[] = [\strlen($regex)];
}
- } elseif ('}' === $car && $inCurlies) {
+ $regex .= $escaping ? '\\{' : '(';
+ } elseif ('}' === $car && $curlies) {
$regex .= $escaping ? '}' : ')';
if (!$escaping) {
- --$inCurlies;
+ array_pop($curlies);
+ }
+ } elseif (',' === $car && $curlies) {
+ if (!$escaping) {
+ $curlies[\count($curlies) - 1][] = \strlen($regex);
}
- } elseif (',' === $car && $inCurlies) {
$regex .= $escaping ? ',' : '|';
} elseif ('\\' === $car) {
if ($escaping) {
@@ -104,16 +110,76 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool
}
continue;
+ } elseif ($escaping && ('[' === $car || ']' === $car)) {
+ $regex .= '\\'.$car;
} else {
$regex .= $car;
}
$escaping = false;
}
+ // A "{" that no "}" closes is a literal character, and so are the "," it contains
+ $prefix = '';
+ $start = 0;
+ foreach (array_merge(...$curlies) as $offset) {
+ $prefix .= substr($regex, $start, $offset - $start).('(' === $regex[$offset] ? '\\{' : ',');
+ $start = $offset + 1;
+ }
+ $regex = $prefix.substr($regex, $start);
+
if ($unanchored) {
$regex = substr_replace($regex, '?', 1 + ('/' === $delimiter) + ($strictLeadingDot ? \strlen('(?=[^\.])') : 0), 0);
}
return $delimiter.'^'.$regex.'$'.$delimiter;
}
+
+ /**
+ * Returns null when a "/" comes before the closing "]", and "\[" when no "]" closes the "[" at position $i.
+ *
+ * $scanEnd is where the last scan stopped without finding a "]": every "[" before that position gets the same result.
+ */
+ private static function bracketExpressionToRegex(string $glob, int &$i, string $delimiter, int &$scanEnd): ?string
+ {
+ $sizeGlob = \strlen($glob);
+
+ if ($i < $scanEnd) {
+ return $scanEnd < $sizeGlob ? null : '\\[';
+ }
+
+ $class = '[';
+
+ for ($j = $i + 1; $j < $sizeGlob; ++$j) {
+ $car = $glob[$j];
+
+ if (']' === $car && $j > $i + 1) {
+ $i = $j;
+
+ return $class.']';
+ }
+
+ if ('/' === $car || ('\\' === $car && '/' === ($glob[$j + 1] ?? ''))) {
+ $scanEnd = $j;
+
+ return null;
+ }
+
+ if ('[' === $car && preg_match('/\[:[a-z]++:\]/A', $glob, $m, 0, $j)) {
+ $class .= $m[0];
+ $j += \strlen($m[0]) - 1;
+ } elseif ('\\' === $car) {
+ if (!isset($glob[++$j])) {
+ break;
+ }
+ $class .= preg_quote($glob[$j], $delimiter);
+ } elseif ($delimiter === $car || str_contains('.()|+^$', $car)) {
+ $class .= '\\'.$car;
+ } else {
+ $class .= $car;
+ }
+ }
+ $scanEnd = $sizeGlob;
+
+ return '\\[';
+ }
}
diff --git a/core/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php b/core/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php
index 3450c49d43..2a4c0b8afe 100644
--- a/core/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php
+++ b/core/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php
@@ -87,12 +87,12 @@ protected function isRegex(string $str): bool
$end = substr($m[1], -1);
if ($start === $end) {
- return !preg_match('/[*?[:alnum:] \\\\]/', $start);
+ return !preg_match('/[*?[:alnum:] \\\\]/', $start) && false !== @preg_match($str, '');
}
foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) {
if ($start === $delimiters[0] && $end === $delimiters[1]) {
- return true;
+ return false !== @preg_match($str, '');
}
}
}
diff --git a/core/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php b/core/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php
index f5fd2d4dc5..c8702d5ee7 100644
--- a/core/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php
+++ b/core/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php
@@ -25,6 +25,7 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
{
private bool $ignoreUnreadableDirs;
private bool $ignoreFirstRewind = true;
+ private array $realPaths = [];
// these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations
private string $rootPath;
@@ -46,6 +47,9 @@ public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs
if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) {
$this->directorySeparator = \DIRECTORY_SEPARATOR;
}
+ if ($flags & self::FOLLOW_SYMLINKS && false !== ($realPath = realpath($path))) {
+ $this->realPaths[$realPath] = true;
+ }
}
/**
@@ -76,6 +80,11 @@ public function hasChildren(bool $allowLinks = false): bool
{
$hasChildren = parent::hasChildren($allowLinks);
+ // don't loop on a directory that resolves to one of its parents
+ if ($hasChildren && $this->realPaths && false !== ($realPath = realpath($this->getPathname())) && isset($this->realPaths[$realPath])) {
+ return false;
+ }
+
if (!$hasChildren || !$this->ignoreUnreadableDirs) {
return $hasChildren;
}
@@ -104,6 +113,8 @@ public function getChildren(): \RecursiveDirectoryIterator
// performance optimization to avoid redoing the same work in all children
$children->rootPath = $this->rootPath;
+
+ $children->realPaths += $this->realPaths;
}
return $children;
diff --git a/core/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php b/core/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php
index 3715e269ed..cc5e45a5e8 100644
--- a/core/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php
+++ b/core/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php
@@ -18,7 +18,10 @@
*/
final class VcsIgnoredFilterIterator extends \FilterIterator
{
+ private string $searchDir;
+ private string $realSearchDir;
private string $baseDir;
+ private ?string $gitInfoExcludeFile = null;
/**
* @var array|null>
@@ -35,12 +38,14 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
*/
public function __construct(\Iterator $iterator, string $baseDir)
{
- // paths are compared to the real path of each file, so the base directory needs the same treatment
+ $this->searchDir = rtrim($this->normalizePath($baseDir), '/').'/';
$this->baseDir = $this->normalizePath(realpath($baseDir) ?: $baseDir);
+ $this->realSearchDir = rtrim($this->baseDir, '/').'/';
foreach ([$this->baseDir, ...$this->parentDirectoriesUpwards($this->baseDir)] as $directory) {
if (@file_exists("{$directory}/.git")) {
$this->baseDir = $directory;
+ $this->gitInfoExcludeFile = $this->getGitInfoExcludePath();
break;
}
}
@@ -50,21 +55,24 @@ public function __construct(\Iterator $iterator, string $baseDir)
public function accept(): bool
{
- $file = $this->current();
+ $path = $this->normalizePath($this->current()->getPathname());
- $fileRealPath = $this->normalizePath($file->getRealPath());
+ // git matches a symlink by its own path, so only the search directory is resolved
+ if (str_starts_with($path, $this->searchDir)) {
+ $path = $this->realSearchDir.substr($path, \strlen($this->searchDir));
+ }
- return !$this->isIgnored($fileRealPath);
+ return !$this->isIgnored($path);
}
- private function isIgnored(string $fileRealPath): bool
+ private function isIgnored(string $path): bool
{
- if (isset($this->ignoredPathsCache[$fileRealPath])) {
- return $this->ignoredPathsCache[$fileRealPath];
+ if (isset($this->ignoredPathsCache[$path])) {
+ return $this->ignoredPathsCache[$path];
}
$ignored = false;
- $parentDirectories = $this->parentDirectoriesDownwards($fileRealPath);
+ $parentDirectories = $this->parentDirectoriesDownwards($path);
if ($parentDirectories && $this->baseDir !== $parentDirectory = $parentDirectories[\count($parentDirectories) - 1]) {
// paths inside an ignored directory are ignored and cannot be re-included
@@ -72,15 +80,24 @@ private function isIgnored(string $fileRealPath): bool
}
if (!$ignored) {
- $isDir = is_dir($fileRealPath);
+ $isDir = !is_link($path) && is_dir($path);
+ $gitignoreFiles = [];
- // the last matching rule of the deepest .gitignore file decides
foreach (array_reverse($parentDirectories) as $parentDirectory) {
- if (null === $rules = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) {
+ $gitignoreFiles["{$parentDirectory}/.gitignore"] = $parentDirectory;
+ }
+
+ if (null !== $this->gitInfoExcludeFile && $this->baseDir === ($parentDirectories[0] ?? null)) {
+ $gitignoreFiles[$this->gitInfoExcludeFile] = $this->baseDir;
+ }
+
+ // the last matching rule of the deepest .gitignore file decides, and the exclude file of the repository comes after the root one
+ foreach ($gitignoreFiles as $gitignoreFile => $parentDirectory) {
+ if (null === $rules = $this->readGitignoreFile($gitignoreFile)) {
continue;
}
- $fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1);
+ $fileRelativePath = substr($path, \strlen($parentDirectory) + 1);
foreach ($rules as [$regex, $isNegated, $isDirOnly]) {
if ($isDirOnly && !$isDir) {
@@ -96,7 +113,7 @@ private function isIgnored(string $fileRealPath): bool
}
}
- return $this->ignoredPathsCache[$fileRealPath] = $ignored;
+ return $this->ignoredPathsCache[$path] = $ignored;
}
/**
@@ -140,6 +157,32 @@ private function parentDirectoriesDownwards(string $fileRealPath): array
);
}
+ private function getGitInfoExcludePath(): ?string
+ {
+ $gitDir = "{$this->baseDir}/.git";
+
+ // worktrees and submodules have a .git file that points to their git directory, which can be outside of open_basedir
+ if (!is_dir($gitDir)) {
+ if (!preg_match('~^gitdir:\s*(.+)~i', (string) @file_get_contents($gitDir), $matches)) {
+ return null;
+ }
+
+ $gitDir = $this->resolveGitPath(trim($matches[1]), $this->baseDir);
+
+ // the git directory of a worktree points to the common directory, where the exclude file is
+ if (false !== $commonDir = @file_get_contents("{$gitDir}/commondir")) {
+ $gitDir = $this->resolveGitPath(trim($commonDir), $gitDir);
+ }
+ }
+
+ return @is_file("{$gitDir}/info/exclude") ? "{$gitDir}/info/exclude" : null;
+ }
+
+ private function resolveGitPath(string $path, string $from): string
+ {
+ return preg_match('~^(?:[a-zA-Z]:)?[/\\\\]~', $path) ? $path : "{$from}/{$path}";
+ }
+
/**
* Returns the rules of a .gitignore file, last one first, as [regex, isNegated, isDirOnly] tuples.
*
diff --git a/core/vendor/symfony/http-foundation/BinaryFileResponse.php b/core/vendor/symfony/http-foundation/BinaryFileResponse.php
index 6f2b884524..e16cd6b921 100644
--- a/core/vendor/symfony/http-foundation/BinaryFileResponse.php
+++ b/core/vendor/symfony/http-foundation/BinaryFileResponse.php
@@ -187,7 +187,7 @@ public function setContentDisposition(string $disposition, string $filename = ''
public function prepare(Request $request): static
{
- if ($this->isInformational() || $this->isEmpty()) {
+ if (!$this->isSuccessful() || $this->isEmpty()) {
parent::prepare($request);
$this->maxlen = 0;
@@ -260,13 +260,15 @@ public function prepare(Request $request): static
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
$range = $request->headers->get('Range');
- if (str_starts_with($range, 'bytes=')) {
- [$start, $end] = explode('-', substr($range, 6), 2) + [1 => 0];
+ if (preg_match('/^bytes=(\d*)-(\d*)$/i', $range, $matches) && ('' !== $matches[1] || '' !== $matches[2])) {
+ $start = $matches[1];
+ $end = $matches[2];
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
if ('' === $start) {
- $start = $fileSize - $end;
+ // a suffix longer than the file selects the whole file, see RFC 9110 section 14.1.2
+ $start = max(0, $fileSize - $end);
$end = $fileSize - 1;
} else {
$start = (int) $start;
diff --git a/core/vendor/symfony/http-foundation/CHANGELOG.md b/core/vendor/symfony/http-foundation/CHANGELOG.md
index 321d58e755..6e8f42a1bc 100644
--- a/core/vendor/symfony/http-foundation/CHANGELOG.md
+++ b/core/vendor/symfony/http-foundation/CHANGELOG.md
@@ -8,7 +8,7 @@ CHANGELOG
* Add support for the `QUERY` HTTP method
* Add support for structured MIME suffix
* Add `Request::set/getAllowedHttpMethodOverride()` to list which HTTP methods can be overridden
- * Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
+ * Deprecate using `Response::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
* Deprecate method `Request::get()`, use properties `->attributes`, `query` or `request` directly instead
* Make `Request::createFromGlobals()` parse the body of PUT, DELETE, PATCH and QUERY requests
* Deprecate HTTP method override for methods GET, HEAD, CONNECT and TRACE; it will be ignored in Symfony 8.0
diff --git a/core/vendor/symfony/http-foundation/Cookie.php b/core/vendor/symfony/http-foundation/Cookie.php
index 10127176b3..bf847d2c96 100644
--- a/core/vendor/symfony/http-foundation/Cookie.php
+++ b/core/vendor/symfony/http-foundation/Cookie.php
@@ -51,17 +51,62 @@ public static function fromString(string $cookie, bool $decode = false): static
'partitioned' => false,
];
- $parts = HeaderUtils::split($cookie, ';=');
- $part = array_shift($parts);
+ [$pair, $attributes] = explode(';', $cookie, 2) + ['', ''];
+ $part = explode('=', $pair, 2);
- $name = $decode ? urldecode($part[0]) : $part[0];
- $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null;
+ $name = trim($part[0]);
+ $value = isset($part[1]) ? trim($part[1]) : null;
+ $name = $decode ? urldecode($name) : $name;
+ $value = $decode && null !== $value ? urldecode($value) : $value;
- $data = HeaderUtils::combine($parts) + $data;
- $data['expires'] = self::expiresTimestamp($data['expires']);
+ foreach (explode(';', $attributes) as $attribute) {
+ $attribute = explode('=', $attribute, 2);
+ $attributeName = strtolower(trim($attribute[0]));
+ $attributeValue = trim($attribute[1] ?? '');
- if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) {
- $data['expires'] = time() + (int) $data['max-age'];
+ if ('raw' === $attributeName || !\array_key_exists($attributeName, $data) && 'max-age' !== $attributeName) {
+ continue;
+ }
+
+ if (\in_array($attributeName, ['secure', 'httponly', 'partitioned'], true)) {
+ $attributeValue = true;
+ } elseif ('expires' === $attributeName) {
+ // double quotes are delimiters in cookie dates, see RFC 6265, section 5.1.1
+ $attributeValue = str_replace('"', '', $attributeValue);
+
+ if (is_numeric($attributeValue)) {
+ $attributeValue = self::expiresTimestamp($attributeValue);
+ } elseif (false === $attributeValue = strtotime($attributeValue)) {
+ continue;
+ } else {
+ $attributeValue = max(1, $attributeValue);
+ }
+ } elseif ('max-age' === $attributeName) {
+ if (!preg_match('/^-?\d+$/D', $attributeValue)) {
+ continue;
+ }
+ } elseif ('samesite' === $attributeName) {
+ $attributeValue = strtolower($attributeValue);
+
+ if (!\in_array($attributeValue, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE], true)) {
+ continue;
+ }
+ }
+
+ $data[$attributeName] = $attributeValue;
+ }
+
+ if (isset($data['max-age'])) {
+ $maxAge = (int) $data['max-age'];
+ $now = time();
+
+ if ($maxAge > 0) {
+ $data['expires'] = $now + min($maxAge, \PHP_INT_MAX - $now);
+ } elseif ($data['expires'] > $now) {
+ $data['expires'] = $now;
+ } elseif (0 === $data['expires']) {
+ $data['expires'] = 1;
+ }
}
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']);
@@ -107,7 +152,7 @@ public function __construct(
throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $name));
}
- if (!$name) {
+ if ('' === $name) {
throw new \InvalidArgumentException('The cookie name cannot be empty.');
}
diff --git a/core/vendor/symfony/http-foundation/InputBag.php b/core/vendor/symfony/http-foundation/InputBag.php
index 8c153dc30e..2dae232514 100644
--- a/core/vendor/symfony/http-foundation/InputBag.php
+++ b/core/vendor/symfony/http-foundation/InputBag.php
@@ -141,12 +141,56 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
$options['flags'] |= \FILTER_NULL_ON_FAILURE;
- $value = filter_var($value, $filter, $options);
+ $invalid = false;
+ $forceArray = !\is_array($value) && ($options['flags'] & \FILTER_FORCE_ARRAY) && !($options['flags'] & \FILTER_REQUIRE_ARRAY);
+
+ if (!$nullOnFailure && \is_array($value) && $value && !($options['flags'] & \FILTER_REQUIRE_SCALAR) && ($options['flags'] & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) {
+ $options['flags'] &= ~(\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY);
+ $value = self::filterArray($value, $filter, $options, $invalid);
+ } else {
+ if ($forceArray) {
+ $options['flags'] &= ~\FILTER_FORCE_ARRAY;
+ }
+
+ $value = filter_var($value, $filter, $options);
+ $invalid = null === $value;
+
+ if ($forceArray) {
+ $value = [$value];
+ }
+ }
- if (null !== $value || $nullOnFailure) {
+ if (!$invalid || $nullOnFailure) {
return $value;
}
throw new BadRequestException(\sprintf('Input value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key));
}
+
+ private static function filterArray(array $value, int $filter, array $options, bool &$invalid, array &$references = []): array
+ {
+ foreach ($value as $key => $item) {
+ if (!\is_array($item)) {
+ $value[$key] = filter_var($item, $filter, $options);
+ $invalid = $invalid || null === $value[$key];
+ continue;
+ }
+
+ $id = \ReflectionReference::fromArrayElement($value, $key)?->getId();
+ if (null !== $id && isset($references[$id])) {
+ continue;
+ }
+ if (null !== $id) {
+ $references[$id] = true;
+ }
+
+ $value[$key] = self::filterArray($item, $filter, $options, $invalid, $references);
+
+ if (null !== $id) {
+ unset($references[$id]);
+ }
+ }
+
+ return $value;
+ }
}
diff --git a/core/vendor/symfony/http-foundation/ParameterBag.php b/core/vendor/symfony/http-foundation/ParameterBag.php
index 5e36cf2f9d..1998806fb8 100644
--- a/core/vendor/symfony/http-foundation/ParameterBag.php
+++ b/core/vendor/symfony/http-foundation/ParameterBag.php
@@ -242,9 +242,26 @@ public function filter(string $key, mixed $default = null, int $filter = \FILTER
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
$options['flags'] |= \FILTER_NULL_ON_FAILURE;
- $value = filter_var($value, $filter, $options);
+ $invalid = false;
+ $forceArray = !\is_array($value) && ($options['flags'] & \FILTER_FORCE_ARRAY) && !($options['flags'] & \FILTER_REQUIRE_ARRAY);
+
+ if (!$nullOnFailure && \is_array($value) && $value && !($options['flags'] & \FILTER_REQUIRE_SCALAR) && ($options['flags'] & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) {
+ $options['flags'] &= ~(\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY);
+ $value = self::filterArray($value, $filter, $options, $invalid);
+ } else {
+ if ($forceArray) {
+ $options['flags'] &= ~\FILTER_FORCE_ARRAY;
+ }
+
+ $value = filter_var($value, $filter, $options);
+ $invalid = null === $value;
+
+ if ($forceArray) {
+ $value = [$value];
+ }
+ }
- if (null !== $value || $nullOnFailure) {
+ if (!$invalid || $nullOnFailure) {
return $value;
}
@@ -268,4 +285,31 @@ public function count(): int
{
return \count($this->parameters);
}
+
+ private static function filterArray(array $value, int $filter, array $options, bool &$invalid, array &$references = []): array
+ {
+ foreach ($value as $key => $item) {
+ if (!\is_array($item)) {
+ $value[$key] = filter_var($item, $filter, $options);
+ $invalid = $invalid || null === $value[$key];
+ continue;
+ }
+
+ $id = \ReflectionReference::fromArrayElement($value, $key)?->getId();
+ if (null !== $id && isset($references[$id])) {
+ continue;
+ }
+ if (null !== $id) {
+ $references[$id] = true;
+ }
+
+ $value[$key] = self::filterArray($item, $filter, $options, $invalid, $references);
+
+ if (null !== $id) {
+ unset($references[$id]);
+ }
+ }
+
+ return $value;
+ }
}
diff --git a/core/vendor/symfony/http-foundation/Request.php b/core/vendor/symfony/http-foundation/Request.php
index fba1cf33ea..a0366a8a0e 100644
--- a/core/vendor/symfony/http-foundation/Request.php
+++ b/core/vendor/symfony/http-foundation/Request.php
@@ -1714,7 +1714,7 @@ public function getPreferredLanguage(?array $locales = null): ?string
$combinations = array_merge(...array_map($this->getLanguageCombinations(...), $preferredLanguages));
foreach ($combinations as $combination) {
foreach ($locales as $locale) {
- if (str_starts_with($locale, $combination)) {
+ if ($locale === $combination || str_starts_with($locale, $combination.'_')) {
return $locale;
}
}
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
index bfdc55ba62..a14086406f 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php
@@ -27,6 +27,7 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
private string|false $prefetchData;
private ?\Throwable $prefetchError = null;
private ?string $newSessionId = null;
+ private ?string $readSessionId = null;
private string $igbinaryEmptyData;
public function __serialize(): array
@@ -80,6 +81,8 @@ public function validateId(#[\SensitiveParameter] string $sessionId): bool
public function read(#[\SensitiveParameter] string $sessionId): string|false
{
+ $this->readSessionId = $sessionId;
+
if (isset($this->prefetchId)) {
$prefetchId = $this->prefetchId;
$prefetchData = $this->prefetchData;
@@ -127,7 +130,8 @@ public function write(#[\SensitiveParameter] string $sessionId, string $data): b
public function destroy(#[\SensitiveParameter] string $sessionId): bool
{
- if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) {
+ // only the handler that read the session manages its cookie, e.g. not the write-only handler of MigratingSessionHandler
+ if ($this->readSessionId === $sessionId && !headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) {
if (!isset($this->sessionName)) {
throw new \LogicException(\sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class));
}
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
index 8482250014..1d4ae1ed4b 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php
@@ -78,6 +78,11 @@ public function read(#[\SensitiveParameter] string $sessionId): string|false
public function write(#[\SensitiveParameter] string $sessionId, string $data): bool
{
+ // empty data must reach the inner handler unchanged so that it destroys the session
+ if ('' === $data || (\function_exists('igbinary_serialize') && igbinary_serialize([]) === $data)) {
+ return $this->handler->write($sessionId, $data);
+ }
+
$failed = [];
$marshalledData = $this->marshaller->marshall(['data' => $data], $failed);
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
index 4f1254f1b6..597b0c99a5 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php
@@ -70,6 +70,11 @@ protected function doRead(#[\SensitiveParameter] string $sessionId): string|fals
public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
{
+ // PHP 8.6 calls updateTimestamp() instead of write() for a session that is still empty
+ if ('' === $data || (\function_exists('igbinary_serialize') && igbinary_serialize([]) === $data)) {
+ return $this->destroy($sessionId);
+ }
+
$this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl());
return true;
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
index d5586030f0..df1620dd17 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php
@@ -140,6 +140,11 @@ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $dat
public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
{
+ // PHP 8.6 calls updateTimestamp() instead of write() for a session that is still empty
+ if ('' === $data || (\function_exists('igbinary_serialize') && igbinary_serialize([]) === $data)) {
+ return $this->destroy($sessionId);
+ }
+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
$expiry = $this->getUTCDateTime($ttl);
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
index a77185e2eb..cde3b9c7c4 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php
@@ -33,11 +33,6 @@ protected function doRead(#[\SensitiveParameter] string $sessionId): string
return '';
}
- public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
- {
- return true;
- }
-
protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
{
return true;
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
index 281925eaa5..5e5a6946d6 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php
@@ -449,6 +449,11 @@ protected function doWrite(#[\SensitiveParameter] string $sessionId, string $dat
public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
{
+ // PHP 8.6 calls updateTimestamp() instead of write() for a session that is still empty
+ if ('' === $data || (\function_exists('igbinary_serialize') && igbinary_serialize([]) === $data)) {
+ return $this->destroy($sessionId);
+ }
+
$expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'));
try {
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/core/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
index 5bbd236dfe..665b51071b 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php
@@ -98,6 +98,11 @@ public function gc(int $maxlifetime): int|false
public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
{
+ // PHP 8.6 calls updateTimestamp() instead of write() for a session that is still empty
+ if ('' === $data || (\function_exists('igbinary_serialize') && igbinary_serialize([]) === $data)) {
+ return $this->destroy($sessionId);
+ }
+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
return $this->redis->expire($this->prefix.$sessionId, (int) $ttl);
diff --git a/core/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/core/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
index 5c08513477..ffb90a97d3 100644
--- a/core/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
+++ b/core/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php
@@ -37,6 +37,8 @@ class NativeSessionStorage implements SessionStorageInterface
protected AbstractProxy|\SessionHandlerInterface $saveHandler;
protected MetadataBag $metadataBag;
+ private static bool $shutdownRegistered = false;
+
/**
* Depending on how you want the storage driver to behave you probably
* want to override this constructor entirely.
@@ -87,7 +89,10 @@ public function __construct(array $options = [], AbstractProxy|\SessionHandlerIn
'use_strict_mode' => 1,
];
- session_register_shutdown();
+ if (!self::$shutdownRegistered) {
+ session_register_shutdown();
+ self::$shutdownRegistered = true;
+ }
$this->setMetadataBag($metaBag);
$this->setOptions($options);
diff --git a/core/vendor/symfony/http-kernel/DependencyInjection/LoggerPass.php b/core/vendor/symfony/http-kernel/DependencyInjection/LoggerPass.php
index 7566d7358c..52addd7dcb 100644
--- a/core/vendor/symfony/http-kernel/DependencyInjection/LoggerPass.php
+++ b/core/vendor/symfony/http-kernel/DependencyInjection/LoggerPass.php
@@ -42,6 +42,7 @@ public function process(ContainerBuilder $container): void
}
$container->register('logger', Logger::class)
- ->setArguments([null, null, null, new Reference(RequestStack::class), $debug]);
+ ->setArguments([null, null, null, new Reference(RequestStack::class), $debug])
+ ->addTag('kernel.reset', ['method' => 'clear', 'on_invalid' => 'ignore']);
}
}
diff --git a/core/vendor/symfony/http-kernel/HttpCache/HttpCache.php b/core/vendor/symfony/http-kernel/HttpCache/HttpCache.php
index 4b58a1f5c6..b445fc38aa 100644
--- a/core/vendor/symfony/http-kernel/HttpCache/HttpCache.php
+++ b/core/vendor/symfony/http-kernel/HttpCache/HttpCache.php
@@ -91,7 +91,12 @@ public function __construct(
array $options = [],
) {
// needed in case there is a fatal error because the backend is too slow to respond
- register_shutdown_function($this->store->cleanup(...));
+ $pid = getmypid();
+ register_shutdown_function(static function () use ($store, $pid) {
+ if ($pid === getmypid()) {
+ $store->cleanup();
+ }
+ });
$this->options = array_merge([
'debug' => false,
diff --git a/core/vendor/symfony/http-kernel/HttpCache/Store.php b/core/vendor/symfony/http-kernel/HttpCache/Store.php
index bcfe4b023e..b66700511c 100644
--- a/core/vendor/symfony/http-kernel/HttpCache/Store.php
+++ b/core/vendor/symfony/http-kernel/HttpCache/Store.php
@@ -28,6 +28,7 @@ class Store implements StoreInterface
private \SplObjectStorage $keyCache;
/** @var array */
private array $locks = [];
+ private int|false $pid;
/**
* Constructor.
@@ -43,6 +44,7 @@ public function __construct(
protected string $root,
private array $options = [],
) {
+ $this->pid = getmypid();
if (!is_dir($this->root) && !@mkdir($this->root, 0o777, true) && !is_dir($this->root)) {
throw new \RuntimeException(\sprintf('Unable to create the store directory (%s).', $this->root));
}
@@ -55,6 +57,8 @@ public function __construct(
*/
public function cleanup(): void
{
+ $this->resetLocksAfterFork();
+
// unlock everything
foreach ($this->locks as $lock) {
flock($lock, \LOCK_UN);
@@ -71,6 +75,8 @@ public function cleanup(): void
*/
public function lock(Request $request): bool|string
{
+ $this->resetLocksAfterFork();
+
$key = $this->getCacheKey($request);
if (!isset($this->locks[$key])) {
@@ -98,6 +104,8 @@ public function lock(Request $request): bool|string
*/
public function unlock(Request $request): bool
{
+ $this->resetLocksAfterFork();
+
$key = $this->getCacheKey($request);
if (isset($this->locks[$key])) {
@@ -113,6 +121,8 @@ public function unlock(Request $request): bool
public function isLocked(Request $request): bool
{
+ $this->resetLocksAfterFork();
+
$key = $this->getCacheKey($request);
if (isset($this->locks[$key])) {
@@ -333,6 +343,8 @@ public function purge(string $url): bool
*/
private function doPurge(string $url): bool
{
+ $this->resetLocksAfterFork();
+
$key = $this->getCacheKey(Request::create($url));
if (isset($this->locks[$key])) {
flock($this->locks[$key], \LOCK_UN);
@@ -364,6 +376,8 @@ private function load(string $key): ?string
*/
private function save(string $key, string $data, bool $overwrite = true): bool
{
+ $this->resetLocksAfterFork();
+
$path = $this->getPath($key);
if (!$overwrite && file_exists($path)) {
@@ -495,4 +509,19 @@ private function restoreResponse(array $headers, ?string $path = null): ?Respons
return new Response($content, $status, $headers);
}
+
+ private function resetLocksAfterFork(): void
+ {
+ if ($this->pid === $pid = getmypid()) {
+ return;
+ }
+
+ // Closing an inherited handle preserves the parent's lock.
+ foreach ($this->locks as $lock) {
+ fclose($lock);
+ }
+
+ $this->locks = [];
+ $this->pid = $pid;
+ }
}
diff --git a/core/vendor/symfony/http-kernel/HttpKernel.php b/core/vendor/symfony/http-kernel/HttpKernel.php
index 8032030c42..994e978e62 100644
--- a/core/vendor/symfony/http-kernel/HttpKernel.php
+++ b/core/vendor/symfony/http-kernel/HttpKernel.php
@@ -54,6 +54,7 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
protected RequestStack $requestStack;
private ArgumentResolverInterface $argumentResolver;
private bool $terminating = false;
+ private ?\WeakReference $terminateException = null;
public function __construct(
protected EventDispatcherInterface $dispatcher,
@@ -112,6 +113,10 @@ public function terminate(Request $request, Response $response): void
try {
$this->terminating = true;
$this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE);
+ } catch (\Throwable $e) {
+ $this->terminateException = \WeakReference::create($e);
+
+ throw $e;
} finally {
$this->terminating = false;
}
@@ -130,9 +135,14 @@ public function terminateWithException(\Throwable $exception, ?Request $request
$this->requestStack->push($request);
}
+ $terminating = $this->terminating;
+ $this->terminating = $terminating || $exception === $this->terminateException?->get();
+
try {
$response = $this->handleThrowable($exception, $request, self::MAIN_REQUEST);
} finally {
+ $this->terminating = $terminating;
+
if ($pop) {
$this->requestStack->pop();
}
diff --git a/core/vendor/symfony/http-kernel/Kernel.php b/core/vendor/symfony/http-kernel/Kernel.php
index e2a3da938f..1d3b9552c4 100644
--- a/core/vendor/symfony/http-kernel/Kernel.php
+++ b/core/vendor/symfony/http-kernel/Kernel.php
@@ -74,11 +74,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '7.4.21';
- public const VERSION_ID = 70421;
+ public const VERSION = '7.4.22';
+ public const VERSION_ID = 70422;
public const MAJOR_VERSION = 7;
public const MINOR_VERSION = 4;
- public const RELEASE_VERSION = 21;
+ public const RELEASE_VERSION = 22;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '11/2028';
@@ -143,6 +143,18 @@ public function terminate(Request $request, Response $response): void
return;
}
+ if (!$this->handlingHttpCache && $this->container->has('http_cache') && ($httpCache = $this->container->get('http_cache')) instanceof TerminableInterface) {
+ $this->handlingHttpCache = true;
+
+ try {
+ $httpCache->terminate($request, $response);
+ } finally {
+ $this->handlingHttpCache = false;
+ }
+
+ return;
+ }
+
if ($this->getHttpKernel() instanceof TerminableInterface) {
$this->getHttpKernel()->terminate($request, $response);
}
@@ -164,8 +176,6 @@ public function shutdown(): void
$this->container = null;
$this->requestStackSize = 0;
$this->resetServices = false;
-
- gc_collect_cycles();
}
public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
@@ -460,6 +470,8 @@ protected function initializeContainer(): void
error_reporting($errorLevel);
}
+ $this->container = null;
+
try {
if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) {
$collectedLogs = [];
diff --git a/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php b/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php
index 8722d96983..5407e129f8 100644
--- a/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php
+++ b/core/vendor/symfony/http-kernel/Profiler/FileProfilerStorage.php
@@ -50,11 +50,19 @@ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?i
return [];
}
+ // the lines before this offset belong to expired profiles, whose files were already removed
+ $offset = is_file($file.'.offset') ? (int) file_get_contents($file.'.offset') : 0;
+
$file = fopen($file, 'r');
fseek($file, 0, \SEEK_END);
+ if (ftell($file) < $offset) {
+ // the index was recreated after the offset was saved
+ $offset = 0;
+ }
+
$result = [];
- while (\count($result) < $limit && $line = $this->readLineFromFile($file)) {
+ while (\count($result) < $limit && ftell($file) > $offset && $line = $this->readLineFromFile($file)) {
$values = str_getcsv($line, ',', '"', '\\');
if (7 > \count($values)) {
@@ -326,6 +334,11 @@ private function removeExpiredProfiles(): void
$handle = fopen($file, 'r');
if ($offset = is_file($file.'.offset') ? (int) file_get_contents($file.'.offset') : 0) {
+ if ($offset > fstat($handle)['size']) {
+ // the index was recreated after the offset was saved
+ $offset = 0;
+ }
+
fseek($handle, $offset);
}
@@ -340,7 +353,7 @@ private function removeExpiredProfiles(): void
[$csvToken, , , , $csvTime] = $values;
- if ($csvTime >= $minimalProfileTimestamp) {
+ if ((int) $csvTime >= $minimalProfileTimestamp) {
break;
}
diff --git a/core/vendor/symfony/mime/Email.php b/core/vendor/symfony/mime/Email.php
index d05115eb51..76e92cd4cb 100644
--- a/core/vendor/symfony/mime/Email.php
+++ b/core/vendor/symfony/mime/Email.php
@@ -573,7 +573,7 @@ public function __serialize(): array
*/
public function __unserialize(array $data): void
{
- if (($data[1] ?? null) instanceof \Stringable || ($data[3] ?? null) instanceof \Stringable) {
+ if (!\is_string($data[1] ?? '') || !\is_string($data[3] ?? '')) {
throw new \BadMethodCallException('Cannot unserialize '.self::class);
}
diff --git a/core/vendor/symfony/mime/Header/ParameterizedHeader.php b/core/vendor/symfony/mime/Header/ParameterizedHeader.php
index 6592a6e6aa..93f218d5b3 100644
--- a/core/vendor/symfony/mime/Header/ParameterizedHeader.php
+++ b/core/vendor/symfony/mime/Header/ParameterizedHeader.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Encoder\Rfc2231Encoder;
+use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* @author Chris Corbyn
@@ -56,6 +57,12 @@ public function getParameter(string $parameter): string
*/
public function setParameters(array $parameters): void
{
+ foreach ($parameters as $k => $v) {
+ if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $k)) {
+ throw new RfcComplianceException(\sprintf('The parameter name "%s" contains non-token characters.', $k));
+ }
+ }
+
$this->parameters = $parameters;
}
@@ -176,7 +183,7 @@ private function getEndOfParameterValue(string $value, bool $encoded = false, bo
{
$forceHttpQuoting = 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName());
if ($forceHttpQuoting || !preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
- $value = '"'.$value.'"';
+ $value = '"'.addcslashes($value, '"\\').'"';
}
$prepend = '=';
if ($encoded) {
diff --git a/core/vendor/symfony/mime/Part/AbstractMultipartPart.php b/core/vendor/symfony/mime/Part/AbstractMultipartPart.php
index 1da0ddf331..3a30beeeaa 100644
--- a/core/vendor/symfony/mime/Part/AbstractMultipartPart.php
+++ b/core/vendor/symfony/mime/Part/AbstractMultipartPart.php
@@ -88,6 +88,47 @@ public function asDebugString(): string
return $str;
}
+ public function __serialize(): array
+ {
+ $data = parent::__serialize();
+
+ if (!method_exists($this, '__sleep')) {
+ $data['boundary'] = $this->boundary;
+ $data['parts'] = $this->parts;
+ }
+
+ return $data;
+ }
+
+ public function __unserialize(array $data): void
+ {
+ foreach (['', "\0".self::class."\0"] as $prefix) {
+ if (!\array_key_exists($prefix.'parts', $data)) {
+ continue;
+ }
+
+ $boundary = $data[$prefix.'boundary'] ?? null;
+ $parts = $data[$prefix.'parts'];
+ unset($data[$prefix.'boundary'], $data[$prefix.'parts']);
+
+ if ((null !== $boundary && !\is_string($boundary)) || !\is_array($parts)) {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ foreach ($parts as $part) {
+ if (!$part instanceof AbstractPart) {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+ }
+
+ $this->boundary = $boundary;
+ $this->parts = $parts;
+ break;
+ }
+
+ parent::__unserialize($data);
+ }
+
private function getBoundary(): string
{
return $this->boundary ??= strtr(base64_encode(random_bytes(6)), '+/', '-_');
diff --git a/core/vendor/symfony/mime/Part/AbstractPart.php b/core/vendor/symfony/mime/Part/AbstractPart.php
index f79266cb9f..8142252db2 100644
--- a/core/vendor/symfony/mime/Part/AbstractPart.php
+++ b/core/vendor/symfony/mime/Part/AbstractPart.php
@@ -66,7 +66,7 @@ abstract public function getMediaSubtype(): string;
public function __serialize(): array
{
if (!method_exists($this, '__sleep')) {
- return ['headers' => $this->headers];
+ return ['headers' => $this->headers] + $this->getSubclassState();
}
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
@@ -87,7 +87,7 @@ public function __serialize(): array
public function __unserialize(array $data): void
{
- if ($wakeup = method_exists($this, '__wakeup') && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
+ if ($wakeup = method_exists($this, '__wakeup') && \in_array((new \ReflectionMethod($this, '__unserialize'))->class, [self::class, AbstractMultipartPart::class], true)) {
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
}
@@ -101,8 +101,24 @@ public function __unserialize(array $data): void
return;
}
+ // the payload comes from __serialize() for a subclass that declares properties, or from the native serialization of Symfony < 7.4
+ $headersKey = \array_key_exists('headers', $data) ? 'headers' : "\0".self::class."\0headers";
+ if (\array_key_exists($headersKey, $data) && self::isCoreClass((new \ReflectionMethod($this, '__unserialize'))->class) && self::isCoreClass((new \ReflectionMethod($this, '__serialize'))->class)) {
+ $this->headers = $data[$headersKey];
+ unset($data[$headersKey]);
+ $this->setSubclassState($data);
+
+ if ($wakeup) {
+ $this->__wakeup();
+ }
+
+ return;
+ }
+
trigger_deprecation('symfony/mime', '7.4', 'Passing more than just key "headers" to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
+ self::rejectStringCasts(static::class, $data);
+
\Closure::bind(function ($data) use ($wakeup) {
foreach ($data as $key => $value) {
$this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
@@ -113,4 +129,84 @@ public function __unserialize(array $data): void
}
}, $this, static::class)($data);
}
+
+ /**
+ * Returns the properties declared by subclasses that are not part of Symfony, keyed like the native serialization does.
+ */
+ private function getSubclassState(): array
+ {
+ if (self::isCoreClass(static::class)) {
+ return [];
+ }
+
+ $state = [];
+ foreach ((array) $this as $key => $value) {
+ if (!str_starts_with($key, "\0".__NAMESPACE__.'\\')) {
+ $state[$key] = $value;
+ }
+ }
+
+ return $state;
+ }
+
+ private function setSubclassState(array $data): void
+ {
+ foreach ($data as $key => $value) {
+ [$class, $name] = "\0" === ($key[0] ?? '') ? explode("\0", substr($key, 1), 2) + [1 => ''] : ['*', $key];
+ $class = '*' === $class ? static::class : $class;
+
+ if (!$this instanceof $class) {
+ throw new \BadMethodCallException('Cannot unserialize '.static::class);
+ }
+
+ $property = property_exists($class, $name) ? new \ReflectionProperty($class, $name) : null;
+
+ if (self::isCoreClass($property?->class ?? $class) || ($property && $value instanceof \Stringable && self::castsToString($property, $value))) {
+ throw new \BadMethodCallException('Cannot unserialize '.static::class);
+ }
+
+ \Closure::bind(function () use ($name, $value) {
+ $this->$name = $value;
+ }, $this, $class)();
+ }
+ }
+
+ /**
+ * Rejects the \Stringable values that would be cast to string when assigned by name in the scope of the class.
+ */
+ private static function rejectStringCasts(string $class, array $data): void
+ {
+ foreach ($data as $key => $value) {
+ $name = "\0" === ($key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key;
+
+ if ($value instanceof \Stringable && property_exists($class, $name) && self::castsToString(new \ReflectionProperty($class, $name), $value)) {
+ throw new \BadMethodCallException('Cannot unserialize '.$class);
+ }
+ }
+ }
+
+ private static function isCoreClass(string $class): bool
+ {
+ return str_starts_with($class, __NAMESPACE__.'\\');
+ }
+
+ private static function castsToString(\ReflectionProperty $property, object $value): bool
+ {
+ $type = $property->getType();
+ $string = false;
+
+ foreach ($type instanceof \ReflectionUnionType ? $type->getTypes() : [$type] as $t) {
+ if (!$t instanceof \ReflectionNamedType) {
+ continue;
+ }
+
+ if ('string' === $t->getName()) {
+ $string = true;
+ } elseif ('object' === $t->getName() || (!$t->isBuiltin() && $value instanceof ($t->getName()))) {
+ return false;
+ }
+ }
+
+ return $string;
+ }
}
diff --git a/core/vendor/symfony/mime/Part/DataPart.php b/core/vendor/symfony/mime/Part/DataPart.php
index 90b9cffc31..3f952e3800 100644
--- a/core/vendor/symfony/mime/Part/DataPart.php
+++ b/core/vendor/symfony/mime/Part/DataPart.php
@@ -163,6 +163,13 @@ public function __serialize(): array
public function __unserialize(array $data): void
{
+ if (!\is_string($data['filename'] ?? $data["\0".self::class."\0filename"] ?? '')
+ || !\is_string($data['mediaType'] ?? $data["\0".self::class."\0mediaType"] ?? null)
+ || !\is_string($data['cid'] ?? $data["\0".self::class."\0cid"] ?? '')
+ ) {
+ throw new \BadMethodCallException('Cannot unserialize '.self::class);
+ }
+
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
}
@@ -180,10 +187,12 @@ public function __unserialize(array $data): void
return;
}
- if (["\0*\0_headers", "\0*\0_parent", "\0".self::class."\0filename", "\0".self::class."\0mediaType"] === array_keys($data)) {
+ $p = "\0".self::class."\0";
+ if (["\0*\0_headers", "\0*\0_parent", $p.'filename', $p.'mediaType'] === array_keys($data) || ["\0*\0_headers", "\0*\0_parent", $p.'filename', $p.'mediaType', $p.'cid'] === array_keys($data)) {
parent::__unserialize(['_headers' => $data["\0*\0_headers"], ...$data["\0*\0_parent"]]);
- $this->filename = $data["\0".self::class."\0filename"];
- $this->mediaType = $data["\0".self::class."\0mediaType"];
+ $this->filename = $data[$p.'filename'];
+ $this->mediaType = $data[$p.'mediaType'];
+ $this->cid = $data[$p.'cid'] ?? null;
if ($wakeup) {
$this->__wakeup();
@@ -194,15 +203,37 @@ public function __unserialize(array $data): void
trigger_deprecation('symfony/mime', '7.4', 'Passing extra keys to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
- \Closure::bind(function ($data) use ($wakeup) {
- foreach ($data as $key => $value) {
- $this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
- }
+ $headers = $data['_headers'] ?? $data["\0*\0_headers"] ?? null;
+ $parent = $data['_parent'] ?? $data["\0*\0_parent"] ?? null;
+ unset($data['_headers'], $data["\0*\0_headers"], $data['_parent'], $data["\0*\0_parent"]);
- if ($wakeup) {
- $this->__wakeup();
- }
- }, $this, static::class)($data);
+ $state = [];
+ foreach (['filename', 'mediaType', 'cid'] as $name) {
+ $key = \array_key_exists($name, $data) ? $name : "\0".self::class."\0".$name;
+ $state[$name] = $data[$key] ?? null;
+ unset($data[$key]);
+ }
+
+ if (!\is_array($parent)) {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ (new \ReflectionMethod(AbstractPart::class, 'setSubclassState'))->invoke($this, $data);
+
+ if ($wakeup) {
+ $this->_headers = $headers;
+ $this->_parent = $parent;
+ } else {
+ parent::__unserialize(['_headers' => $headers, ...$parent]);
+ }
+
+ foreach ($state as $name => $value) {
+ $this->$name = $value;
+ }
+
+ if ($wakeup) {
+ $this->__wakeup();
+ }
}
/**
diff --git a/core/vendor/symfony/mime/Part/Multipart/FormDataPart.php b/core/vendor/symfony/mime/Part/Multipart/FormDataPart.php
index ca4080198d..c74757d681 100644
--- a/core/vendor/symfony/mime/Part/Multipart/FormDataPart.php
+++ b/core/vendor/symfony/mime/Part/Multipart/FormDataPart.php
@@ -44,6 +44,24 @@ public function getParts(): array
return $this->prepareFields($this->fields);
}
+ public function __serialize(): array
+ {
+ return parent::__serialize() + ['fields' => $this->fields];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ foreach (['fields', "\0".self::class."\0fields"] as $key) {
+ if (\array_key_exists($key, $data)) {
+ $this->fields = $data[$key];
+ unset($data[$key]);
+ break;
+ }
+ }
+
+ parent::__unserialize($data);
+ }
+
private function prepareFields(array $fields): array
{
$values = [];
diff --git a/core/vendor/symfony/mime/Part/Multipart/RelatedPart.php b/core/vendor/symfony/mime/Part/Multipart/RelatedPart.php
index 18ca9f17ab..c02ea4d2d3 100644
--- a/core/vendor/symfony/mime/Part/Multipart/RelatedPart.php
+++ b/core/vendor/symfony/mime/Part/Multipart/RelatedPart.php
@@ -39,6 +39,24 @@ public function getMediaSubtype(): string
return 'related';
}
+ public function __serialize(): array
+ {
+ return parent::__serialize() + ['mainPart' => $this->mainPart];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ foreach (['mainPart', "\0".self::class."\0mainPart"] as $key) {
+ if (\array_key_exists($key, $data)) {
+ $this->mainPart = $data[$key];
+ unset($data[$key]);
+ break;
+ }
+ }
+
+ parent::__unserialize($data);
+ }
+
private function generateContentId(): string
{
return bin2hex(random_bytes(16)).'@symfony';
diff --git a/core/vendor/symfony/mime/Part/SMimePart.php b/core/vendor/symfony/mime/Part/SMimePart.php
index 102ac3f7dc..8dacb8f737 100644
--- a/core/vendor/symfony/mime/Part/SMimePart.php
+++ b/core/vendor/symfony/mime/Part/SMimePart.php
@@ -119,10 +119,11 @@ public function __serialize(): array
public function __unserialize(array $data): void
{
- foreach (['body', 'type', 'subtype'] as $prop) {
- if (($data[$prop] ?? $data["\0".self::class."\0".$prop] ?? $data["\0*\0".$prop] ?? null) instanceof \Stringable) {
- throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
- }
+ if (($data['body'] ?? $data["\0".self::class."\0body"] ?? $data["\0*\0body"] ?? null) instanceof \Stringable
+ || !\is_string($data['type'] ?? $data["\0".self::class."\0type"] ?? $data["\0*\0type"] ?? null)
+ || !\is_string($data['subtype'] ?? $data["\0".self::class."\0subtype"] ?? $data["\0*\0subtype"] ?? null)
+ ) {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
@@ -163,15 +164,31 @@ public function __unserialize(array $data): void
trigger_deprecation('symfony/mime', '7.4', 'Passing extra keys to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
- \Closure::bind(function ($data) use ($wakeup) {
- foreach ($data as $key => $value) {
- $this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
- }
+ $headers = $data['_headers'] ?? $data["\0*\0_headers"] ?? null;
+ unset($data['_headers'], $data["\0*\0_headers"]);
- if ($wakeup) {
- $this->__wakeup();
- }
- }, $this, static::class)($data);
+ $state = [];
+ foreach (['body', 'type', 'subtype', 'parameters'] as $name) {
+ $key = \array_key_exists($name, $data) ? $name : "\0".self::class."\0".$name;
+ $state[$name] = $data[$key] ?? null;
+ unset($data[$key]);
+ }
+
+ (new \ReflectionMethod(AbstractPart::class, 'setSubclassState'))->invoke($this, $data);
+
+ if ($wakeup) {
+ $this->_headers = $headers;
+ } else {
+ parent::__unserialize(['headers' => $headers]);
+ }
+
+ foreach ($state as $name => $value) {
+ $this->$name = $value;
+ }
+
+ if ($wakeup) {
+ $this->__wakeup();
+ }
}
/**
diff --git a/core/vendor/symfony/mime/Part/TextPart.php b/core/vendor/symfony/mime/Part/TextPart.php
index c83e7acde8..246ba87ab0 100644
--- a/core/vendor/symfony/mime/Part/TextPart.php
+++ b/core/vendor/symfony/mime/Part/TextPart.php
@@ -276,8 +276,10 @@ public function __serialize(): array
public function __unserialize(array $data): void
{
- foreach (['charset', 'subtype', 'disposition', 'name', 'encoding'] as $prop) {
- if (($data[$prop] ?? $data["\0".self::class."\0".$prop] ?? $data["\0*\0".$prop] ?? null) instanceof \Stringable) {
+ foreach (['charset' => true, 'subtype' => false, 'disposition' => true, 'name' => true, 'encoding' => false] as $prop => $nullable) {
+ $value = $data[$prop] ?? $data["\0".self::class."\0".$prop] ?? $data["\0*\0".$prop] ?? null;
+
+ if (!\is_string($value) && (!$nullable || null !== $value)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
}
@@ -329,15 +331,25 @@ public function __unserialize(array $data): void
trigger_deprecation('symfony/mime', '7.4', 'Passing extra keys to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
- \Closure::bind(function ($data) use ($wakeup) {
- foreach ($data as $key => $value) {
- $this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
- }
+ $state = [];
+ foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
+ $key = \array_key_exists($name, $data) ? $name : "\0".self::class."\0".$name;
+ $state[$name] = $data[$key] ?? null;
+ unset($data[$key]);
+ }
- if ($wakeup) {
- $this->__wakeup();
- }
- }, $this, static::class)($data);
+ (new \ReflectionMethod(AbstractPart::class, 'setSubclassState'))->invoke($this, $data);
+
+ foreach ($state as $name => $value) {
+ $this->$name = $value;
+ }
+
+ if ($wakeup) {
+ $this->_headers = $headers;
+ $this->__wakeup();
+ } elseif (!\is_string($this->body) && !$this->body instanceof File) {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
}
/**
diff --git a/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php b/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php
index 94d080ac9b..a7e18d7def 100644
--- a/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php
+++ b/core/vendor/symfony/polyfill-intl-grapheme/Grapheme.php
@@ -49,6 +49,9 @@ final class Grapheme
['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'],
];
+ // The characters that extend a grapheme cluster and that ICU 74 ignores when it compares strings
+ private const IGNORABLE_RX = '/^[\x{034F}\x{0488}-\x{0489}\x{0591}-\x{05AF}\x{05BD}\x{05C4}-\x{05C5}\x{0610}-\x{061A}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{0740}\x{0743}-\x{0744}\x{0747}-\x{074A}\x{0898}-\x{089D}\x{08CA}-\x{08E1}\x{08EA}-\x{08EF}\x{08F3}\x{0951}-\x{0952}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F3E}-\x{0F3F}\x{0F86}-\x{0F87}\x{0FC6}\x{17B4}-\x{17B5}\x{17D3}\x{180B}-\x{180D}\x{180F}\x{1A7F}\x{1B6B}-\x{1B73}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE8}\x{1CF4}\x{1CF7}-\x{1CF9}\x{200C}-\x{200D}\x{2D7F}\x{A670}-\x{A672}\x{A8E0}-\x{A8F1}\x{FE00}-\x{FE0F}\x{FE21}\x{FE23}-\x{FE26}\x{FE28}\x{FE2A}-\x{FE2D}\x{FE2F}\x{102E0}\x{10EFD}-\x{10EFF}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{13440}\x{13447}-\x{13455}\x{16FE4}\x{1CF00}-\x{1CF2D}\x{1CF30}-\x{1CF46}\x{1D165}-\x{1D169}\x{1D16D}-\x{1D172}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D242}-\x{1D244}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E8D0}-\x{1E8D6}\x{E0020}-\x{E007F}\x{E0100}-\x{E01EF}]*+\z/u';
+
// indexed by the $mode argument of grapheme_position()
private const POSITION_FUNCTIONS = ['grapheme_strpos', 'grapheme_stripos', 'grapheme_strrpos', 'grapheme_strripos'];
@@ -377,13 +380,72 @@ private static function grapheme_position($s, $needle, $offset, $mode)
$needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle);
}
}
- if ($reverse) {
- $needlePos = strrpos($s, $needle);
- } else {
- $needlePos = strpos($s, $needle);
+ $needleLen = \strlen($needle);
+ $needlePos = $reverse ? strrpos($s, $needle) : strpos($s, $needle);
+
+ while (false !== $needlePos) {
+ if (false !== $i = self::graphemeIndex($s, $needlePos, $needlePos + $needleLen)) {
+ return $i + $offset;
+ }
+
+ if (!$reverse) {
+ $needlePos = strpos($s, $needle, $needlePos + 1);
+ } elseif ($needlePos) {
+ $needlePos = strrpos(substr($s, 0, $needlePos + $needleLen - 1), $needle);
+ } else {
+ $needlePos = false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the position in grapheme units of the bytes from $start to $end,
+ * or false when they split a grapheme cluster on characters that are not ignorable.
+ */
+ private static function graphemeIndex($s, $start, $end)
+ {
+ if (self::isAsciiBoundary($s, $start) && self::isAsciiBoundary($s, $end)) {
+ return self::grapheme_strlen(substr($s, 0, $start));
}
- return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false;
+ $i = $pos = 0;
+
+ if ($start) {
+ // count the clusters that start before $start, then find where the last one starts
+ $prefix = substr($s, 0, $start);
+ preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $prefix, -1, $i);
+
+ if (!$i) {
+ return false;
+ }
+
+ $pos = 1 < $i ? $start - \strlen(preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $prefix, $i - 1)) : 0;
+ }
+
+ do {
+ if (!preg_match('/\G'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $cluster, 0, $pos)) {
+ return false;
+ }
+
+ $next = $pos + \strlen($cluster[0]);
+
+ if ($pos < $start && $start < $next && !preg_match(self::IGNORABLE_RX, substr($s, $start, min($next, $end) - $start))) {
+ return false;
+ }
+
+ if ($pos < $end && $end < $next && !preg_match(self::IGNORABLE_RX, substr($s, $end, $next - $end))) {
+ return false;
+ }
+ } while (($pos = $next) < $end);
+
+ return $i;
+ }
+
+ private static function isAsciiBoundary($s, $i)
+ {
+ return 0 === $i || !isset($s[$i]) || ($s[$i - 1] < "\x80" && $s[$i] < "\x80" && ("\r" !== $s[$i - 1] || "\n" !== $s[$i]));
}
public static function grapheme_strrev(string $string)
diff --git a/core/vendor/symfony/polyfill-mbstring/Mbstring.php b/core/vendor/symfony/polyfill-mbstring/Mbstring.php
index bc9e6acfa4..5a59d26904 100644
--- a/core/vendor/symfony/polyfill-mbstring/Mbstring.php
+++ b/core/vendor/symfony/polyfill-mbstring/Mbstring.php
@@ -827,7 +827,10 @@ public static function mb_http_output($encoding = null)
public static function mb_strwidth($s, $encoding = null)
{
$encoding = self::getEncoding($encoding);
- $wideChars = '/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u';
+ static $wideChars = null;
+ if (null === $wideChars) {
+ $wideChars = self::getData('eastAsianWidthRegexp');
+ }
if ('UTF-8' !== $encoding) {
$s = self::iconv($encoding, 'UTF-8', $s);
@@ -1374,7 +1377,12 @@ private static function html_encoding_callback(array $m)
private static function title_case(array $s)
{
- return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
+ static $titleCase = null;
+ if (null === $titleCase) {
+ $titleCase = self::getData('titleCase');
+ }
+
+ return ($titleCase[$s[1]] ?? self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8')).self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
}
private static function getData($file)
@@ -1414,6 +1422,10 @@ private static function getEncoding($encoding)
return 'UTF-16BE';
}
+ if ('UCS-2' === $encoding) {
+ return 'UCS-2BE';
+ }
+
return $encoding;
}
diff --git a/core/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php b/core/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php
new file mode 100644
index 0000000000..34d875e526
--- /dev/null
+++ b/core/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php
@@ -0,0 +1,5 @@
+ 'Dž',
+ 'Dž' => 'Dž',
+ 'dž' => 'Dž',
+ 'LJ' => 'Lj',
+ 'Lj' => 'Lj',
+ 'lj' => 'Lj',
+ 'NJ' => 'Nj',
+ 'Nj' => 'Nj',
+ 'nj' => 'Nj',
+ 'DZ' => 'Dz',
+ 'Dz' => 'Dz',
+ 'dz' => 'Dz',
+ 'ა' => 'ა',
+ 'ბ' => 'ბ',
+ 'გ' => 'გ',
+ 'დ' => 'დ',
+ 'ე' => 'ე',
+ 'ვ' => 'ვ',
+ 'ზ' => 'ზ',
+ 'თ' => 'თ',
+ 'ი' => 'ი',
+ 'კ' => 'კ',
+ 'ლ' => 'ლ',
+ 'მ' => 'მ',
+ 'ნ' => 'ნ',
+ 'ო' => 'ო',
+ 'პ' => 'პ',
+ 'ჟ' => 'ჟ',
+ 'რ' => 'რ',
+ 'ს' => 'ს',
+ 'ტ' => 'ტ',
+ 'უ' => 'უ',
+ 'ფ' => 'ფ',
+ 'ქ' => 'ქ',
+ 'ღ' => 'ღ',
+ 'ყ' => 'ყ',
+ 'შ' => 'შ',
+ 'ჩ' => 'ჩ',
+ 'ც' => 'ც',
+ 'ძ' => 'ძ',
+ 'წ' => 'წ',
+ 'ჭ' => 'ჭ',
+ 'ხ' => 'ხ',
+ 'ჯ' => 'ჯ',
+ 'ჰ' => 'ჰ',
+ 'ჱ' => 'ჱ',
+ 'ჲ' => 'ჲ',
+ 'ჳ' => 'ჳ',
+ 'ჴ' => 'ჴ',
+ 'ჵ' => 'ჵ',
+ 'ჶ' => 'ჶ',
+ 'ჷ' => 'ჷ',
+ 'ჸ' => 'ჸ',
+ 'ჹ' => 'ჹ',
+ 'ჺ' => 'ჺ',
+ 'ჽ' => 'ჽ',
+ 'ჾ' => 'ჾ',
+ 'ჿ' => 'ჿ',
+ 'ß' => 'Ss',
+ 'ff' => 'Ff',
+ 'fi' => 'Fi',
+ 'fl' => 'Fl',
+ 'ffi' => 'Ffi',
+ 'ffl' => 'Ffl',
+ 'ſt' => 'St',
+ 'st' => 'St',
+ 'և' => 'Եւ',
+ 'ﬓ' => 'Մն',
+ 'ﬔ' => 'Մե',
+ 'ﬕ' => 'Մի',
+ 'ﬖ' => 'Վն',
+ 'ﬗ' => 'Մխ',
+ 'ᾀ' => 'ᾈ',
+ 'ᾁ' => 'ᾉ',
+ 'ᾂ' => 'ᾊ',
+ 'ᾃ' => 'ᾋ',
+ 'ᾄ' => 'ᾌ',
+ 'ᾅ' => 'ᾍ',
+ 'ᾆ' => 'ᾎ',
+ 'ᾇ' => 'ᾏ',
+ 'ᾈ' => 'ᾈ',
+ 'ᾉ' => 'ᾉ',
+ 'ᾊ' => 'ᾊ',
+ 'ᾋ' => 'ᾋ',
+ 'ᾌ' => 'ᾌ',
+ 'ᾍ' => 'ᾍ',
+ 'ᾎ' => 'ᾎ',
+ 'ᾏ' => 'ᾏ',
+ 'ᾐ' => 'ᾘ',
+ 'ᾑ' => 'ᾙ',
+ 'ᾒ' => 'ᾚ',
+ 'ᾓ' => 'ᾛ',
+ 'ᾔ' => 'ᾜ',
+ 'ᾕ' => 'ᾝ',
+ 'ᾖ' => 'ᾞ',
+ 'ᾗ' => 'ᾟ',
+ 'ᾘ' => 'ᾘ',
+ 'ᾙ' => 'ᾙ',
+ 'ᾚ' => 'ᾚ',
+ 'ᾛ' => 'ᾛ',
+ 'ᾜ' => 'ᾜ',
+ 'ᾝ' => 'ᾝ',
+ 'ᾞ' => 'ᾞ',
+ 'ᾟ' => 'ᾟ',
+ 'ᾠ' => 'ᾨ',
+ 'ᾡ' => 'ᾩ',
+ 'ᾢ' => 'ᾪ',
+ 'ᾣ' => 'ᾫ',
+ 'ᾤ' => 'ᾬ',
+ 'ᾥ' => 'ᾭ',
+ 'ᾦ' => 'ᾮ',
+ 'ᾧ' => 'ᾯ',
+ 'ᾨ' => 'ᾨ',
+ 'ᾩ' => 'ᾩ',
+ 'ᾪ' => 'ᾪ',
+ 'ᾫ' => 'ᾫ',
+ 'ᾬ' => 'ᾬ',
+ 'ᾭ' => 'ᾭ',
+ 'ᾮ' => 'ᾮ',
+ 'ᾯ' => 'ᾯ',
+ 'ᾳ' => 'ᾼ',
+ 'ᾼ' => 'ᾼ',
+ 'ῃ' => 'ῌ',
+ 'ῌ' => 'ῌ',
+ 'ῳ' => 'ῼ',
+ 'ῼ' => 'ῼ',
+ 'ᾲ' => 'Ὰͅ',
+ 'ᾴ' => 'Άͅ',
+ 'ῂ' => 'Ὴͅ',
+ 'ῄ' => 'Ήͅ',
+ 'ῲ' => 'Ὼͅ',
+ 'ῴ' => 'Ώͅ',
+ 'ᾷ' => 'ᾼ͂',
+ 'ῇ' => 'ῌ͂',
+ 'ῷ' => 'ῼ͂',
+);
diff --git a/core/vendor/symfony/process/PhpProcess.php b/core/vendor/symfony/process/PhpProcess.php
index 930f591f0d..97081c17aa 100644
--- a/core/vendor/symfony/process/PhpProcess.php
+++ b/core/vendor/symfony/process/PhpProcess.php
@@ -42,7 +42,12 @@ public function __construct(string $script, ?string $cwd = null, ?array $env = n
if ('phpdbg' === \PHP_SAPI) {
$file = tempnam(sys_get_temp_dir(), 'dbg');
file_put_contents($file, $script);
- register_shutdown_function('unlink', $file);
+ $pid = getmypid();
+ register_shutdown_function(static function () use ($file, $pid) {
+ if ($pid === getmypid()) {
+ unlink($file);
+ }
+ });
$php[] = $file;
$script = null;
}
diff --git a/core/vendor/symfony/process/PhpSubprocess.php b/core/vendor/symfony/process/PhpSubprocess.php
index 8282f93cd4..11e3c3ffab 100644
--- a/core/vendor/symfony/process/PhpSubprocess.php
+++ b/core/vendor/symfony/process/PhpSubprocess.php
@@ -66,7 +66,12 @@ public function __construct(array $command, ?string $cwd = null, ?array $env = n
$tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir());
$php = array_merge($php, ['-n', '-c', $tmpIni]);
- register_shutdown_function('unlink', $tmpIni);
+ $pid = getmypid();
+ register_shutdown_function(static function () use ($tmpIni, $pid) {
+ if ($pid === getmypid()) {
+ unlink($tmpIni);
+ }
+ });
$command = array_merge($php, $command);
diff --git a/core/vendor/symfony/routing/Loader/schema/routing.schema.json b/core/vendor/symfony/routing/Loader/schema/routing.schema.json
index d3695af4c5..bf4909b2cc 100644
--- a/core/vendor/symfony/routing/Loader/schema/routing.schema.json
+++ b/core/vendor/symfony/routing/Loader/schema/routing.schema.json
@@ -62,17 +62,7 @@
"locale": { "type": ["string", "null"] },
"format": { "type": ["string", "null"] },
"utf8": { "type": ["boolean", "null"] },
- "stateless": { "type": ["boolean", "null"] },
- "deprecated": {
- "type": "object",
- "properties": {
- "package": { "type": "string" },
- "version": { "type": "string" },
- "message": { "type": "string" }
- },
- "required": ["package", "version"],
- "additionalProperties": false
- }
+ "stateless": { "type": ["boolean", "null"] }
},
"required": ["path"],
"additionalProperties": false
diff --git a/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php
index 4060e73bea..d79e1ba2df 100644
--- a/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php
+++ b/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php
@@ -82,7 +82,7 @@ public function getCompiledRoutes(bool $forDump = false): array
[$staticRoutes, $dynamicRoutes] = $this->groupStaticRoutes($routes);
- $conditions = [null];
+ $conditions = ['' => null];
$compiledRoutes[] = $this->compileStaticRoutes($staticRoutes, $conditions);
$chunkLimit = \count($dynamicRoutes);
@@ -104,7 +104,7 @@ public function getCompiledRoutes(bool $forDump = false): array
if ($forDump) {
$compiledRoutes[2] = $compiledRoutes[4];
}
- unset($conditions[0]);
+ unset($conditions['']);
if ($conditions) {
foreach ($conditions as $expression => $condition) {
@@ -427,7 +427,7 @@ private function compileRoute(Route $route, string $name, string|array|null $var
unset($defaults['_canonical_route']);
}
- if ($condition = $route->getCondition()) {
+ if ('' !== $condition = $route->getCondition()) {
$condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request', 'params']);
$condition = $conditions[$condition] ??= (str_contains($condition, '$request') ? 1 : -1) * \count($conditions);
} else {
diff --git a/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php b/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
index 5177c269ae..2105473b7c 100644
--- a/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
+++ b/core/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
@@ -34,8 +34,8 @@ trait CompiledUrlMatcherTrait
public function match(string $pathinfo): array
{
- $allow = $allowSchemes = [];
- if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
+ $allow = $allowSchemes = $trailingSlashSchemes = [];
+ if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes, $trailingSlashSchemes)) {
return $ret;
}
if ($allow) {
@@ -58,11 +58,17 @@ public function match(string $pathinfo): array
$this->context->setScheme($scheme);
}
} elseif ('' !== $trimmedPathinfo = rtrim($pathinfo, '/')) {
+ $fallbackSchemes = $trailingSlashSchemes;
+ $fallbackPathinfo = $pathinfo;
$pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo;
- if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) {
+ if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes, $trailingSlashSchemes)) {
return $this->redirect($pathinfo, $ret['_route']) + $ret;
}
- if ($allowSchemes) {
+ if ($allowSchemes = $allowSchemes ?: $trailingSlashSchemes) {
+ goto redirect_scheme;
+ }
+ if ($allowSchemes = $fallbackSchemes) {
+ $pathinfo = $fallbackPathinfo;
goto redirect_scheme;
}
}
@@ -70,9 +76,9 @@ public function match(string $pathinfo): array
throw new ResourceNotFoundException(\sprintf('No routes found for "%s".', $pathinfo));
}
- private function doMatch(string $pathinfo, array &$allow = [], array &$allowSchemes = []): array
+ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSchemes = [], array &$trailingSlashSchemes = []): array
{
- $allow = $allowSchemes = [];
+ $allow = $allowSchemes = $trailingSlashSchemes = [];
$pathinfo = '' === ($pathinfo = rawurldecode($pathinfo)) ? '/' : $pathinfo;
$trimmedPathinfo = '' === ($trimmedPathinfo = rtrim($pathinfo, '/')) ? '/' : $trimmedPathinfo;
$context = $this->context;
@@ -104,6 +110,8 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche
if ('/' !== $pathinfo && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
+ $trailingSlashSchemes = $allowSchemes;
+
return $allow = $allowSchemes = [];
}
continue;
@@ -154,6 +162,8 @@ private function doMatch(string $pathinfo, array &$allow = [], array &$allowSche
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) {
+ $trailingSlashSchemes = $allowSchemes;
+
return $allow = $allowSchemes = [];
}
continue;
diff --git a/core/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php b/core/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php
index 3e7b78b5e6..47478d68f8 100644
--- a/core/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php
+++ b/core/vendor/symfony/routing/Matcher/RedirectableUrlMatcher.php
@@ -44,6 +44,8 @@ public function match(string $pathinfo): array
} elseif ('' === $trimmedPathinfo = rtrim($pathinfo, '/')) {
throw $e;
} else {
+ $fallbackSchemes = $this->trailingSlashSchemes;
+ $fallbackPathinfo = $pathinfo;
try {
$pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo;
$ret = parent::match($pathinfo);
@@ -53,6 +55,13 @@ public function match(string $pathinfo): array
if ($this->allowSchemes) {
goto redirect_scheme;
}
+ if ($this->allowSchemes = $this->trailingSlashSchemes) {
+ goto redirect_scheme;
+ }
+ if ($this->allowSchemes = $fallbackSchemes) {
+ $pathinfo = $fallbackPathinfo;
+ goto redirect_scheme;
+ }
throw $e;
}
}
diff --git a/core/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php b/core/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php
index 5729df225a..6309f18a9c 100644
--- a/core/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php
+++ b/core/vendor/symfony/routing/Matcher/TraceableUrlMatcher.php
@@ -53,7 +53,7 @@ public function getTracesForRequest(Request $request): array
protected function matchCollection(string $pathinfo, RouteCollection $routes): array
{
// HEAD and GET are equivalent as per RFC
- if ('HEAD' === $method = $this->context->getMethod()) {
+ if ('HEAD' === $method = $requestMethod = $this->context->getMethod()) {
$method = 'GET';
}
$supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface;
@@ -127,6 +127,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods, true))) {
$this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route);
+ $this->trailingSlashSchemes = $this->allowSchemes;
return $this->allow = $this->allowSchemes = [];
}
@@ -140,7 +141,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a
continue;
}
- if ($requiredMethods && !\in_array($method, $requiredMethods, true)) {
+ if ($requiredMethods && !\in_array($method, $requiredMethods, true) && !\in_array($requestMethod, $requiredMethods, true)) {
$this->allow = array_merge($this->allow, $requiredMethods);
$this->addTrace(\sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route);
continue;
diff --git a/core/vendor/symfony/routing/Matcher/UrlMatcher.php b/core/vendor/symfony/routing/Matcher/UrlMatcher.php
index 8fde37685b..f5dafd5db1 100644
--- a/core/vendor/symfony/routing/Matcher/UrlMatcher.php
+++ b/core/vendor/symfony/routing/Matcher/UrlMatcher.php
@@ -43,6 +43,14 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
* @internal
*/
protected array $allowSchemes = [];
+
+ /**
+ * Collects URI schemes that would be allowed for the request before a trailing slash redirection took precedence.
+ *
+ * @internal
+ */
+ protected array $trailingSlashSchemes = [];
+
protected ?Request $request = null;
protected ExpressionLanguage $expressionLanguage;
@@ -69,7 +77,7 @@ public function getContext(): RequestContext
public function match(string $pathinfo): array
{
- $this->allow = $this->allowSchemes = [];
+ $this->allow = $this->allowSchemes = $this->trailingSlashSchemes = [];
$pathinfo = '' === ($pathinfo = rawurldecode($pathinfo)) ? '/' : $pathinfo;
if ($ret = $this->matchCollection($pathinfo, $this->routes)) {
@@ -114,7 +122,7 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
protected function matchCollection(string $pathinfo, RouteCollection $routes): array
{
// HEAD and GET are equivalent as per RFC
- if ('HEAD' === $method = $this->context->getMethod()) {
+ if ('HEAD' === $method = $requestMethod = $this->context->getMethod()) {
$method = 'GET';
}
$supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface;
@@ -164,6 +172,8 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a
if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) {
if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods, true))) {
+ $this->trailingSlashSchemes = $this->allowSchemes;
+
return $this->allow = $this->allowSchemes = [];
}
continue;
@@ -174,7 +184,7 @@ protected function matchCollection(string $pathinfo, RouteCollection $routes): a
continue;
}
- if ($requiredMethods && !\in_array($method, $requiredMethods, true)) {
+ if ($requiredMethods && !\in_array($method, $requiredMethods, true) && !\in_array($requestMethod, $requiredMethods, true)) {
$this->allow = array_merge($this->allow, $requiredMethods);
continue;
}
@@ -216,7 +226,7 @@ protected function getAttributes(Route $route, string $name, array $attributes):
protected function handleRouteRequirements(string $pathinfo, string $name, Route $route, array $routeParameters): array
{
// expression condition
- if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), [
+ if ('' !== $route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), [
'context' => $this->context,
'request' => $this->request ?: $this->createRequest($pathinfo),
'params' => $routeParameters,
diff --git a/core/vendor/symfony/string/AbstractUnicodeString.php b/core/vendor/symfony/string/AbstractUnicodeString.php
index 7c59fe9ec9..c6f7135641 100644
--- a/core/vendor/symfony/string/AbstractUnicodeString.php
+++ b/core/vendor/symfony/string/AbstractUnicodeString.php
@@ -405,10 +405,16 @@ public function localeTitle(string $locale): static
public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
{
- if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
+ if ('' === $chars) {
+ return clone $this;
+ }
+ if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" === $chars) {
+ $chars = preg_quote($chars);
+ } elseif (false === $ranges = preg_match('/\.\./u', $chars)) {
throw new InvalidArgumentException('Invalid UTF-8 chars.');
+ } else {
+ $chars = $ranges ? self::quoteCharRanges($chars) : preg_quote($chars);
}
- $chars = preg_quote($chars);
$str = clone $this;
$str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string);
@@ -418,10 +424,16 @@ public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
{
- if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
+ if ('' === $chars) {
+ return clone $this;
+ }
+ if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" === $chars) {
+ $chars = preg_quote($chars);
+ } elseif (false === $ranges = preg_match('/\.\./u', $chars)) {
throw new InvalidArgumentException('Invalid UTF-8 chars.');
+ } else {
+ $chars = $ranges ? self::quoteCharRanges($chars) : preg_quote($chars);
}
- $chars = preg_quote($chars);
$str = clone $this;
$str->string = preg_replace("{[$chars]++$}uD", '', $str->string);
@@ -451,10 +463,16 @@ public function trimPrefix($prefix): static
public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static
{
- if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) {
+ if ('' === $chars) {
+ return clone $this;
+ }
+ if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" === $chars) {
+ $chars = preg_quote($chars);
+ } elseif (false === $ranges = preg_match('/\.\./u', $chars)) {
throw new InvalidArgumentException('Invalid UTF-8 chars.');
+ } else {
+ $chars = $ranges ? self::quoteCharRanges($chars) : preg_quote($chars);
}
- $chars = preg_quote($chars);
$str = clone $this;
$str->string = preg_replace("{^[$chars]++}uD", '', $str->string);
@@ -544,6 +562,27 @@ public function width(bool $ignoreAnsiDecoration = true): int
return $width;
}
+ /**
+ * Quotes $chars for a character class, reading "x..y" as a range of code points like trim() does with bytes.
+ */
+ private static function quoteCharRanges(string $chars): string
+ {
+ $chars = preg_split('//u', $chars, -1, \PREG_SPLIT_NO_EMPTY);
+ $n = \count($chars);
+ $class = '';
+
+ for ($i = 0; $i < $n; ++$i) {
+ if ($i + 3 < $n && '.' === $chars[$i + 1] && '.' === $chars[$i + 2] && ($from = mb_ord($chars[$i], 'UTF-8')) <= ($to = mb_ord($chars[$i + 3], 'UTF-8'))) {
+ $class .= \sprintf('\x{%X}-\x{%X}', $from, $to);
+ $i += 3;
+ } elseif ('.' !== $chars[$i] || '.' !== ($chars[$i + 1] ?? '')) {
+ $class .= preg_quote($chars[$i]);
+ }
+ }
+
+ return $class;
+ }
+
private function pad(int $len, self $pad, int $type): static
{
$sLen = $this->length();
@@ -594,7 +633,9 @@ private function wcswidth(string $string): int
if ($joined) {
$joined = false;
- continue;
+ if ("\x7F" < $c[0] && preg_match(self::EMOJI_REGEX, $c)) {
+ continue;
+ }
}
$codePoint = mb_ord($c, 'UTF-8');
diff --git a/core/vendor/symfony/string/CodePointString.php b/core/vendor/symfony/string/CodePointString.php
index f699168e4d..97e4520fc9 100644
--- a/core/vendor/symfony/string/CodePointString.php
+++ b/core/vendor/symfony/string/CodePointString.php
@@ -55,6 +55,10 @@ public function chunk(int $length = 1): array
return [];
}
+ if (\strlen($this->string) <= $length) {
+ return [clone $this];
+ }
+
$rx = '/(';
while (65535 < $length) {
$rx .= '.{65535}';
@@ -197,7 +201,7 @@ public function replace(string $from, string $to): static
public function slice(int $start = 0, ?int $length = null): static
{
$str = clone $this;
- $str->string = mb_substr($this->string, $start, $length, 'UTF-8');
+ $str->string = mb_substr($this->string, max(-\PHP_INT_MAX, $start), max(-\PHP_INT_MAX, $length ?? \PHP_INT_MAX), 'UTF-8');
return $str;
}
@@ -209,8 +213,8 @@ public function splice(string $replacement, int $start = 0, ?int $length = null)
}
$str = clone $this;
- $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0;
- $length = $length ? \strlen(mb_substr(substr($this->string, $start), 0, $length, 'UTF-8')) : $length;
+ $start = $start ? \strlen(mb_substr($this->string, 0, max(-\PHP_INT_MAX, $start), 'UTF-8')) : 0;
+ $length = $length ? \strlen(mb_substr(substr($this->string, $start), 0, max(-\PHP_INT_MAX, $length), 'UTF-8')) : $length;
$str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);
return $str;
diff --git a/core/vendor/symfony/string/Slugger/AsciiSlugger.php b/core/vendor/symfony/string/Slugger/AsciiSlugger.php
index 689a404788..e15497c5f6 100644
--- a/core/vendor/symfony/string/Slugger/AsciiSlugger.php
+++ b/core/vendor/symfony/string/Slugger/AsciiSlugger.php
@@ -63,7 +63,7 @@ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface
/**
* Cache of transliterators per locale.
*
- * @var \Transliterator[]
+ * @var (\Transliterator|null)[]
*/
private array $transliterators = [];
@@ -106,7 +106,7 @@ public function slug(string $string, string $separator = '-', ?string $locale =
$locale ??= $this->defaultLocale;
$transliterator = [];
- if ($locale && ('de' === $locale || str_starts_with($locale, 'de_'))) {
+ if ($locale && ('de' === $locale || str_starts_with($locale, 'de_') || str_starts_with($locale, 'de-'))) {
// Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl)
$transliterator = ['de-ASCII'];
} elseif (\function_exists('transliterator_transliterate') && $locale) {
@@ -151,6 +151,8 @@ public function slug(string $string, string $separator = '-', ?string $locale =
private function createTransliterator(string $locale): ?\Transliterator
{
+ $locale = str_replace('-', '_', $locale);
+
if (\array_key_exists($locale, $this->transliterators)) {
return $this->transliterators[$locale];
}
@@ -167,10 +169,10 @@ private function createTransliterator(string $locale): ?\Transliterator
// Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
- $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
+ return $this->transliterators[$locale] = $this->transliterators[$parent] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
}
- return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null;
+ return $this->transliterators[$locale] = null;
}
private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator
@@ -181,6 +183,8 @@ private function createEmojiTransliterator(?string $locale): ?EmojiTransliterato
return null;
}
+ $locale = null !== $locale ? str_replace('-', '_', $locale) : null;
+
while (null !== $locale) {
try {
return EmojiTransliterator::create("emoji-$locale");
@@ -197,11 +201,11 @@ private static function getParentLocale(?string $locale): ?string
if (!$locale) {
return null;
}
- if (false === $str = strrchr($locale, '_')) {
+ if (false === $i = strrpos(str_replace('-', '_', $locale), '_')) {
// no parent locale
return null;
}
- return substr($locale, 0, -\strlen($str));
+ return substr($locale, 0, $i);
}
}
diff --git a/core/vendor/symfony/string/UnicodeString.php b/core/vendor/symfony/string/UnicodeString.php
index ca3499ffea..d23833b048 100644
--- a/core/vendor/symfony/string/UnicodeString.php
+++ b/core/vendor/symfony/string/UnicodeString.php
@@ -73,6 +73,10 @@ public function chunk(int $length = 1): array
return [];
}
+ if (\strlen($this->string) <= $length) {
+ return [clone $this];
+ }
+
$rx = '/(';
while (65535 < $length) {
$rx .= '\X{65535}';
@@ -179,7 +183,7 @@ public function indexOfLast(string|iterable|AbstractString $needle, int $offset
if (0 > $offset) {
// workaround https://bugs.php.net/74264
if (0 > $offset += grapheme_strlen($needle)) {
- $string = grapheme_substr($string, 0, $offset);
+ $string = grapheme_substr($string, 0, max(-2147483648, $offset));
}
$offset = 0;
}
@@ -248,12 +252,10 @@ public function replace(string $from, string $to): static
if ('' !== $from && false !== $from) {
$tail = $str->string;
$result = '';
- $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos';
- while ('' !== $tail && false !== $i = $indexOf($tail, $from)) {
- $slice = grapheme_substr($tail, 0, $i);
- $result .= $slice.$to;
- $tail = substr($tail, \strlen($slice) + \strlen($from));
+ while ('' !== $tail && false !== $i = $this->search($tail, $from, $length)) {
+ $result .= substr($tail, 0, $i).$to;
+ $tail = substr($tail, $i + $length);
}
$str->string = $result.$tail;
@@ -284,7 +286,10 @@ public function slice(int $start = 0, ?int $length = null): static
{
$str = clone $this;
- $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647);
+ // grapheme_substr() accepts only 32-bit offsets and lengths
+ $start = max(-2147483648, min($start, 2147483647));
+ $length = max(-2147483648, min($length ?? 2147483647, 2147483647));
+ $str->string = (string) grapheme_substr($this->string, $start, $length);
return $str;
}
@@ -293,8 +298,8 @@ public function splice(string $replacement, int $start = 0, ?int $length = null)
{
$str = clone $this;
- $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0;
- $length = $length ? \strlen(grapheme_substr(substr($this->string, $start), 0, $length)) : $length;
+ $start = $start ? \strlen(grapheme_substr($this->string, 0, max(-2147483648, min($start, 2147483647)))) : 0;
+ $length = $length ? \strlen(grapheme_substr(substr($this->string, $start), 0, max(-2147483648, min($length, 2147483647)))) : $length;
$str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647);
if (normalizer_is_normalized($str->string)) {
@@ -333,12 +338,11 @@ public function split(string $delimiter, ?int $limit = null, ?int $flags = null)
$str = clone $this;
$tail = $this->string;
$chunks = [];
- $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos';
- while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) {
- $str->string = grapheme_substr($tail, 0, $i);
+ while (1 < $limit && false !== $i = $this->search($tail, $delimiter, $length)) {
+ $str->string = substr($tail, 0, $i);
$chunks[] = clone $str;
- $tail = substr($tail, \strlen($str->string) + \strlen($delimiter));
+ $tail = substr($tail, $i + $length);
--$limit;
}
@@ -473,4 +477,49 @@ public function __clone()
$this->ignoreCase = false;
}
+
+ /**
+ * Returns the byte offset of the first occurrence of $needle in $haystack and sets $length to its length in bytes.
+ */
+ private function search(string $haystack, string $needle, ?int &$length = null): int|false
+ {
+ if (false === $i = $this->ignoreCase ? grapheme_stripos($haystack, $needle) : grapheme_strpos($haystack, $needle)) {
+ return false;
+ }
+
+ $offset = \strlen(grapheme_substr($haystack, 0, $i));
+ $length = \strlen($needle);
+ $match = substr($haystack, $offset, $length);
+
+ // On non-ASCII strings, grapheme_strpos() ignores characters like U+200B.
+ // Its match can then differ from $needle, or start after the first characters of $needle.
+ // Without ext-intl, a match that starts inside a grapheme is reported at the next grapheme.
+ if (!$this->ignoreCase) {
+ if ($needle === $match) {
+ return $offset;
+ }
+
+ $previous = $i ? \strlen(grapheme_substr($haystack, 0, $i - 1)) : 0;
+
+ return strpos($haystack, $needle, max(0, min($offset + 1 - $length, $previous + 1)));
+ }
+
+ if (0 === strcasecmp($needle, $match) || (preg_match('//u', $match) && (
+ mb_convert_case($needle, \MB_CASE_FOLD_SIMPLE, 'UTF-8') === mb_convert_case($match, \MB_CASE_FOLD_SIMPLE, 'UTF-8')
+ || (0 === grapheme_stripos($needle, $match) && 0 === grapheme_stripos($match, $needle))
+ ))) {
+ return $offset;
+ }
+
+ $previous = $i ? mb_strlen(grapheme_substr($haystack, 0, $i - 1), 'UTF-8') : 0;
+ $i = mb_strlen(substr($haystack, 0, $offset), 'UTF-8');
+
+ if (false === $i = mb_stripos($haystack, $needle, max(0, min($i + 1 - mb_strlen($needle, 'UTF-8'), $previous + 1)), 'UTF-8')) {
+ return false;
+ }
+
+ $length = \strlen(mb_substr($haystack, $i, mb_strlen($needle, 'UTF-8'), 'UTF-8'));
+
+ return \strlen(mb_substr($haystack, 0, $i, 'UTF-8'));
+ }
}
diff --git a/core/vendor/symfony/translation/Command/XliffLintCommand.php b/core/vendor/symfony/translation/Command/XliffLintCommand.php
index 30553821f6..45e2110ff0 100644
--- a/core/vendor/symfony/translation/Command/XliffLintCommand.php
+++ b/core/vendor/symfony/translation/Command/XliffLintCommand.php
@@ -133,11 +133,11 @@ private function validate(string $content, ?string $file = null): array
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
$expectedFilenamePattern = $this->requireStrictFileNames ? \sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : \sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
- if (0 === preg_match($expectedFilenamePattern, basename($file))) {
+ if (null !== $file && 0 === preg_match($expectedFilenamePattern, basename($file))) {
$errors[] = [
'line' => -1,
'column' => -1,
- 'message' => \sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
+ 'message' => \sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "%s" attribute of the file.', basename($file), $targetLanguage, str_starts_with($document->documentElement->getAttribute('version'), '2.') ? 'trgLang' : 'target-language'),
];
}
}
@@ -264,13 +264,14 @@ private function isReadable(string $fileOrDirectory): bool
private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
{
- foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
- if ('target-language' === $attribute->nodeName) {
- return $attribute->nodeValue;
- }
+ $xliff = $xliffContents->documentElement;
+ if ('xliff' === $xliff?->localName && str_starts_with($xliff->getAttribute('version'), '2.')) {
+ return $xliff->getAttribute('trgLang') ?: null;
}
- return null;
+ $file = $xliffContents->getElementsByTagName('file')->item(0);
+
+ return $file instanceof \DOMElement ? ($file->getAttribute('target-language') ?: null) : null;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
diff --git a/core/vendor/symfony/translation/Dumper/FileDumper.php b/core/vendor/symfony/translation/Dumper/FileDumper.php
index 6fae26438c..91668421c4 100644
--- a/core/vendor/symfony/translation/Dumper/FileDumper.php
+++ b/core/vendor/symfony/translation/Dumper/FileDumper.php
@@ -49,7 +49,7 @@ public function dump(MessageCatalogue $messages, array $options = []): void
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
if (!file_exists($fullpath)) {
$directory = \dirname($fullpath);
- if (!file_exists($directory) && !@mkdir($directory, 0o777, true)) {
+ if (!file_exists($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)) {
throw new RuntimeException(\sprintf('Unable to create directory "%s".', $directory));
}
}
diff --git a/core/vendor/symfony/translation/Resources/data/parents.json b/core/vendor/symfony/translation/Resources/data/parents.json
index 0197e451b3..ce82bc7704 100644
--- a/core/vendor/symfony/translation/Resources/data/parents.json
+++ b/core/vendor/symfony/translation/Resources/data/parents.json
@@ -136,7 +136,11 @@
"ff_Adlm": "root",
"hi_Latn": "en_IN",
"kk_Arab": "root",
+ "kok_Latn": "root",
"ks_Deva": "root",
+ "kxv_Deva": "root",
+ "kxv_Orya": "root",
+ "kxv_Telu": "root",
"nb": "no",
"nn": "no",
"pa_Arab": "root",
@@ -151,9 +155,12 @@
"pt_ST": "pt_PT",
"pt_TL": "pt_PT",
"sd_Deva": "root",
+ "shi_Latn": "root",
"sr_Latn": "root",
"uz_Arab": "root",
"uz_Cyrl": "root",
+ "vai_Latn": "root",
+ "yue_Hans": "root",
"zh_Hant": "root",
"zh_Hant_MO": "zh_Hant_HK"
}
diff --git a/core/vendor/symfony/translation/Util/XliffUtils.php b/core/vendor/symfony/translation/Util/XliffUtils.php
index 1ce8f185ae..88613be6fc 100644
--- a/core/vendor/symfony/translation/Util/XliffUtils.php
+++ b/core/vendor/symfony/translation/Util/XliffUtils.php
@@ -108,8 +108,11 @@ private static function shouldEnableEntityLoader(): bool
$dom->loadXML(' ');
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
- register_shutdown_function(static function () use ($tmpfile) {
- @unlink($tmpfile);
+ $pid = getmypid();
+ register_shutdown_function(static function () use ($tmpfile, $pid) {
+ if (getmypid() === $pid) {
+ @unlink($tmpfile);
+ }
});
$schema = '
@@ -167,8 +170,11 @@ private static function fixXmlLocation(string $schemaSource, string $xmlUri): st
$newPath = self::getFileUrl($path);
} elseif ($tmpfile = tempnam(sys_get_temp_dir(), 'symfony')) {
copy($path, $tmpfile);
- register_shutdown_function(static function () use ($tmpfile) {
- @unlink($tmpfile);
+ $pid = getmypid();
+ register_shutdown_function(static function () use ($tmpfile, $pid) {
+ if (getmypid() === $pid) {
+ @unlink($tmpfile);
+ }
});
$newPath = self::getFileUrl($tmpfile);
} else {
diff --git a/core/vendor/symfony/var-dumper/Caster/ExceptionCaster.php b/core/vendor/symfony/var-dumper/Caster/ExceptionCaster.php
index 9726a99edc..1111a446df 100644
--- a/core/vendor/symfony/var-dumper/Caster/ExceptionCaster.php
+++ b/core/vendor/symfony/var-dumper/Caster/ExceptionCaster.php
@@ -322,7 +322,7 @@ private static function traceUnshift(array &$trace, ?string $class, string $file
private static function extractSource(string $srcLines, int $line, int $srcContext, string $lang, ?string $file, array $frame): EnumStub
{
- $srcLines = explode("\n", $srcLines);
+ $srcLines = explode("\n", str_replace(["\r\n", "\r"], "\n", $srcLines));
$src = [];
for ($i = $line - 1 - $srcContext; $i <= $line - 1 + $srcContext; ++$i) {
@@ -359,7 +359,7 @@ private static function extractSource(string $srcLines, int $line, int $srcConte
do {
$pad = null;
for ($i = $srcContext << 1; $i >= 0; --$i) {
- if (isset($src[$i][$ltrim]) && "\r" !== ($c = $src[$i][$ltrim]) && "\n" !== $c) {
+ if (isset($src[$i][$ltrim]) && "\n" !== ($c = $src[$i][$ltrim])) {
$pad ??= $c;
if ((' ' !== $c && "\t" !== $c) || $pad !== $c) {
break;
@@ -373,7 +373,7 @@ private static function extractSource(string $srcLines, int $line, int $srcConte
foreach ($src as $i => $c) {
if ($ltrim) {
- $c = isset($c[$ltrim]) && "\r" !== $c[$ltrim] ? substr($c, $ltrim) : ltrim($c, " \t");
+ $c = isset($c[$ltrim]) ? substr($c, $ltrim) : ltrim($c, " \t");
}
$c = substr($c, 0, -1);
if ($i !== $srcContext) {
diff --git a/core/vendor/symfony/var-dumper/Cloner/Stub.php b/core/vendor/symfony/var-dumper/Cloner/Stub.php
index 18577f4b47..81faac1a92 100644
--- a/core/vendor/symfony/var-dumper/Cloner/Stub.php
+++ b/core/vendor/symfony/var-dumper/Cloner/Stub.php
@@ -14,6 +14,8 @@
/**
* Represents the main properties of a PHP variable.
*
+ * Stub classes meant to be extended should not declare private properties: they are not serialized with child classes.
+ *
* @author Nicolas Grekas
*/
class Stub
diff --git a/core/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php b/core/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php
index 2beaea684d..a3b2fabf59 100644
--- a/core/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php
+++ b/core/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php
@@ -66,7 +66,7 @@ public function getContext(): ?array
$file = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getPath() : null;
if ($src) {
- $src = explode("\n", $src);
+ $src = explode("\n", str_replace(["\r\n", "\r"], "\n", $src));
$fileExcerpt = [];
for ($i = max($line - 3, 1), $max = min($line + 3, \count($src)); $i <= $max; ++$i) {
diff --git a/core/vendor/tracy/tracy/composer.json b/core/vendor/tracy/tracy/composer.json
index a4f2e7530e..d46107b08a 100644
--- a/core/vendor/tracy/tracy/composer.json
+++ b/core/vendor/tracy/tracy/composer.json
@@ -1,21 +1,17 @@
{
"name": "tracy/tracy",
- "description": "😎 Tracy: the addictive tool to ease debugging PHP code for cool developers. Friendly design, logging, profiler, advanced features like debugging AJAX calls or CLI support. You will love it.",
- "keywords": ["debug", "debugger", "nette", "profiler", "xdebug"],
+ "description": "😎 Tracy: the addictive PHP debugger. Clear error pages with full context, a debug bar, readable dumps, AJAX and CLI support, and logging with e-mail alerts in production.",
+ "keywords": ["tracy", "nette", "debugger", "debugging", "debug bar", "error handler", "dump", "var_dump", "logging", "whoops"],
"homepage": "https://tracy.nette.org",
"license": ["BSD-3-Clause"],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
- },
- {
- "name": "Nette Community",
- "homepage": "https://nette.org/contributors"
}
],
"require": {
- "php": "8.2 - 8.5",
+ "php": "8.2 - 8.6",
"ext-session": "*",
"ext-json": "*"
},
@@ -27,7 +23,7 @@
"nette/tester": "^2.6",
"latte/latte": "^2.5 || ^3.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
- "phpstan/phpstan": "^2.1@stable",
+ "phpstan/phpstan": "^2.3@stable",
"phpstan/extension-installer": "^1.4@stable",
"nette/phpstan-rules": "^1.0"
},
diff --git a/core/vendor/tracy/tracy/readme.md b/core/vendor/tracy/tracy/readme.md
index 2298b2d432..30b787200f 100644
--- a/core/vendor/tracy/tracy/readme.md
+++ b/core/vendor/tracy/tracy/readme.md
@@ -48,7 +48,7 @@ composer require tracy/tracy
Alternatively, you can download the whole package or [tracy.phar](https://github.com/nette/tracy/releases) file.
-Tracy is compatible with PHP 8.2 to 8.5.
+Tracy is compatible with PHP 8.2 to 8.6.
diff --git a/core/vendor/tracy/tracy/src/Bridges/Nette/Bridge.php b/core/vendor/tracy/tracy/src/Bridges/Nette/Bridge.php
index 31108464ea..d8de7b9720 100644
--- a/core/vendor/tracy/tracy/src/Bridges/Nette/Bridge.php
+++ b/core/vendor/tracy/tracy/src/Bridges/Nette/Bridge.php
@@ -9,9 +9,7 @@
use Nette;
use Tracy;
-use Tracy\BlueScreen;
-use Tracy\Helpers;
-use const ENT_IGNORE;
+use Tracy\{BlueScreen, Helpers};
/**
diff --git a/core/vendor/tracy/tracy/src/Bridges/Psr/PsrToTracyLoggerAdapter.php b/core/vendor/tracy/tracy/src/Bridges/Psr/PsrToTracyLoggerAdapter.php
index d6d19c3c1d..b6ae661e94 100644
--- a/core/vendor/tracy/tracy/src/Bridges/Psr/PsrToTracyLoggerAdapter.php
+++ b/core/vendor/tracy/tracy/src/Bridges/Psr/PsrToTracyLoggerAdapter.php
@@ -37,7 +37,14 @@ public function __construct(
public function log(mixed $value, string $level = self::INFO): void
{
if ($value instanceof \Throwable) {
- $message = get_debug_type($value) . ': ' . $value->getMessage() . ($value->getCode() ? ' #' . $value->getCode() : '') . ' in ' . $value->getFile() . ':' . $value->getLine();
+ $message = get_debug_type($value)
+ . ': '
+ . $value->getMessage()
+ . ($value->getCode() ? ' #' . $value->getCode() : '')
+ . ' in '
+ . $value->getFile()
+ . ':'
+ . $value->getLine();
$context = ['exception' => $value];
} elseif (!is_string($value)) {
diff --git a/core/vendor/tracy/tracy/src/Tracy/Bar/Bar.php b/core/vendor/tracy/tracy/src/Tracy/Bar/Bar.php
index c85df6c745..aab89d6d1e 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Bar/Bar.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Bar/Bar.php
@@ -65,6 +65,7 @@ public function renderLoader(DeferredContent $defer): void
/**
* Renders debug bar.
+ * @internal
*/
public function render(DeferredContent $defer): void
{
@@ -89,7 +90,10 @@ public function render(DeferredContent $defer): void
}
} elseif (Helpers::isHtmlMode()) {
if (preg_match('#^Content-Length:#im', implode("\n", headers_list()))) {
- Debugger::log(new \LogicException('Tracy cannot display the Bar because the Content-Length header is being sent'), Debugger::EXCEPTION);
+ Debugger::log(
+ new \LogicException('Tracy cannot display the Bar because the Content-Length header is being sent'),
+ Debugger::EXCEPTION,
+ );
}
$content = $this->renderPartial('main');
@@ -180,10 +184,11 @@ private function renderPanels(string $suffix = ''): array
/**
* Captures debug bar as plain text (markdown) for AI agents.
+ * @internal
*/
public function renderAgent(): string
{
- $time = number_format((microtime(true) - Debugger::$time) * 1000, 1);
+ $time = number_format((microtime(as_float: true) - Debugger::$time) * 1000, 1);
$memory = number_format(memory_get_peak_usage() / 1_000_000, 2);
$parts = ["Tracy Bar | $time ms | $memory MB"];
foreach ($this->panels as $panel) {
diff --git a/core/vendor/tracy/tracy/src/Tracy/Bar/panels/info.panel.php b/core/vendor/tracy/tracy/src/Tracy/Bar/panels/info.panel.php
index 325a9cad13..c43fb56dcf 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Bar/panels/info.panel.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Bar/panels/info.panel.php
@@ -3,7 +3,7 @@
namespace Tracy;
use Composer\Autoload\ClassLoader;
-use function count;
+use function count, extension_loaded, function_exists;
use const PHP_VERSION, PHP_ZTS;
/** @var DefaultBarPanel $this */
@@ -16,7 +16,10 @@
$systemUsage = -round(($this->cpuUsage['ru_stime.tv_sec'] * 1e6 + $this->cpuUsage['ru_stime.tv_usec']) / $this->time / 10000);
}
-$countClasses = fn(array $list): int => count(array_filter($list, fn(string $name): bool => (new \ReflectionClass($name))->isUserDefined()));
+$countClasses = fn(array $list): int => count(array_filter(
+ $list,
+ fn(string $name): bool => (new \ReflectionClass($name))->isUserDefined(),
+));
$ipFormatter = static function (?string $ip): ?string {
if ($ip === '127.0.0.1' || $ip === '::1') {
@@ -31,13 +34,17 @@
$info = [
'Execution time' => number_format($this->time * 1000, 1, '.', "\u{202f}") . "\u{202f}ms",
- 'CPU usage user + system' => isset($userUsage, $systemUsage) ? (int) $userUsage . "\u{202f}% + " . (int) $systemUsage . "\u{202f}%" : null,
+ 'CPU usage user + system' => isset($userUsage, $systemUsage)
+ ? (int) $userUsage . "\u{202f}% + " . (int) $systemUsage . "\u{202f}%"
+ : null,
'Peak of allocated memory' => number_format(memory_get_peak_usage() / 1_000_000, 2, '.', "\u{202f}") . "\u{202f}MB",
'Included files' => count(get_included_files()),
'Classes + interfaces + traits' => $countClasses(get_declared_classes()) . ' + '
. $countClasses(get_declared_interfaces()) . ' + ' . $countClasses(get_declared_traits()),
'OPcache' => $opcache ? round(count($cachedFiles) * 100 / count(get_included_files())) . "\u{202f}% cached" : '–',
- 'JIT' => empty($jit['buffer_size']) ? '–' : round(($jit['buffer_size'] - $jit['buffer_free']) / $jit['buffer_size'] * 100) . "\u{202f}% used",
+ 'JIT' => empty($jit['buffer_size'])
+ ? '–'
+ : round(($jit['buffer_size'] - $jit['buffer_free']) / $jit['buffer_size'] * 100) . "\u{202f}% used",
'Your IP' => $ipFormatter($_SERVER['REMOTE_ADDR'] ?? null),
'Server IP' => $ipFormatter($_SERVER['SERVER_ADDR'] ?? null),
'HTTP method / response code' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] . ' / ' . http_response_code() : null,
@@ -50,7 +57,7 @@
$info = array_map('strval', array_filter($info + (array) $this->data));
$packages = $devPackages = [];
-if (class_exists(ClassLoader::class, autoload: false)) {
+if (class_exists(ClassLoader::class, false)) {
$baseDir = (function () {
$baseDir = null;
@include dirname((string) (new \ReflectionClass(ClassLoader::class))->getFileName()) . '/autoload_psr4.php'; // @ may not exist
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php
index a670f58f0c..f1df4a7d47 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/BlueScreen.php
@@ -7,8 +7,8 @@
namespace Tracy;
-use function count, in_array;
-use const ARRAY_FILTER_USE_KEY, ENT_IGNORE, PHP_VERSION_ID;
+use function count, function_exists, in_array;
+use const INFO_CONFIGURATION, INFO_MODULES, PHP_VERSION_ID;
/**
@@ -72,7 +72,7 @@ public function __construct()
public function addPanel(callable $panel): static
{
$panel = $panel(...);
- if (!in_array($panel, $this->panels, strict: true)) {
+ if (!in_array($panel, $this->panels, true)) {
$this->panels[] = $panel;
}
@@ -135,7 +135,10 @@ public function renderAgent(\Throwable $exception, ?array $logLocation = null):
/** @internal */
public function renderToAjax(\Throwable $exception, DeferredContent $defer): void
{
- $defer->addSetup('Tracy.BlueScreen.loadAjax', Helpers::capture(fn() => $this->renderTemplate($exception, __DIR__ . '/dist/content.phtml')));
+ $defer->addSetup(
+ 'Tracy.BlueScreen.loadAjax',
+ Helpers::capture(fn() => $this->renderTemplate($exception, __DIR__ . '/dist/content.phtml')),
+ );
if (Helpers::isAgent()) {
$defer->addSetup('console.error', $this->renderAgent($exception));
}
@@ -148,7 +151,7 @@ public function renderToAjax(\Throwable $exception, DeferredContent $defer): voi
*/
public function renderToFile(\Throwable $exception, string $file, ?array $logLocation = null): bool
{
- if ($handle = @fopen($file, 'x')) {
+ if ($handle = @fopen($file, 'x')) { // @ file may already exist
ob_start(); // double buffer prevents sending HTTP headers in some PHP
ob_start(function ($buffer) use ($handle) {
fwrite($handle, $buffer);
@@ -159,7 +162,7 @@ public function renderToFile(\Throwable $exception, string $file, ?array $logLoc
ob_end_clean();
fclose($handle);
- if ($handle = @fopen(substr($file, 0, -5) . '.md', 'x')) {
+ if ($handle = @fopen(substr($file, 0, -5) . '.md', 'x')) { // @ file may already exist
fwrite($handle, $this->renderAgent($exception, $logLocation));
fclose($handle);
}
@@ -195,7 +198,10 @@ private function renderTemplate(
$httpHeaders = apache_request_headers();
} else {
$httpHeaders = array_filter($_SERVER, fn($k) => str_starts_with($k, 'HTTP_'), ARRAY_FILTER_USE_KEY);
- $httpHeaders = array_combine(array_map(fn($k) => strtolower(strtr(substr($k, 5), '_', '-')), array_keys($httpHeaders)), $httpHeaders);
+ $httpHeaders = array_combine(
+ array_map(fn($k) => strtolower(strtr(substr($k, 5), '_', '-')), array_keys($httpHeaders)),
+ $httpHeaders,
+ );
}
$this->snapshot = [];
@@ -203,14 +209,15 @@ private function renderTemplate(
$dump = $this->getDumper();
$agentDump = $this->getAgentDumper();
- $css = array_map(file_get_contents(...), array_merge([
+ $css = array_map(file_get_contents(...), [
__DIR__ . '/../assets/reset.css',
__DIR__ . '/assets/bluescreen.css',
__DIR__ . '/../assets/toggle.css',
__DIR__ . '/../assets/table-sort.css',
__DIR__ . '/../assets/tabs.css',
__DIR__ . '/../Dumper/assets/dumper-light.css',
- ], Debugger::$customCssFiles));
+ ...Debugger::$customCssFiles,
+ ]);
$css = Helpers::minifyCss(implode('', $css));
$js = array_map(fn($file) => '(function(){' . file_get_contents($file) . '})();', [
@@ -290,7 +297,7 @@ private function renderActions(\Throwable $ex): array
if (preg_match('# ([\'"])(\w{3,}(?:\\\\\w{2,})+)\1#i', $ex->getMessage(), $m)) {
$class = $m[2];
if (
- !class_exists($class, autoload: false) && !interface_exists($class, autoload: false) && !trait_exists($class, autoload: false)
+ !class_exists($class, false) && !interface_exists($class, autoload: false) && !trait_exists($class, autoload: false)
&& ($file = Helpers::guessClassFile($class)) && !@is_file($file) // @ - may trigger error
) {
[$content, $line] = $this->generateNewFileContents($file, $class);
@@ -368,10 +375,17 @@ public static function highlightFile(
$source = $php
? CodeHighlighter::highlightPhp($source, $line, $column)
- : '
' . CodeHighlighter::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $column) . '
';
+ : ''
+ . CodeHighlighter::highlightLine(htmlspecialchars($source, ENT_IGNORE, 'UTF-8'), $line, $column)
+ . '
';
if ($editor = Helpers::editorUri($file, $line)) {
- $source = substr_replace($source, ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"', 4, 0);
+ $source = substr_replace(
+ $source,
+ ' title="Ctrl-Click to open in editor" data-tracy-href="' . Helpers::escapeHtml($editor) . '"',
+ 4,
+ 0,
+ );
}
return $source;
@@ -417,10 +431,14 @@ public function isCollapsed(string $file): bool
public function prepareStack(\Throwable $ex): array
{
$stack = $ex->getTrace();
- while ($stack && (
- in_array($stack[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)
- || (($stack[0]['class'] ?? null) === Debugger::class && in_array($stack[0]['function'], ['shutdownHandler', 'errorHandler'], true))
- )) {
+ while (
+ $stack
+ && (
+ in_array($stack[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)
+ || (($stack[0]['class'] ?? null) === Debugger::class
+ && in_array($stack[0]['function'], ['shutdownHandler', 'errorHandler'], true))
+ )
+ ) {
array_shift($stack);
}
@@ -429,7 +447,10 @@ public function prepareStack(\Throwable $ex): array
!$ex instanceof \ErrorException
|| in_array($ex->getSeverity(), [E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED], true)
) {
- $n = Helpers::countTransparentFrames([['file' => $ex->getFile(), 'line' => $ex->getLine()], ...$stack], [...$this->collapsePaths, ...Debugger::$transparentPaths]);
+ $n = Helpers::countTransparentFrames(
+ [['file' => $ex->getFile(), 'line' => $ex->getLine()], ...$stack],
+ [...$this->collapsePaths, ...Debugger::$transparentPaths],
+ );
$expanded = $n > 0 && $n <= count($stack) ? $n - 1 : null;
}
@@ -455,7 +476,10 @@ public function getDumper(): \Closure
}
- /** @return \Closure(mixed, int|string): string */
+ /**
+ * @return \Closure(mixed, int|string): string
+ * @internal
+ */
public function getAgentDumper(): \Closure
{
return fn($v, $k = null): string => Dumper::toText($v, [
@@ -481,7 +505,7 @@ public function formatMessage(\Throwable $exception): string
$msg = preg_replace_callback(
'#(\w+\\\[\w\\\]+\w)(?:::(\w+))?#',
function ($m) {
- $classLike = class_exists($m[1], autoload: false) || interface_exists($m[1], autoload: false) || trait_exists($m[1], autoload: false);
+ $classLike = class_exists($m[1], false) || interface_exists($m[1], autoload: false) || trait_exists($m[1], autoload: false);
if ($classLike && isset($m[2]) && method_exists($m[1], $m[2])) {
$r = new \ReflectionMethod($m[1], $m[2]);
} elseif ($classLike) {
@@ -492,7 +516,11 @@ function ($m) {
return $m[0];
}
- return '' . $m[0] . ' ';
+ return ''
+ . $m[0]
+ . ' ';
},
$msg,
);
@@ -501,7 +529,11 @@ function ($m) {
$msg = preg_replace_callback(
'#([\w\\\/.:-]+\.(?:php|phpt|phtml|latte|neon))(?|:(\d+)| on line (\d+))?#',
fn($m) => @is_file($m[1]) // @ - may trigger error
- ? '' . $m[0] . ' '
+ ? ''
+ . $m[0]
+ . ' '
: $m[0],
$msg,
);
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php
index 0820b4a4ac..f343ffebe6 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/CodeHighlighter.php
@@ -7,8 +7,7 @@
namespace Tracy;
-use function array_pop, array_shift, array_unshift, count, explode, floor, implode, max, min, preg_replace, preg_replace_callback, rtrim, sprintf, str_replace, strip_tags, strlen, strtr;
-use const T_CLASS_C, T_CLOSE_TAG, T_COMMENT, T_CONSTANT_ENCAPSED_STRING, T_DIR, T_DNUMBER, T_DOC_COMMENT, T_ENCAPSED_AND_WHITESPACE, T_FILE, T_FUNC_C, T_INLINE_HTML, T_LINE, T_LNUMBER, T_METHOD_C, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, T_NAME_RELATIVE, T_NS_C, T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_STRING, T_TRAIT_C, T_VARIABLE, T_WHITESPACE;
+use function count, strlen;
/** @internal */
@@ -87,7 +86,7 @@ private static function highlightPhpCode(string $code): string
$code = str_replace("\r\n", "\n", $code);
$code = preg_replace('#(__halt_compiler\s*\(\)\s*;).*#is', '$1', $code);
$code = rtrim($code);
- $code = preg_replace('#/\*sensitive\{\*/.*?/\*}\*/#s', Dumper\Describer::HiddenValue, $code);
+ $code = self::hideSensitive($code);
$last = $out = '';
foreach (\PhpToken::tokenize($code) as $token) {
@@ -121,6 +120,19 @@ private static function highlightPhpCode(string $code): string
}
+ /**
+ * Replaces sensitive regions with a placeholder, keeping the line numbering intact.
+ */
+ public static function hideSensitive(string $code): string
+ {
+ return preg_replace_callback(
+ '#/\*sensitive\{\*/.*?/\*}\*/#s',
+ fn($m) => Dumper\Describer::HiddenValue . str_repeat("\n", substr_count($m[0], "\n")),
+ $code,
+ );
+ }
+
+
/**
* Returns syntax highlighted source code to Terminal.
*/
diff --git a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml
index d0ab024516..9d82e64e7c 100644
--- a/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml
+++ b/core/vendor/tracy/tracy/src/Tracy/BlueScreen/dist/agent.phtml
@@ -1,5 +1,6 @@
$srcLine) /* pos 24:2 */ {
- echo sprintf('%s%4d | %s', $i + 1 === $line ? '► ' : ' ', $i + 1, rtrim($srcLine)) /* pos 25:3 */;
+ foreach (array_slice($srcLines, $start, $end - $start, true) as $i => $srcLine) /* pos 26:2 */ {
+ echo sprintf('%s%4d | %s', $i + 1 === $line ? '► ' : ' ', $i + 1, rtrim($srcLine)) /* pos 27:3 */;
echo "\n";
}
echo '```
';
- $mapped = Debugger::mapSource($file, $line) /* pos 28:2 */;
- if (!$mapped || !@is_file($mapped['file'])) /* pos 29:2 */ return;
- $mLine = $mapped['line'] /* pos 30:2 */;
+ $mapped = Debugger::mapSource($file, $line) /* pos 30:2 */;
+ if (!$mapped || !@is_file($mapped['file'])) /* pos 31:2 */ return;
+ $mLine = $mapped['line'] /* pos 32:2 */;
echo '
Mapped source: ';
- echo Tracy\Helpers::escapeMd($mapped['file'] . ($mLine ? ':' . $mLine : '')) /* pos 32:17 */;
+ echo Tracy\Helpers::escapeMd($mapped['file'] . ($mLine ? ':' . $mLine : '')) /* pos 34:17 */;
echo "\n";
- $mappedLines = @file($mapped['file']) /* pos 33:2 */;
- if ($mappedLines && $mLine) /* pos 34:2 */ {
- $mStart = (int) max(0, $mLine - 4) /* pos 35:3 */;
- $mEnd = (int) min(count($mappedLines), $mLine + 3) /* pos 36:3 */;
+ $mappedLines = @file($mapped['file']) /* pos 35:2 */;
+ if ($mappedLines && $mLine) /* pos 36:2 */ {
+ $mStart = (int) max(0, $mLine - 4) /* pos 37:3 */;
+ $mEnd = (int) min(count($mappedLines), $mLine + 3) /* pos 38:3 */;
echo '
```
';
- foreach (array_slice($mappedLines, $mStart, $mEnd - $mStart, true) as $i => $srcLine) /* pos 39:3 */ {
- echo sprintf('%s%4d | %s', $i + 1 === $mLine ? '► ' : ' ', $i + 1, rtrim($srcLine)) /* pos 40:4 */;
+ foreach (array_slice($mappedLines, $mStart, $mEnd - $mStart, true) as $i => $srcLine) /* pos 41:3 */ {
+ echo sprintf('%s%4d | %s', $i + 1 === $mLine ? '► ' : ' ', $i + 1, rtrim($srcLine)) /* pos 42:4 */;
echo "\n";
}
@@ -61,107 +64,107 @@ Mapped source: ';
}
};
echo "\n";
-foreach (Helpers::getExceptionChain($exception) as $i => $ex) /* pos 46:1 */ {
- $title = $blueScreen->getExceptionTitle($ex) /* pos 47:2 */;
- $code = $ex->getCode() ? ' #' . $ex->getCode() : null /* pos 48:2 */;
- if ($i === 0) /* pos 49:2 */ {
+foreach (Helpers::getExceptionChain($exception) as $i => $ex) /* pos 48:1 */ {
+ $title = $blueScreen->getExceptionTitle($ex) /* pos 49:2 */;
+ $code = $ex->getCode() ? ' #' . $ex->getCode() : null /* pos 50:2 */;
+ if ($i === 0) /* pos 51:2 */ {
echo '# ';
- echo Tracy\Helpers::escapeMd($title) /* pos 50:5 */;
+ echo Tracy\Helpers::escapeMd($title) /* pos 52:5 */;
echo ': ';
- echo Tracy\Helpers::escapeMd($ex->getMessage()) /* pos 50:15 */;
- echo Tracy\Helpers::escapeMd($code) /* pos 50:34 */;
+ echo Tracy\Helpers::escapeMd($ex->getMessage()) /* pos 52:15 */;
+ echo Tracy\Helpers::escapeMd($code) /* pos 52:34 */;
echo "\n";
- } else /* pos 51:2 */ {
+ } else /* pos 53:2 */ {
echo '
## Caused by: ';
- echo Tracy\Helpers::escapeMd($title) /* pos 53:17 */;
+ echo Tracy\Helpers::escapeMd($title) /* pos 55:17 */;
echo ': ';
- echo Tracy\Helpers::escapeMd($ex->getMessage()) /* pos 53:27 */;
- echo Tracy\Helpers::escapeMd($code) /* pos 53:46 */;
+ echo Tracy\Helpers::escapeMd($ex->getMessage()) /* pos 55:27 */;
+ echo Tracy\Helpers::escapeMd($code) /* pos 55:46 */;
echo "\n";
}
echo '
in ';
- echo Tracy\Helpers::escapeMd($ex->getFile()) /* pos 56:5 */;
+ echo Tracy\Helpers::escapeMd($ex->getFile()) /* pos 58:5 */;
echo ':';
- echo Tracy\Helpers::escapeMd($ex->getLine()) /* pos 56:22 */;
+ echo Tracy\Helpers::escapeMd($ex->getLine()) /* pos 58:22 */;
echo "\n";
$_blocks['renderSource']($ex->getFile(),$ex->getLine());
echo "\n";
- $base = new \Exception /* pos 59:2 */;
- if (count(get_mangled_object_vars($ex)) > count(get_mangled_object_vars($base))) /* pos 60:2 */ {
+ $base = new \Exception /* pos 61:2 */;
+ if (count(get_mangled_object_vars($ex)) > count(get_mangled_object_vars($base))) /* pos 62:2 */ {
echo '
## Exception Properties
';
- echo Tracy\Helpers::escapeMd($agentDump($ex)) /* pos 64:3 */;
+ echo Tracy\Helpers::escapeMd($agentDump($ex)) /* pos 66:3 */;
echo "\n";
}
echo "\n";
- [$stack] = $blueScreen->prepareStack($ex) /* pos 67:2 */;
- if ($stack) /* pos 68:2 */ {
+ [$stack] = $blueScreen->prepareStack($ex) /* pos 69:2 */;
+ if ($stack) /* pos 70:2 */ {
echo '
## Stack Trace
';
- foreach ($stack as $j => $row) /* pos 72:3 */ {
- $funcName = isset($row['class']) ? $row['class'] . ($row['type'] ?? '::') . $row['function'] : $row['function'] /* pos 73:4 */;
- $location = isset($row['file']) ? $row['file'] . ':' . ($row['line'] ?? '?') : 'inner-code' /* pos 74:4 */;
- $inlineArgs = '' /* pos 75:4 */;
- $complexArgs = [] /* pos 76:4 */;
- if (!empty($row['args'])) /* pos 77:4 */ {
+ foreach ($stack as $j => $row) /* pos 74:3 */ {
+ $funcName = isset($row['class']) ? $row['class'] . ($row['type'] ?? '::') . $row['function'] : $row['function'] /* pos 75:4 */;
+ $location = isset($row['file']) ? $row['file'] . ':' . ($row['line'] ?? '?') : 'inner-code' /* pos 76:4 */;
+ $inlineArgs = '' /* pos 77:4 */;
+ $complexArgs = [] /* pos 78:4 */;
+ if (!empty($row['args'])) /* pos 79:4 */ {
try {
- $params = isset($row['class']) ? (new \ReflectionMethod($row['class'], $row['function']))->getParameters() : (new \ReflectionFunction($row['function']))->getParameters() /* pos 79:6 */;
+ $params = isset($row['class']) ? (new \ReflectionMethod($row['class'], $row['function']))->getParameters() : (new \ReflectionFunction($row['function']))->getParameters() /* pos 81:6 */;
} catch (\Throwable) {
- $params = [] /* pos 81:6 */;
+ $params = [] /* pos 83:6 */;
}
- $allSimple = true /* pos 83:5 */;
- $argParts = [] /* pos 84:5 */;
- foreach ($row['args'] as $k => $v) /* pos 85:5 */ {
- $name = isset($params[$k]) && !$params[$k]->isVariadic() ? '$' . $params[$k]->getName() : '#' . $k /* pos 86:6 */;
- $dump = rtrim($agentDump($v)) /* pos 87:6 */;
+ $allSimple = true /* pos 85:5 */;
+ $argParts = [] /* pos 86:5 */;
+ foreach ($row['args'] as $k => $v) /* pos 87:5 */ {
+ $name = isset($params[$k]) && !$params[$k]->isVariadic() ? '$' . $params[$k]->getName() : '#' . $k /* pos 88:6 */;
+ $dump = rtrim($agentDump($v)) /* pos 89:6 */;
if (str_contains($dump, '
-') || strlen($dump) > 50) /* pos 88:6 */ {
- $allSimple = false /* pos 89:7 */;
- } else /* pos 90:6 */ {
- $argParts[] = $name . ' = ' . $dump /* pos 91:7 */;
+') || strlen($dump) > 50) /* pos 90:6 */ {
+ $allSimple = false /* pos 91:7 */;
+ } else /* pos 92:6 */ {
+ $argParts[] = $name . ' = ' . $dump /* pos 93:7 */;
}
- $complexArgs[] = $name . ' = ' . $dump /* pos 93:6 */;
+ $complexArgs[] = $name . ' = ' . $dump /* pos 95:6 */;
}
- if ($allSimple) /* pos 95:5 */ {
- $inlineArgs = implode(', ', $argParts) /* pos 96:6 */;
+ if ($allSimple) /* pos 97:5 */ {
+ $inlineArgs = implode(', ', $argParts) /* pos 98:6 */;
}
}
- if ($inlineArgs) /* pos 99:4 */ {
- echo Tracy\Helpers::escapeMd($j + 1) /* pos 100:5 */;
+ if ($inlineArgs) /* pos 101:4 */ {
+ echo Tracy\Helpers::escapeMd($j + 1) /* pos 102:5 */;
echo '. ';
- echo Tracy\Helpers::escapeMd($funcName) /* pos 100:16 */;
+ echo Tracy\Helpers::escapeMd($funcName) /* pos 102:16 */;
echo '(';
- echo Tracy\Helpers::escapeMd($inlineArgs) /* pos 100:28 */;
+ echo Tracy\Helpers::escapeMd($inlineArgs) /* pos 102:28 */;
echo ')
';
- } elseif ($complexArgs) /* pos 101:4 */ {
- echo Tracy\Helpers::escapeMd($j + 1) /* pos 102:5 */;
+ } elseif ($complexArgs) /* pos 103:4 */ {
+ echo Tracy\Helpers::escapeMd($j + 1) /* pos 104:5 */;
echo '. ';
- echo Tracy\Helpers::escapeMd($funcName) /* pos 102:16 */;
+ echo Tracy\Helpers::escapeMd($funcName) /* pos 104:16 */;
echo '(';
echo Tracy\Helpers::escapeMd(implode(',
-', $complexArgs)) /* pos 102:28 */;
+', $complexArgs)) /* pos 104:28 */;
echo ')
';
- } else /* pos 103:4 */ {
- echo Tracy\Helpers::escapeMd($j + 1) /* pos 104:5 */;
+ } else /* pos 105:4 */ {
+ echo Tracy\Helpers::escapeMd($j + 1) /* pos 106:5 */;
echo '. ';
- echo Tracy\Helpers::escapeMd($funcName) /* pos 104:16 */;
+ echo Tracy\Helpers::escapeMd($funcName) /* pos 106:16 */;
echo '()
';
}
echo ' in ';
- echo Tracy\Helpers::escapeMd($location) /* pos 106:10 */;
+ echo Tracy\Helpers::escapeMd($location) /* pos 108:10 */;
echo "\n";
- if (isset($row['file'], $row['line']) && @is_file($row['file'])) /* pos 107:4 */ {
+ if (isset($row['file'], $row['line']) && @is_file($row['file'])) /* pos 109:4 */ {
$_blocks['renderSource']($row['file'],$row['line']);
}
echo "\n";
@@ -173,106 +176,106 @@ in ';
}
echo "\n";
-if ($lastError) /* pos 115:1 */ {
+if ($lastError) /* pos 117:1 */ {
echo '
## Last Muted Error
';
- echo Tracy\Helpers::escapeMd(Helpers::errorTypeToString($lastError['type'])) /* pos 119:2 */;
+ echo Tracy\Helpers::escapeMd(Helpers::errorTypeToString($lastError['type'])) /* pos 121:2 */;
echo ': ';
- echo Tracy\Helpers::escapeMd($lastError['message']) /* pos 119:51 */;
+ echo Tracy\Helpers::escapeMd($lastError['message']) /* pos 121:51 */;
echo '
in ';
- echo Tracy\Helpers::escapeMd($lastError['file']) /* pos 120:5 */;
+ echo Tracy\Helpers::escapeMd($lastError['file']) /* pos 122:5 */;
echo ':';
- echo Tracy\Helpers::escapeMd($lastError['line']) /* pos 120:24 */;
+ echo Tracy\Helpers::escapeMd($lastError['line']) /* pos 122:24 */;
echo "\n";
- if (@is_file($lastError['file'])) /* pos 121:2 */ {
+ if (@is_file($lastError['file'])) /* pos 123:2 */ {
$_blocks['renderSource']($lastError['file'],$lastError['line']);
}
}
echo "\n";
-if (!Helpers::isCli() && isset($_SERVER['REQUEST_METHOD'])) /* pos 126:1 */ {
+if (!Helpers::isCli() && isset($_SERVER['REQUEST_METHOD'])) /* pos 128:1 */ {
echo '
## HTTP Request
';
- echo Tracy\Helpers::escapeMd($_SERVER['REQUEST_METHOD']) /* pos 130:2 */;
+ echo Tracy\Helpers::escapeMd($_SERVER['REQUEST_METHOD']) /* pos 132:2 */;
echo ' ';
- echo Tracy\Helpers::escapeMd($source) /* pos 130:31 */;
+ echo Tracy\Helpers::escapeMd($source) /* pos 132:31 */;
echo "\n";
- if ($httpHeaders) /* pos 131:2 */ {
+ if ($httpHeaders) /* pos 133:2 */ {
echo '
### Headers
';
- foreach ($httpHeaders as $k => $v) /* pos 135:3 */ {
+ foreach ($httpHeaders as $k => $v) /* pos 137:3 */ {
echo '- ';
- echo Tracy\Helpers::escapeMd($k) /* pos 136:6 */;
+ echo Tracy\Helpers::escapeMd($k) /* pos 138:6 */;
echo ': ';
- echo Tracy\Helpers::escapeMd($v) /* pos 136:12 */;
+ echo Tracy\Helpers::escapeMd($v) /* pos 138:12 */;
echo "\n";
}
}
echo "\n";
- if (!empty($_GET)) /* pos 140:2 */ {
+ if (!empty($_GET)) /* pos 142:2 */ {
echo '
### $_GET
';
- echo Tracy\Helpers::escapeMd($agentDump($_GET)) /* pos 144:3 */;
+ echo Tracy\Helpers::escapeMd($agentDump($_GET)) /* pos 146:3 */;
echo "\n";
}
echo "\n";
- if (!empty($_POST)) /* pos 147:2 */ {
+ if (!empty($_POST)) /* pos 149:2 */ {
echo '
### $_POST
';
- echo Tracy\Helpers::escapeMd($agentDump($_POST)) /* pos 151:3 */;
+ echo Tracy\Helpers::escapeMd($agentDump($_POST)) /* pos 153:3 */;
echo "\n";
}
echo "\n";
- if (!empty($_COOKIE)) /* pos 154:2 */ {
+ if (!empty($_COOKIE)) /* pos 156:2 */ {
echo '
### $_COOKIE
';
- echo Tracy\Helpers::escapeMd($agentDump($_COOKIE)) /* pos 158:3 */;
+ echo Tracy\Helpers::escapeMd($agentDump($_COOKIE)) /* pos 160:3 */;
echo "\n";
}
}
echo "\n";
-if ($showEnvironment) /* pos 162:1 */ {
+if ($showEnvironment) /* pos 164:1 */ {
echo '
## Environment
| Key | Value |
|-----|-------|
| PHP | ';
- echo Tracy\Helpers::escapeMd(\PHP_VERSION . ' (' . (\PHP_ZTS ? 'TS' : 'NTS') . ')') /* pos 168:10 */;
+ echo Tracy\Helpers::escapeMd(\PHP_VERSION . ' (' . (\PHP_ZTS ? 'TS' : 'NTS') . ')') /* pos 170:10 */;
echo ' |
';
- if (isset($_SERVER['REQUEST_METHOD'])) /* pos 169:2 */ {
+ if (isset($_SERVER['REQUEST_METHOD'])) /* pos 171:2 */ {
echo '| Method | ';
- echo Tracy\Helpers::escapeMd($_SERVER['REQUEST_METHOD']) /* pos 170:14 */;
+ echo Tracy\Helpers::escapeMd($_SERVER['REQUEST_METHOD']) /* pos 172:14 */;
echo ' |
';
}
echo '| Source | ';
- echo Tracy\Helpers::escapeMd($source) /* pos 172:13 */;
+ echo Tracy\Helpers::escapeMd($source) /* pos 174:13 */;
echo ' |
';
- if (isset($_SERVER['SERVER_SOFTWARE'])) /* pos 173:2 */ {
+ if (isset($_SERVER['SERVER_SOFTWARE'])) /* pos 175:2 */ {
echo '| Server | ';
- echo Tracy\Helpers::escapeMd($_SERVER['SERVER_SOFTWARE']) /* pos 174:14 */;
+ echo Tracy\Helpers::escapeMd($_SERVER['SERVER_SOFTWARE']) /* pos 176:14 */;
echo ' |
';
}
echo '| Tracy | ';
- echo Tracy\Helpers::escapeMd(Debugger::Version) /* pos 176:12 */;
+ echo Tracy\Helpers::escapeMd(Debugger::Version) /* pos 178:12 */;
echo ' |
';
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php
index b48dbb75ec..51956ed3ab 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/Debugger.php
@@ -8,7 +8,7 @@
namespace Tracy;
use ErrorException;
-use function in_array, is_bool, is_int, is_string;
+use function dirname, function_exists, in_array, ini_get, is_bool, is_int, is_string;
use const PHP_VERSION;
@@ -17,7 +17,7 @@
*/
class Debugger
{
- public const Version = '2.12.1';
+ public const Version = '2.12.2';
/** server modes for Debugger::enable() */
public const
@@ -67,7 +67,7 @@ class Debugger
/** determines whether any error will cause immediate death in development mode; if integer that it's matched against error severity */
public static bool|int $strictMode = false;
- /** disables the @ (shut-up) operator so that notices and warnings are no longer hidden; if integer than it's matched against error severity */
+ /** disables the @ (shut-up) operator so notices and warnings are not hidden; if integer than it's matched against error severity */
public static bool|int $scream = false;
/** @var array functions that are automatically called after fatal error */
@@ -139,7 +139,7 @@ class Debugger
/** @var string[] */
public static array $customJsFiles = [];
- /** @var string[] path prefixes of "transparent" packages whose frames are skipped when locating user code and collapsed in stack traces */
+ /** @var string[] path prefixes of "transparent" packages whose frames are skipped when locating user code and collapsed in traces */
public static array $transparentPaths = [];
/** @var array<\Closure(string, int): ?array{file: string, label: string, line?: int, column?: int, active?: bool}> */
@@ -167,7 +167,7 @@ final public function __construct()
/**
* Enables displaying or logging errors and exceptions.
- * @param bool|string|string[] $mode use constant Debugger::Production, Development, Detect (autodetection) or IP address(es) whitelist.
+ * @param bool|string|string[] $mode Debugger::Production, Development, Detect (autodetection) or IP address(es) whitelist
* @param ?string $logDirectory error log directory
* @param string|string[]|null $email administrator email; enables email sending in production mode
*/
@@ -280,7 +280,7 @@ public static function isEnabled(): bool
public static function shutdownHandler(): void
{
$error = error_get_last();
- if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], strict: true)) {
+ if (in_array($error['type'] ?? null, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR], true)) {
$e = new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']);
if (!empty($error['trace'])) {
(new \ReflectionClass(\Exception::class))->getProperty('trace')->setValue($e, $error['trace']);
@@ -314,7 +314,7 @@ public static function exceptionHandler(\Throwable $exception): void
self::$reserved = null;
self::$obStatus = ob_get_status(full_status: true);
- @http_response_code(500);
+ @http_response_code(500); // @ headers may already be sent
Helpers::improveException($exception);
self::removeOutputBuffers(errorOccurred: true);
@@ -368,7 +368,7 @@ public static function removeOutputBuffers(bool $errorOccurred): void
{
while (ob_get_level() > self::$obLevel) {
$status = ob_get_status();
- if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], strict: true)) {
+ if (in_array($status['name'], ['ob_gzhandler', 'zlib output compression'], true)) {
break;
}
@@ -458,9 +458,9 @@ public static function setSessionStorage(SessionStorage $storage): void
public static function getSessionStorage(): SessionStorage
{
if (empty(self::$sessionStorage)) {
- self::$sessionStorage = @is_dir($dir = (string) session_save_path())
- || @is_dir($dir = (string) ini_get('upload_tmp_dir'))
- || @is_dir($dir = sys_get_temp_dir())
+ self::$sessionStorage = @is_dir($dir = (string) session_save_path()) // @ the path may be too long or outside open_basedir
+ || @is_dir($dir = (string) ini_get('upload_tmp_dir')) // @ the path may be too long or outside open_basedir
+ || @is_dir($dir = sys_get_temp_dir()) // @ the path may be too long or outside open_basedir
|| ($dir = (string) self::$logDirectory)
? new FileSession($dir)
: new NativeSession;
@@ -632,7 +632,7 @@ public static function detectDebugMode(string|array|null $list = null): bool
$list[] = '[::1]'; // workaround for PHP < 7.3.4
}
- return in_array($addr, $list, strict: true) || in_array("$secret@$addr", $list, strict: true);
+ return in_array($addr, $list, true) || in_array("$secret@$addr", $list, true);
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php
index cb40be1982..08c3f5fa9b 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/DeferredContent.php
@@ -7,7 +7,7 @@
namespace Tracy;
-use function array_slice, is_string, strlen;
+use function is_string, strlen;
/**
@@ -122,7 +122,7 @@ public function sendAssets(): bool
private function buildJsCss(): string
{
- $css = array_map(file_get_contents(...), array_merge([
+ $css = array_map(file_get_contents(...), [
__DIR__ . '/../assets/reset.css',
__DIR__ . '/../Bar/assets/bar.css',
__DIR__ . '/../assets/toggle.css',
@@ -131,7 +131,8 @@ private function buildJsCss(): string
__DIR__ . '/../Dumper/assets/dumper-light.css',
__DIR__ . '/../Dumper/assets/dumper-dark.css',
__DIR__ . '/../BlueScreen/assets/bluescreen.css',
- ], Debugger::$customCssFiles));
+ ...Debugger::$customCssFiles,
+ ]);
$js1 = array_map(fn($file) => '(function() {' . file_get_contents($file) . '})();', [
__DIR__ . '/../Bar/assets/bar.js',
diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php
index 8f81711fab..689f16a2d7 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/DevelopmentStrategy.php
@@ -8,7 +8,7 @@
namespace Tracy;
use ErrorException;
-use function is_bool;
+use function function_exists, is_bool;
/**
@@ -72,7 +72,7 @@ private function renderExceptionCli(\Throwable $exception): void
return;
}
- if (Helpers::detectColors() && @is_file($exception->getFile())) {
+ if (Helpers::detectColors() && @is_file($exception->getFile())) { // @ the path may be too long or outside open_basedir
echo "\n\n" . CodeHighlighter::highlightPhpCli((string) file_get_contents($exception->getFile()), $exception->getLine()) . "\n";
}
@@ -99,7 +99,7 @@ public function handleError(
&& !isset($_GET['_tracy_skip_error'])
) {
$e = new ErrorException($message, 0, $severity, $file, $line);
- @$e->skippable = true; // dynamic properties are deprecated since PHP 8.2
+ @$e->skippable = true; // @ dynamic properties are deprecated
Debugger::exceptionHandler($e);
exit(255);
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Debugger/ProductionStrategy.php b/core/vendor/tracy/tracy/src/Tracy/Debugger/ProductionStrategy.php
index ed760e00a0..32b01d9887 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Debugger/ProductionStrategy.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Debugger/ProductionStrategy.php
@@ -8,8 +8,7 @@
namespace Tracy;
use ErrorException;
-use function is_resource;
-use const STDERR;
+use function function_exists, ini_get, is_resource;
/**
diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php
index 76a938c36d..b3f5436f26 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Describer.php
@@ -8,7 +8,7 @@
namespace Tracy\Dumper;
use Tracy\Helpers;
-use function array_map, array_slice, class_exists, count, file, get_debug_type, get_resource_type, htmlspecialchars, implode, interface_exists, is_array, is_bool, is_float, is_int, is_object, is_resource, is_string, is_subclass_of, json_encode, method_exists, preg_match, spl_object_id, str_replace, strlen, strpos, strtolower, trim, uksort;
+use function count, is_array, is_bool, is_float, is_int, is_object, is_resource, is_string, strlen;
/**
@@ -55,7 +55,7 @@ public function describe(mixed $var): \stdClass
{
// exposers are sorted from the most specific type to the most general; '' acts as the universal supertype
$isSubtypeOf = fn(string $type, string $parent): bool => $parent === ''
- || ((class_exists($type, autoload: false) || interface_exists($type, autoload: false)) && is_subclass_of($type, $parent));
+ || ((class_exists($type, false) || interface_exists($type, autoload: false)) && is_subclass_of($type, $parent));
uksort($this->objectExposers, fn($a, $b): int => $isSubtypeOf($b, $a) <=> $isSubtypeOf($a, $b));
try {
@@ -202,7 +202,14 @@ private function describeObject(object $obj, int $depth = 0): Value
$described = $this->isSensitive((string) $k, $v, get_debug_type($obj)) // props come from user callbacks (__debugInfo, custom exposers)
? new Value(Value::TypeText, self::hideValue($v))
: null;
- $this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props ?? [], $k), described: $described);
+ $this->addPropertyTo(
+ $value,
+ (string) $k,
+ $v,
+ Value::PropertyVirtual,
+ $this->getReferenceId($props ?? [], $k),
+ described: $described,
+ );
}
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php
index 552967c9a4..971859bf40 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Dumper.php
@@ -9,12 +9,8 @@
use Dom;
use Ds;
-use Tracy\Dumper\Describer;
-use Tracy\Dumper\Exposer;
-use Tracy\Dumper\Renderer;
+use Tracy\Dumper\{Describer, Exposer, Renderer};
use Uri;
-use function array_flip, array_map, file_get_contents, fwrite, str_replace;
-use const STDOUT;
/**
@@ -105,8 +101,8 @@ class Dumper
/** @var array */
private static array $enumProperties = [];
- private Describer $describer;
- private Renderer $renderer;
+ private readonly Describer $describer;
+ private readonly Renderer $renderer;
/**
diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php
index 87106d7f35..abc5f5a1ff 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Exposer.php
@@ -10,7 +10,7 @@
use Dom;
use Ds;
use Uri;
-use function array_diff_key, array_key_exists, count, end, explode, get_debug_type, get_mangled_object_vars, implode, iterator_to_array, sort;
+use function array_key_exists, count;
use const PHP_VERSION_ID;
diff --git a/core/vendor/tracy/tracy/src/Tracy/Dumper/Renderer.php b/core/vendor/tracy/tracy/src/Tracy/Dumper/Renderer.php
index e849c6f79f..d713159949 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Dumper/Renderer.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Dumper/Renderer.php
@@ -8,7 +8,7 @@
namespace Tracy\Dumper;
use Tracy\Helpers;
-use function count, htmlspecialchars, is_array, is_bool, is_float, is_int, is_object, is_string, str_repeat, str_replace, strlen, strrpos, substr, substr_count;
+use function count, is_array, is_bool, is_float, is_int, is_object, is_string, strlen;
/**
@@ -53,7 +53,7 @@ public function renderAsHtml(\stdClass $model): string
$html = $this->renderVar($value);
$json = $snapshot = null;
- } elseif ($this->lazy && (is_array($value) && $value || is_object($value))) { // full lazy-loading
+ } elseif ($this->lazy && ((is_array($value) && $value) || is_object($value))) { // full lazy-loading
$html = '';
$snapshot = $this->collectingMode ? null : $this->snapshot;
$json = $value;
@@ -82,10 +82,10 @@ public function renderAsHtml(\stdClass $model): string
}
return ' 100 ? "\n" : '')
+ . ($json && $this->collapseTop === true ? ' tracy-collapsed' : '') . '"'
+ . ($snapshot !== null ? " data-tracy-snapshot='" . Helpers::jsonEncode($snapshot) . "'" : '')
+ . ($json ? " data-tracy-dump='" . Helpers::jsonEncode($json) . "'" : '')
+ . ($location || strlen($html) > 100 ? "\n" : '')
. '>'
. $location
. $html
@@ -231,7 +231,11 @@ private function renderArray(array|Value $array, int $depth): string
if ($this->lazy !== false) {
$ref = new Value(Value::TypeRef, $array->id);
$this->copySnapshot($ref);
- return '" . $out . ' ';
+ return '"
+ . $out
+ . ' ';
} elseif ($this->hash) {
return $out . (isset($this->above[$array->id]) ? ' see above ' : ' see below ');
@@ -314,7 +318,11 @@ private function renderObject(Value $object, int $depth): string
if ($this->lazy !== false) {
$ref = new Value(Value::TypeRef, $object->id);
$this->copySnapshot($ref);
- return '" . $out . ' ';
+ return '"
+ . $out
+ . ' ';
} elseif ($this->hash) {
return $out . (isset($this->above[$object->id]) ? ' see above ' : ' see below ');
@@ -368,7 +376,11 @@ private function renderResource(Value $resource, int $depth): string
if ($this->lazy !== false) {
$ref = new Value(Value::TypeRef, $resource->id);
$this->copySnapshot($ref);
- return '" . $out . ' ';
+ return '"
+ . $out
+ . ' ';
}
return $out . ' see above ';
diff --git a/core/vendor/tracy/tracy/src/Tracy/Helpers.php b/core/vendor/tracy/tracy/src/Tracy/Helpers.php
index 96258dd709..5c7a8da1b3 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Helpers.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Helpers.php
@@ -7,8 +7,8 @@
namespace Tracy;
-use function array_filter, array_map, array_merge, array_pop, array_slice, array_unique, basename, bin2hex, class_exists, constant, count, dechex, defined, dirname, end, escapeshellarg, explode, extension_loaded, func_get_args, function_exists, get_class_methods, get_declared_classes, get_defined_functions, getenv, getmypid, headers_list, htmlspecialchars, htmlspecialchars_decode, iconv_strlen, implode, in_array, ini_set, is_a, is_array, is_callable, is_file, is_object, is_string, json_encode, levenshtein, ltrim, mb_strlen, mb_substr, method_exists, ob_end_clean, ob_get_clean, ob_start, ord, preg_match, preg_replace, preg_replace_callback, random_bytes, rawurlencode, rtrim, sapi_windows_vt100_support, spl_object_id, str_contains, str_pad, str_replace, strcasecmp, stream_isatty, strip_tags, strlen, strtoupper, strtr, substr, trait_exists, utf8_decode;
-use const DIRECTORY_SEPARATOR, ENT_HTML5, ENT_QUOTES, ENT_SUBSTITUTE, JSON_HEX_AMP, JSON_HEX_APOS, JSON_HEX_TAG, JSON_INVALID_UTF8_SUBSTITUTE, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE, PHP_EOL, PHP_SAPI, STDOUT, STR_PAD_LEFT;
+use function count, defined, extension_loaded, func_get_args, function_exists, is_array, is_object, is_string, strlen;
+use const ENT_HTML5, ENT_QUOTES, ENT_SUBSTITUTE, PHP_SAPI;
/**
@@ -40,6 +40,7 @@ public static function editorLink(string $file, ?int $line = null): string
basename($file),
$line ? ":$line" : '',
);
+
} else {
return self::formatHtml('% ', $file . ($line ? ":$line" : ''));
}
@@ -141,7 +142,10 @@ public static function countTransparentFrames(array $trace, ?array $paths = null
$next = $trace[$key + 1] ?? null;
$nextReflection = match (true) {
$next === null => null,
- isset($next['class'], $next['function']) && method_exists($next['class'], $next['function']) => new \ReflectionMethod($next['class'], $next['function']),
+ isset($next['class'], $next['function']) && method_exists($next['class'], $next['function']) => new \ReflectionMethod(
+ $next['class'],
+ $next['function'],
+ ),
isset($next['function']) && function_exists($next['function']) => new \ReflectionFunction($next['function']),
default => null,
};
@@ -256,15 +260,19 @@ public static function improveException(\Throwable $e): void
} elseif (is_array($arg) && !method_exists($arg[0], $arg[1])) {
$hint = self::getSuggestion(get_class_methods($arg[0]) ?: [], $arg[1]);
$class = is_object($arg[0]) ? $arg[0]::class : $arg[0];
- $message = str_replace($m[2], "but method $class::$arg[1]() does not exist" . ($hint ? " (did you mean $hint?)" : ''), $message);
+ $message = str_replace(
+ $m[2],
+ "but method $class::$arg[1]() does not exist" . ($hint ? " (did you mean $hint?)" : ''),
+ $message,
+ );
} elseif (is_string($arg) && !function_exists($arg)) {
- $funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']);
+ $funcs = [...get_defined_functions()['internal'], ...get_defined_functions()['user']];
$hint = self::getSuggestion($funcs, $arg);
$message = str_replace($m[2], "but function '$arg' does not exist" . ($hint ? " (did you mean $hint?)" : ''), $message);
}
} elseif (preg_match('#^Call to undefined function (\S+\\\)?(\w+)\(#', $message, $m)) {
- $funcs = array_merge(get_defined_functions()['internal'], get_defined_functions()['user']);
+ $funcs = [...get_defined_functions()['internal'], ...get_defined_functions()['user']];
if ($hint = self::getSuggestion($funcs, $m[1] . $m[2]) ?: self::getSuggestion($funcs, $m[2])) {
$message = "Call to undefined function $m[2](), did you mean $hint()?";
$replace = ["$m[2](", "$hint("];
@@ -303,7 +311,7 @@ public static function improveException(\Throwable $e): void
if (isset($replace)) {
$loc = Debugger::mapSource($e->getFile(), $e->getLine()) ?? ['file' => $e->getFile(), 'line' => $e->getLine()];
- @$e->tracyAction = [ // dynamic properties are deprecated since PHP 8.2
+ @$e->tracyAction = [ // @ dynamic properties are deprecated
'link' => self::editorUri($loc['file'], $loc['line'], 'fix', $replace[0], $replace[1]),
'label' => 'fix it',
];
@@ -341,7 +349,7 @@ public static function guessClassFile(string $class): ?string
if ($i > $max && $i < count($segments) && ($file = (new \ReflectionClass($class))->getFileName())) {
$max = $i;
- $res = array_merge(array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, $i - count($parts)), array_slice($segments, $i));
+ $res = [...array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, $i - count($parts)), ...array_slice($segments, $i)];
$res = implode(DIRECTORY_SEPARATOR, $res) . '.php';
}
}
@@ -359,7 +367,10 @@ public static function getSuggestion(array $items, string $value): ?string
{
$best = null;
$min = (strlen($value) / 4 + 1) * 10 + .1;
- $items = array_map(fn($item) => $item instanceof \ReflectionMethod || $item instanceof \ReflectionProperty ? $item->getName() : (string) $item, $items);
+ $items = array_map(
+ fn($item) => $item instanceof \ReflectionMethod || $item instanceof \ReflectionProperty ? $item->getName() : (string) $item,
+ $items,
+ );
foreach (array_unique($items) as $item) {
if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
$min = $len;
@@ -421,7 +432,11 @@ public static function isCli(): bool
/** @internal */
public static function getNonce(bool $attr = false): ?string
{
- return preg_match('#^Content-Security-Policy(?:-Report-Only)?:.*\sscript-src(?:-elem)?\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi', implode("\n", headers_list()), $m)
+ return preg_match(
+ '#^Content-Security-Policy(?:-Report-Only)?:.*\sscript-src(?:-elem)?\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi',
+ implode("\n", headers_list()),
+ $m,
+ )
? ($attr ? ' nonce="' . self::escapeHtml($m[1]) . '"' : $m[1])
: null;
}
@@ -496,8 +511,8 @@ private static function doEncodeString(string $s, bool $utf8, bool $showWhitespa
$s = preg_replace_callback(
$utf8 ? '#[\p{C}<&]#u' : '#[\x00-\x1F\x7F-\xFF<&]#',
fn($m) => $special[$m[0]] ?? (strlen($m[0]) === 1
- ? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) . ' '
- : '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '} '),
+ ? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) . ' '
+ : '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '} '),
$s,
);
$s = str_replace('', '', $s);
@@ -527,7 +542,7 @@ public static function utf8Length(string $s): int
return match (true) {
extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'),
extension_loaded('iconv') => iconv_strlen($s, 'UTF-8') ?: strlen($s),
- default => strlen(@utf8_decode($s)), // deprecated
+ default => strlen(@utf8_decode($s)), // @ utf8_decode() is deprecated
};
}
@@ -682,7 +697,7 @@ public static function detectColors(): bool
public static function getExceptionChain(\Throwable $ex): array
{
$res = [$ex];
- while (($ex = $ex->getPrevious()) && !in_array($ex, $res, strict: true)) {
+ while (($ex = $ex->getPrevious()) && !in_array($ex, $res, true)) {
$res[] = $ex;
}
@@ -759,7 +774,15 @@ public static function jsonEncode(mixed $value, bool $inScript = false): string
{
$old = @ini_set('serialize_precision', '-1'); // @ may be disabled
try {
- return json_encode($value, ($inScript ? JSON_HEX_TAG : 0) | JSON_HEX_APOS | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
+ return json_encode(
+ $value,
+ ($inScript ? JSON_HEX_TAG : 0)
+ | JSON_HEX_APOS
+ | JSON_HEX_AMP
+ | JSON_UNESCAPED_UNICODE
+ | JSON_UNESCAPED_SLASHES
+ | JSON_INVALID_UTF8_SUBSTITUTE,
+ );
} finally {
if ($old !== false) {
ini_set('serialize_precision', $old);
diff --git a/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php b/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php
index 39b5d458cb..c77d9f7039 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Logger/Logger.php
@@ -7,8 +7,8 @@
namespace Tracy;
-use function in_array, is_string;
-use const DIRECTORY_SEPARATOR, FILE_APPEND, LOCK_EX, LOCK_UN, PHP_EOL;
+use function is_string;
+use const DIRECTORY_SEPARATOR, FILE_APPEND, LOCK_EX;
/**
@@ -74,7 +74,7 @@ public function log(mixed $message, string $level = self::INFO)
$this->logException($message, $exceptionFile);
}
- if (in_array($level, [self::ERROR, self::EXCEPTION, self::CRITICAL], strict: true)) {
+ if (in_array($level, [self::ERROR, self::EXCEPTION, self::CRITICAL], true)) {
$this->sendEmail($message);
}
@@ -92,7 +92,10 @@ public static function formatMessage($message): string
foreach (Helpers::getExceptionChain($message) as $exception) {
$tmp[] = ($exception instanceof \ErrorException
? Helpers::errorTypeToString($exception->getSeverity()) . ': ' . $exception->getMessage()
- : get_debug_type($exception) . ': ' . $exception->getMessage() . ($exception->getCode() ? ' #' . $exception->getCode() : '')
+ : get_debug_type($exception)
+ . ': '
+ . $exception->getMessage()
+ . ($exception->getCode() ? ' #' . $exception->getCode() : '')
) . ' in ' . $exception->getFile() . ':' . $exception->getLine();
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/OutputDebugger/OutputDebugger.php b/core/vendor/tracy/tracy/src/Tracy/OutputDebugger/OutputDebugger.php
index 87b21605e3..a4841ac46d 100644
--- a/core/vendor/tracy/tracy/src/Tracy/OutputDebugger/OutputDebugger.php
+++ b/core/vendor/tracy/tracy/src/Tracy/OutputDebugger/OutputDebugger.php
@@ -7,8 +7,7 @@
namespace Tracy;
-use function array_slice, count;
-use const PHP_OUTPUT_HANDLER_FINAL;
+use function count;
/**
diff --git a/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php b/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php
index 2c13182f48..f778f6df2a 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Session/FileSession.php
@@ -8,7 +8,6 @@
namespace Tracy;
use function is_string;
-use const LOCK_EX, LOCK_UN;
/**
@@ -53,7 +52,7 @@ private function open(): void
if (
!is_string($id)
|| !preg_match('#^\w{10}\z#i', $id)
- || !($file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'r+')) // intentionally @
+ || !($file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'r+')) // @ file may not exist
) {
$id = Helpers::createId();
setcookie($this->cookieName, $id, [
@@ -64,7 +63,7 @@ private function open(): void
'samesite' => 'Lax',
]);
- $file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'c+'); // intentionally @
+ $file = @fopen($path = $this->dir . '/' . self::FilePrefix . $id, 'c+'); // @ is escalated to exception
if ($file === false) {
throw new \RuntimeException("Unable to create file '$path'. " . (error_get_last()['message'] ?? ''));
}
@@ -72,7 +71,7 @@ private function open(): void
@chmod($path, 0o600); // @ - may not be supported by the filesystem
}
- if (!@flock($file, LOCK_EX)) { // intentionally @
+ if (!@flock($file, LOCK_EX)) { // @ is escalated to exception
throw new \RuntimeException("Unable to acquire exclusive lock on '$path'. " . (error_get_last()['message'] ?? ''));
}
diff --git a/core/vendor/tracy/tracy/src/Tracy/Session/NativeSession.php b/core/vendor/tracy/tracy/src/Tracy/Session/NativeSession.php
index b662ed2e82..9020ae2c24 100644
--- a/core/vendor/tracy/tracy/src/Tracy/Session/NativeSession.php
+++ b/core/vendor/tracy/tracy/src/Tracy/Session/NativeSession.php
@@ -7,8 +7,6 @@
namespace Tracy;
-use const PHP_SESSION_ACTIVE;
-
/**
* Session storage backed by native PHP session ($_SESSION).
diff --git a/core/vendor/tracy/tracy/tools/create-phar/create-phar.php b/core/vendor/tracy/tracy/tools/create-phar/create-phar.php
index 8905778ccf..e08e291436 100644
--- a/core/vendor/tracy/tracy/tools/create-phar/create-phar.php
+++ b/core/vendor/tracy/tracy/tools/create-phar/create-phar.php
@@ -16,7 +16,10 @@
");
$phar->startBuffering();
-foreach ($iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/../../src', RecursiveDirectoryIterator::SKIP_DOTS)) as $file) {
+foreach ($iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
+ __DIR__ . '/../../src',
+ RecursiveDirectoryIterator::SKIP_DOTS,
+)) as $file) {
if (!in_array($file->getExtension(), ['php', 'phtml', 'js', 'css'], true)) {
continue;
}
diff --git a/core/vendor/vlucas/phpdotenv/composer.json b/core/vendor/vlucas/phpdotenv/composer.json
index 60aa7a58fd..db6e40b9e8 100644
--- a/core/vendor/vlucas/phpdotenv/composer.json
+++ b/core/vendor/vlucas/phpdotenv/composer.json
@@ -54,7 +54,7 @@
"forward-command": false
},
"branch-alias": {
- "dev-master": "5.6-dev"
+ "dev-master": "5.8-dev"
}
}
}
diff --git a/core/vendor/vlucas/phpdotenv/src/Parser/Lines.php b/core/vendor/vlucas/phpdotenv/src/Parser/Lines.php
index de290c5a4b..e2a212892e 100644
--- a/core/vendor/vlucas/phpdotenv/src/Parser/Lines.php
+++ b/core/vendor/vlucas/phpdotenv/src/Parser/Lines.php
@@ -43,6 +43,11 @@ public static function process(array $lines)
}
}
+ // Emit an unterminated multiline value so the parser reports the missing quote.
+ if ($multiline) {
+ $output[] = \implode("\n", $multilineBuffer);
+ }
+
return $output;
}
@@ -111,11 +116,7 @@ private static function looksLikeMultilineStart(string $line)
*/
private static function looksLikeMultilineStop(string $line, bool $started)
{
- if ($line === '"') {
- return true;
- }
-
- return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) {
+ return Regex::occurrences('/(?map(static function (int $count) use ($started) {
return $started ? $count > 1 : $count >= 1;
})->success()->getOrElse(false);
}
diff --git a/manager/actions/files.dynamic.php b/manager/actions/files.dynamic.php
index 537a330915..e9b2614711 100755
--- a/manager/actions/files.dynamic.php
+++ b/manager/actions/files.dynamic.php
@@ -7,6 +7,7 @@
}
$token_check = checkToken();
$newToken = makeToken();
+$tokenRefusedMessage = '' . e(\EvolutionCMS\Support\CsrfMessage::text()) . ' ';
// settings
$theme_image_path = EVO_MANAGER_URL . 'media/style/' . evo()->getConfig('manager_theme') . '/images/';
$excludes = [
@@ -157,11 +158,11 @@ function fileManagerDeleteDirectoryZip(array $zipPaths): void
fileManagerDeleteDirectoryZip($directoryZipPaths);
$directoryZipMessage = '' . $_lang['files_zip_deleted'] . ' ';
} else {
- $directoryZipMessage = 'Invalid token ';
+ $directoryZipMessage = $tokenRefusedMessage;
}
} elseif (get_by_key($_REQUEST, 'mode') == 'downloadzip') {
if (!$token_check) {
- $directoryZipMessage = 'Invalid token ';
+ $directoryZipMessage = $tokenRefusedMessage;
} elseif ($directoryZipBlocked) {
$directoryZipMessage = '' . $_lang['files_zip_unavailable'] . ' ';
} elseif (fileManagerDirectoryZipExists($directoryZipPaths)) {
@@ -333,19 +334,19 @@ function renameFile(file) {
if ($token_check) {
$information = fileupload();
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
} elseif (get_by_key($_POST, 'mode') == 'save') {
if ($token_check) {
echo textsave();
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
} elseif (get_by_key($_REQUEST, 'mode') == 'delete') {
if ($token_check) {
echo delete_file();
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
} elseif (get_by_key($_POST, 'mode') == 'savegroups') {
if ($token_check) {
@@ -421,7 +422,7 @@ function renameFile(file) {
}
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
echo $directoryZipMessage;
@@ -521,7 +522,7 @@ function renameFile(file) {
}
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// End Unzip - Raymond
@@ -549,7 +550,7 @@ function renameFile(file) {
\EvolutionCMS\Support\FileManagerAccess::forgetRestrictions(fileManagerAclKey($folderTarget['relative']));
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Create folder here
@@ -572,7 +573,7 @@ function renameFile(file) {
}
umask($old_umask);
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Create file here
@@ -595,7 +596,7 @@ function renameFile(file) {
umask($old_umask);
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Duplicate file here
@@ -634,7 +635,7 @@ function renameFile(file) {
}
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Copy folder here, beside the original
@@ -715,7 +716,7 @@ function renameFile(file) {
}
umask($old_umask);
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Delete a link here: the link itself, never what it leads to
@@ -741,7 +742,7 @@ function renameFile(file) {
echo '' . $_lang['files_link_deleted'] . ' ';
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Rename folder here
@@ -782,7 +783,7 @@ function renameFile(file) {
umask($old_umask);
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
// Rename file here
@@ -820,7 +821,7 @@ function renameFile(file) {
}
}
} else {
- echo 'Invalid token ';
+ echo $tokenRefusedMessage;
}
}
} // End New Folder - Raymond
diff --git a/manager/index.php b/manager/index.php
index 79169f1bac..b032b2ca25 100755
--- a/manager/index.php
+++ b/manager/index.php
@@ -116,6 +116,8 @@
header('Pragma: no-cache');
header('X-UA-Compatible: IE=edge;FF=3;OtherUA=4');
header('X-XSS-Protection: 0');
+// The manager's own POSTs need a Referer (validate_referer), whatever policy a proxy sets; other sites get none
+header('Referrer-Policy: same-origin');
$php_ver_comp = version_compare(phpversion(), "8.3");
// -1 if left is less, 0 if equal, +1 if left is higher
diff --git a/manager/media/browser/mcpuk/browse.php b/manager/media/browser/mcpuk/browse.php
index 14a8b90962..ff7d19d1b1 100755
--- a/manager/media/browser/mcpuk/browse.php
+++ b/manager/media/browser/mcpuk/browse.php
@@ -49,7 +49,7 @@ function returnNoPermissionsMessage($role) {
if ($token === '' || !hash_equals(csrf_token(), $token)) {
header('HTTP/1.1 403 Forbidden');
header('Content-Type: text/plain; charset=utf-8');
- die(json_encode(['error' => 'Invalid CSRF token.']));
+ die(json_encode(['error' => \EvolutionCMS\Support\CsrfMessage::text()], JSON_UNESCAPED_UNICODE));
}
}
diff --git a/manager/media/browser/mcpuk/tpl/tpl_browser.php b/manager/media/browser/mcpuk/tpl/tpl_browser.php
index b6810b692e..4660b0afad 100755
--- a/manager/media/browser/mcpuk/tpl/tpl_browser.php
+++ b/manager/media/browser/mcpuk/tpl/tpl_browser.php
@@ -5,6 +5,7 @@
KCFinder: /
session['dir'] ?>
+
diff --git a/manager/media/script/tests/document-save-helper.test.js b/manager/media/script/tests/document-save-helper.test.js
index 643f0f241c..cde0bbe487 100644
--- a/manager/media/script/tests/document-save-helper.test.js
+++ b/manager/media/script/tests/document-save-helper.test.js
@@ -52,8 +52,8 @@ test('parseResponse tells a save apart from a refusal and from a non-JSON answer
const denied = helper.parseResponse(422, '{"success":false,"message":"duplicate alias"}');
assert.deepEqual(denied, { ok: false, message: 'duplicate alias', unconfirmed: false });
- const csrf = helper.parseResponse(403, '{"error":"CSRF token mismatch","code":"csrf_token_mismatch"}');
- assert.deepEqual(csrf, { ok: false, message: '', unconfirmed: false });
+ const csrf = helper.parseResponse(403, '{"error":"CSRF token mismatch","code":"csrf_token_mismatch","message":"Reload the page"}');
+ assert.deepEqual(csrf, { ok: false, message: 'Reload the page', unconfirmed: false });
// the login page, a plugin that exited, a fatal error: the save may have run and is never replayed
assert.deepEqual(helper.parseResponse(200, 'login'), { ok: false, message: '', unconfirmed: true });
diff --git a/manager/media/style/common/login.tpl b/manager/media/style/common/login.tpl
index 71e99bd584..27da22f2d2 100755
--- a/manager/media/style/common/login.tpl
+++ b/manager/media/style/common/login.tpl
@@ -3,6 +3,7 @@
[+site_name_text+] (Evolution CMS Manager Login)
+
@@ -91,6 +92,23 @@
window.location = self.document.location;
}
var form = document.loginfrm;
+ // A refusal can come back as JSON (CSRF checks) or as a whole page; alert only its text.
+ function loginResponseMessage(xhr) {
+ var text = String(xhr.responseText || '');
+ try {
+ var body = JSON.parse(text);
+ if (body && typeof body.message === 'string' && body.message !== '') {
+ return body.message;
+ }
+ } catch (err) {}
+ if (/^\s*
[+site_name_text+] (Evolution CMS Content Manager)
+