Bump ImapEngine from 1.25.1 to 1.25.2

This commit is contained in:
johnnyq
2026-07-18 11:17:53 -04:00
parent 9cc7e5ff3c
commit 1a3d7a1e0d
32 changed files with 2375 additions and 269 deletions

View File

@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.13.0 - 2026-07-16
### Added
- Add `Utils::` `asciiToLower`, `asciiToUpper`, `asciiUcFirst`, `caselessEquals`, `caselessContains`
### Changed
- Use locale-independent ASCII case folding everywhere case is normalized
- Trigger a runtime deprecation for previously deprecated functionality in 2.3.0
## 2.12.5 - 2026-07-13
### Fixed
- Compare header names and hosts with locale-independent ASCII lowercasing
- Compare hosts without locale sensitivity when detecting cross-origin redirects
## 2.12.4 - 2026-07-08
### Changed
- Pass explicit trim characters ahead of the PHP 8.6 trim default change
### Fixed
- Anchor server port and response start-line patterns to the true end of input
- Treat host-less origin-form request targets starting with `//` as paths in `Message::parseRequest()`
- Reject raw DEL bytes in bracketed IP-literal hosts instead of parsing a mutated host
- Reject invalid bytes after a bracketed IP-literal host instead of reparsing a different host
## 2.12.3 - 2026-06-23
### Security

View File

@@ -455,6 +455,56 @@ string. This function does not modify the provided keys when an array is
encountered (like `http_build_query()` would).
## `GuzzleHttp\Psr7\Utils::asciiToLower`
`public static function asciiToLower(string $string): string`
Converts ASCII uppercase letters in a string to lowercase.
Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::asciiToUpper`
`public static function asciiToUpper(string $string): string`
Converts ASCII lowercase letters in a string to uppercase.
Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::asciiUcFirst`
`public static function asciiUcFirst(string $string): string`
Converts the first character of a string to uppercase when it is an ASCII
lowercase letter.
Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion is
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
elements require.
## `GuzzleHttp\Psr7\Utils::caselessContains`
`public static function caselessContains(string $haystack, string $needle): bool`
Checks whether the haystack contains the needle, comparing ASCII letters
case-insensitively and without locale sensitivity.
## `GuzzleHttp\Psr7\Utils::caselessEquals`
`public static function caselessEquals(string $left, string $right): bool`
Checks whether two strings are equal, comparing ASCII letters
case-insensitively and without locale sensitivity.
## `GuzzleHttp\Psr7\Utils::caselessRemove`
`public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`

View File

@@ -95,6 +95,8 @@ final class Header
*/
public static function normalize($header): array
{
\trigger_deprecation('guzzlehttp/psr7', '2.3', 'Header::normalize() is deprecated and will be removed in guzzlehttp/psr7 3.0. Use Header::splitList() instead.');
$result = [];
foreach ((array) $header as $value) {
foreach (self::splitList($value) as $parsed) {
@@ -142,7 +144,7 @@ final class Header
}
if (!$isQuoted && $value[$i] === ',') {
$v = \trim($v);
$v = \trim($v, " \n\r\t\0\x0B");
if ($v !== '') {
$result[] = $v;
}
@@ -167,7 +169,7 @@ final class Header
$v .= $value[$i];
}
$v = \trim($v);
$v = \trim($v, " \n\r\t\0\x0B");
if ($v !== '') {
$result[] = $v;
}

View File

@@ -19,7 +19,7 @@ final class Message
{
if ($message instanceof RequestInterface) {
$msg = trim($message->getMethod().' '
.$message->getRequestTarget())
.$message->getRequestTarget(), " \n\r\t\0\x0B")
.' HTTP/'.$message->getProtocolVersion();
if (!$message->hasHeader('host')) {
$msg .= "\r\nHost: ".$message->getUri()->getHost();
@@ -33,7 +33,7 @@ final class Message
}
foreach ($message->getHeaders() as $name => $values) {
if (is_string($name) && strtolower($name) === 'set-cookie') {
if (is_string($name) && Utils::asciiToLower($name) === 'set-cookie') {
foreach ($values as $value) {
$msg .= "\r\n{$name}: ".$value;
}
@@ -265,8 +265,10 @@ final class Message
$host = self::getHostFromHeaders($headers);
// If no host is found, then a full URI cannot be constructed.
// Collapse leading slashes so an origin-form target cannot be
// parsed as a network-path reference with its own authority.
if ($host === null) {
return $path;
return self::normalizePathForOriginForm($path);
}
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
@@ -274,6 +276,15 @@ final class Message
return $scheme.'://'.$host.'/'.ltrim($path, '/');
}
private static function normalizePathForOriginForm(string $path): string
{
if (0 === strpos($path, '//')) {
return '/'.ltrim($path, '/');
}
return $path;
}
/**
* @param array $headers Array of headers (each value an array).
*/
@@ -283,7 +294,7 @@ final class Message
// Numeric array keys are converted to int by PHP.
$k = (string) $k;
return strtolower($k) === 'host';
return Utils::asciiToLower($k) === 'host';
});
if (!$hostKey) {
@@ -349,7 +360,7 @@ final class Message
// According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
// the space between status-code and reason-phrase is required. But
// browsers accept responses without space and reason as well.
$responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line']);
$responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/D', $data['start-line']);
if ($responseStartLineMatch === false) {
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());

View File

@@ -62,12 +62,12 @@ trait MessageTrait
public function hasHeader($header): bool
{
return isset($this->headerNames[strtolower($header)]);
return isset($this->headerNames[Utils::asciiToLower($header)]);
}
public function getHeader($header): array
{
$header = strtolower($header);
$header = Utils::asciiToLower($header);
if (!isset($this->headerNames[$header])) {
return [];
@@ -103,7 +103,7 @@ trait MessageTrait
}
}
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
$normalized = Utils::asciiToLower($header);
$new = clone $this;
if (isset($new->headerNames[$normalized])) {
@@ -135,7 +135,7 @@ trait MessageTrait
}
}
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
$normalized = Utils::asciiToLower($header);
$new = clone $this;
if (isset($new->headerNames[$normalized])) {
@@ -154,7 +154,7 @@ trait MessageTrait
*/
public function withoutHeader($header): MessageInterface
{
$normalized = strtolower($header);
$normalized = Utils::asciiToLower($header);
if (!isset($this->headerNames[$normalized])) {
return $this;
@@ -218,7 +218,7 @@ trait MessageTrait
}
}
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
$normalized = Utils::asciiToLower($header);
if (isset($this->headerNames[$normalized])) {
$header = $this->headerNames[$normalized];
$this->headers[$header] = array_merge($this->headers[$header], $value);

View File

@@ -1300,6 +1300,6 @@ final class MimeType
*/
public static function fromExtension(string $extension): ?string
{
return self::MIME_TYPES[strtolower($extension)] ?? null;
return self::MIME_TYPES[Utils::asciiToLower($extension)] ?? null;
}
}

View File

@@ -78,7 +78,7 @@ final class MultipartStream implements StreamInterface
$str .= "{$key}: {$value}\r\n";
}
return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n";
return "--{$this->boundary}\r\n".trim($str, " \n\r\t\0\x0B")."\r\n\r\n";
}
/**
@@ -227,9 +227,9 @@ final class MultipartStream implements StreamInterface
*/
private static function getHeader(array $headers, string $key): ?string
{
$lowercaseHeader = strtolower($key);
$lowercaseHeader = Utils::asciiToLower($key);
foreach ($headers as $k => $v) {
if (strtolower((string) $k) === $lowercaseHeader) {
if (Utils::asciiToLower((string) $k) === $lowercaseHeader) {
return $v;
}
}

View File

@@ -47,7 +47,7 @@ class Request implements RequestInterface
}
self::warnOnMethodCasingChange($method);
$this->method = strtoupper($method);
$this->method = Utils::asciiToUpper($method);
$this->uri = $uri;
$this->setHeaders($headers);
$this->protocol = $version;
@@ -108,7 +108,7 @@ class Request implements RequestInterface
$this->assertMethod($method);
self::warnOnMethodCasingChange($method);
$new = clone $this;
$new->method = strtoupper($method);
$new->method = Utils::asciiToUpper($method);
return $new;
}
@@ -184,7 +184,7 @@ class Request implements RequestInterface
private static function warnOnMethodCasingChange(string $method): void
{
if ($method !== strtoupper($method)) {
if ($method !== Utils::asciiToUpper($method)) {
\trigger_deprecation(
'guzzlehttp/psr7',
'2.11',

View File

@@ -165,7 +165,7 @@ class ServerRequest extends Request implements ServerRequestInterface
*/
public static function fromGlobals(): ServerRequestInterface
{
$method = strtoupper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
$method = Utils::asciiToUpper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
$headers = self::removeInvalidHostHeader(self::getAllHeaders());
$uri = self::getUriFromGlobals();
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
@@ -220,7 +220,7 @@ class ServerRequest extends Request implements ServerRequestInterface
private static function removeInvalidHostHeader(array $headers): array
{
foreach ($headers as $name => $value) {
if (strtolower((string) $name) !== 'host') {
if (Utils::asciiToLower((string) $name) !== 'host') {
continue;
}
@@ -269,7 +269,7 @@ class ServerRequest extends Request implements ServerRequestInterface
}
$serverPort = self::getServerParam('SERVER_PORT');
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/', $serverPort) === 1) {
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/D', $serverPort) === 1) {
$uri = $uri->withPort((int) $serverPort);
}

View File

@@ -99,9 +99,11 @@ class Uri implements UriInterface, \JsonSerializable
return self::parsePathNoSchemeReference($url);
}
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4 tails.
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4
// tails. DEL (\x7F) is excluded so a raw-DEL host falls through to the
// general path and is rejected rather than silently mutated by parse_url().
$prefix = '';
$ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20/?#@]+\])(.*)\z%s', $url, $matches);
$ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
if ($ipv6Prefix === false) {
return false;
@@ -111,7 +113,11 @@ class Uri implements UriInterface, \JsonSerializable
/** @var array{0:string, 1:string, 2:string} $matches */
$suffix = $matches[2];
if ($suffix !== '' && strpos(':/?#', $suffix[0]) === false) {
// After the bracketed host only an optional numeric port and/or a
// path, query, or fragment may follow. Anything else (for example
// `:80@evil` or `:80x`) would let parse_url() reinterpret a
// different host.
if (preg_match('%\A(?::[0-9]*)?(?:[/?#].*)?\z%s', $suffix) !== 1) {
return false;
}
@@ -690,7 +696,7 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('Scheme must be a string');
}
$scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
$scheme = Utils::asciiToLower($scheme);
if ($scheme !== '' && !preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
\trigger_deprecation(
@@ -733,7 +739,7 @@ class Uri implements UriInterface, \JsonSerializable
throw new \InvalidArgumentException('Host must be a string');
}
$host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
$host = Utils::asciiToLower($host);
self::assertValidHost($host);
return $host;

View File

@@ -19,7 +19,7 @@ final class UriComparator
*/
public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
{
if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
if (!Utils::caselessEquals($original->getHost(), $modified->getHost())) {
return true;
}

View File

@@ -192,7 +192,7 @@ final class UriNormalizer
$regex = '/(?:%[A-Fa-f0-9]{2})++/';
$callback = function (array $match): string {
return strtoupper($match[0]);
return Utils::asciiToUpper($match[0]);
};
return $uri

View File

@@ -10,6 +10,65 @@ use Psr\Http\Message\UriInterface;
final class Utils
{
/**
* Converts ASCII uppercase letters in a string to lowercase.
*
* Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the
* conversion is locale-independent and leaves every non-ASCII byte
* unchanged, as HTTP protocol elements require.
*/
public static function asciiToLower(string $string): string
{
return strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
}
/**
* Converts ASCII lowercase letters in a string to uppercase.
*
* Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the
* conversion is locale-independent and leaves every non-ASCII byte
* unchanged, as HTTP protocol elements require.
*/
public static function asciiToUpper(string $string): string
{
return strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
}
/**
* Converts the first character of a string to uppercase when it is an
* ASCII lowercase letter.
*
* Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion
* is locale-independent and leaves every non-ASCII byte unchanged, as
* HTTP protocol elements require.
*/
public static function asciiUcFirst(string $string): string
{
if ($string === '') {
return '';
}
return self::asciiToUpper($string[0]).substr($string, 1);
}
/**
* Checks whether the haystack contains the needle, comparing ASCII
* letters case-insensitively and without locale sensitivity.
*/
public static function caselessContains(string $haystack, string $needle): bool
{
return str_contains(self::asciiToLower($haystack), self::asciiToLower($needle));
}
/**
* Checks whether two strings are equal, comparing ASCII letters
* case-insensitively and without locale sensitivity.
*/
public static function caselessEquals(string $left, string $right): bool
{
return self::asciiToLower($left) === self::asciiToLower($right);
}
/**
* Remove the items given by the keys, case insensitively from the data.
*
@@ -20,11 +79,11 @@ final class Utils
$result = [];
foreach ($keys as &$key) {
$key = strtolower((string) $key);
$key = self::asciiToLower((string) $key);
}
foreach ($data as $k => $v) {
if (!in_array(strtolower((string) $k), $keys)) {
if (!in_array(self::asciiToLower((string) $k), $keys)) {
$result[$k] = $v;
}
}
@@ -212,7 +271,7 @@ final class Utils
if ($host !== '') {
if (isset($changes['set_headers']) && is_array($changes['set_headers'])) {
foreach (array_keys($changes['set_headers']) as $header) {
if (strtolower((string) $header) === 'host') {
if (self::asciiToLower((string) $header) === 'host') {
throw new \InvalidArgumentException(
'Cannot modify request with both a URI containing a host and an explicit Host header.'
);
@@ -248,7 +307,7 @@ final class Utils
$hasHost = false;
foreach (array_keys($headers) as $header) {
if (strtolower((string) $header) === 'host') {
if (self::asciiToLower((string) $header) === 'host') {
$hasHost = true;
break;
}
@@ -284,7 +343,7 @@ final class Utils
$addedHeaders = [];
foreach ($headers as $header => $value) {
$header = (string) $header;
$normalized = strtolower($header);
$normalized = self::asciiToLower($header);
if (isset($addedHeaders[$normalized])) {
/** @var RequestInterface */