Bump imapengine from 1.25.4 to 1.25.6 and dependencies

This commit is contained in:
johnnyq
2026-08-26 13:02:29 -04:00
parent 71cfdf5e39
commit 3d9a41bec4
54 changed files with 978 additions and 543 deletions

View File

@@ -5,6 +5,26 @@ 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).
## 3.1.0 - 2026-08-24
### Added
- Add `Utils::redactUriForMessage()` and `Utils::redactUriStringForMessage()` for URI diagnostics
- Add support for PHP 8.6
### Changed
- Omit rejected header values and sensitive URI components from automatic exception messages
## 3.0.1 - 2026-08-24
### Fixed
- Prefix relative paths that begin with a colon segment with `./` instead of throwing
- Apply the `/.` prefix for authority-less `//` paths to percent-encoding normalizations as well
- Keep colon-leading first path segments when reading the paths of scheme-less non-native URIs
- Stop throwing when removing the default `file` host strands a `//` path, prefixing it with `/.`
## 3.0.0 - 2026-07-20
### Added

View File

@@ -19,8 +19,8 @@ composer require guzzlehttp/psr7
| Version | Status | PHP Version |
|---------|--------------|--------------|
| 3.0 | Latest | >=7.4,<8.6 |
| 2.13 | Maintenance | >=7.2.5,<8.6 |
| 3.1 | Latest | >=7.4,<8.7 |
| 2.13 | Maintenance | >=7.2.5,<8.7 |
| 1.9 | End of Life | >=5.4,<8.2 |
## Quick Start

View File

@@ -32,6 +32,28 @@ userinfo.
A URI that does not parse has no trustworthy authority boundary, so everything
between any scheme and its last `@` is redacted as a safe-side fallback.
## `GuzzleHttp\Psr7\Utils::redactUriForMessage`
`public static function redactUriForMessage(UriInterface $uri): string`
Formats a URI for automatic diagnostics.
The whole userinfo component is replaced by "***", and the query and
fragment are removed. The scheme, host, port, and path are preserved.
The returned string is diagnostic-escaped and the formatter never
throws.
## `GuzzleHttp\Psr7\Utils::redactUriStringForMessage`
`public static function redactUriStringForMessage(string $uri): string`
Formats a raw URI for automatic diagnostics, including malformed input.
The whole userinfo component is replaced by "***", and the query and
fragment are removed. The scheme, host, port, and path are preserved
where their boundaries can be determined safely. The returned string is
diagnostic-escaped and the formatter never throws.
## `GuzzleHttp\Psr7\Utils::uriFor`
`public static function uriFor(string|UriInterface $uri): UriInterface`

View File

@@ -102,6 +102,14 @@ IDNA is treated as a client concern. Consumers that need DNS IDNs must perform
the conversion themselves, for example via Guzzle's `idn_conversion` request
option.
Validity here is a statement about URI syntax only. It does not imply that the
host resolves or that a client will interpret it exactly as written. A PSR-7 URI
object does not know which resolver or transport will consume it, so it cannot
establish destination identity. Consumers that restrict the destination must
enforce that policy at the client or transport boundary and ensure the
connection uses an approved address; checking the spelling or a separate DNS
lookup is insufficient.
### `GuzzleHttp\Psr7\Rfc3986::isValidPort`
`public static function isValidPort(string $port): bool`
@@ -248,6 +256,10 @@ the current request URI.
Converts the relative URI into a new URI that is resolved against the base URI.
When the resolved path is a relative-path reference whose first segment contains
a colon, which would be mistaken for a scheme name (RFC 3986 Section 4.2), it is
prefixed with `./`, e.g. `./a:b`.
### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`
`public static function removeDotSegments(string $path): string`
@@ -313,6 +325,13 @@ means the URIs `/?#` and `/` are treated equivalent which is not necessarily
true according to RFC 3986. But that difference is highly uncommon in reality.
So this potential normalization is implied in PSR-7 as well.
A path the URI cannot hold, such as a `//`-leading path without an authority or
a relative-path reference whose first segment contains a colon, is prefixed with
`/.` or `./` respectively instead of throwing, as `UriResolver::resolve()` does.
The percent-encoding normalizations only do so where they rewrote the path. For
example, decoding `a%41:` yields `./aA:`, since `aA:` would be an absolute URI
with the scheme `aa`.
The following normalizations are available:
- `UriNormalizer::PRESERVING_NORMALIZATIONS`
@@ -369,7 +388,11 @@ The following normalizations are available:
`file:///myfile`, and `file://localhost/myfile` are equivalent according to
RFC 3986.
Example: `file://localhost/myfile``file:///myfile`
When removing the host leaves a URI without an authority whose path begins
with `//`, the path is serialized with a `/.` prefix.
Example: `file://localhost/myfile``file:///myfile`,
`file://localhost//x``file:///.//x`
- `UriNormalizer::REMOVE_DEFAULT_PORT`

View File

@@ -78,7 +78,7 @@ trait MessageTrait
public function withHeader(string $name, $value): MessageInterface
{
$this->assertHeader($name);
$value = $this->normalizeHeaderValue($value);
$value = $this->normalizeHeaderValue($name, $value);
$normalized = Utils::asciiToLower($name);
$new = clone $this;
@@ -97,7 +97,7 @@ trait MessageTrait
public function withAddedHeader(string $name, $value): MessageInterface
{
$this->assertHeader($name);
$value = $this->normalizeHeaderValue($value);
$value = $this->normalizeHeaderValue($name, $value);
$normalized = Utils::asciiToLower($name);
$new = clone $this;
@@ -166,7 +166,7 @@ trait MessageTrait
$header = (string) $header;
$this->assertHeader($header);
$value = $this->normalizeHeaderValue($value);
$value = $this->normalizeHeaderValue($header, $value);
$normalized = Utils::asciiToLower($header);
if (isset($this->headerNames[$normalized])) {
$header = $this->headerNames[$normalized];
@@ -183,17 +183,17 @@ trait MessageTrait
*
* @return string[]
*/
private function normalizeHeaderValue($value): array
private function normalizeHeaderValue(string $header, $value): array
{
if (is_array($value) && $value === []) {
throw new \InvalidArgumentException('Header value must be a non-empty array or string.');
}
if (!is_array($value)) {
return $this->trimAndValidateHeaderValues([$value]);
return $this->trimAndValidateHeaderValues($header, [$value]);
}
return $this->trimAndValidateHeaderValues($value);
return $this->trimAndValidateHeaderValues($header, $value);
}
/**
@@ -210,9 +210,9 @@ trait MessageTrait
*
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
*/
private function trimAndValidateHeaderValues(array $values): array
private function trimAndValidateHeaderValues(string $header, array $values): array
{
return array_map(function ($value): string {
return array_map(function ($value) use ($header): string {
if (!is_string($value)) {
throw new \InvalidArgumentException(sprintf(
'Header value must be a string or array of strings but %s provided.',
@@ -221,7 +221,7 @@ trait MessageTrait
}
$trimmed = trim($value, " \t");
$this->assertValue($trimmed);
$this->assertValue($header, $trimmed);
return $trimmed;
}, array_values($values));
@@ -254,7 +254,7 @@ trait MessageTrait
* obs-text = %x80-FF
* obs-fold = CRLF 1*( SP / HTAB )
*/
private function assertValue(string $value): void
private function assertValue(string $header, string $value): void
{
// The regular expression intentionally does not support the obs-fold
// production, because as per RFC 9112#5.2:
@@ -269,7 +269,11 @@ trait MessageTrait
// obscure feature of HTTP/1.1 and thus not accepting folding is not
// likely to break any legitimate use case.
if (!Rfc9110::isFieldValue($value)) {
throw new \InvalidArgumentException(sprintf('Invalid header value: %s', DiagnosticValue::escape($value)));
$reason = strpbrk($value, "\r\n") !== false
? 'must not contain CR or LF characters'
: 'contains an invalid control character';
throw new \InvalidArgumentException(sprintf('Header "%s" %s.', DiagnosticValue::escape($header), $reason));
}
}
}

View File

@@ -72,7 +72,7 @@ final class MultipartStream implements StreamInterface
$key = (string) $key;
self::validatePartHeaderName($key);
self::validatePartHeaderValue($value);
self::validatePartHeaderValue($key, $value);
$str .= "{$key}: {$value}\r\n";
}
@@ -252,7 +252,7 @@ final class MultipartStream implements StreamInterface
throw new \InvalidArgumentException('Multipart part header value must be a string.');
}
self::validatePartHeaderValue($value);
self::validatePartHeaderValue($key, $value);
$normalized[$key] = $value;
}
@@ -267,10 +267,14 @@ final class MultipartStream implements StreamInterface
}
}
private static function validatePartHeaderValue(string $value): void
private static function validatePartHeaderValue(string $name, string $value): void
{
if (!Rfc9110::isFieldValue($value)) {
throw new \InvalidArgumentException(sprintf('Invalid multipart part header value: %s', DiagnosticValue::escape($value)));
$reason = strpbrk($value, "\r\n") !== false
? 'must not contain CR or LF characters'
: 'contains an invalid control character';
throw new \InvalidArgumentException(sprintf('Multipart part header "%s" %s.', DiagnosticValue::escape($name), $reason));
}
}

View File

@@ -150,7 +150,7 @@ class Request implements RequestInterface
$host .= ':'.$port;
}
$this->assertValue($host);
$this->assertValue('Host', $host);
return $host;
}

View File

@@ -69,7 +69,7 @@ class Uri implements UriInterface, \JsonSerializable
if ($uri !== '') {
$parts = UriParser::parse($uri);
if ($parts === false) {
throw new MalformedUriException(\sprintf('Unable to parse URI: %s', DiagnosticValue::escape($uri)));
throw new MalformedUriException(\sprintf('Unable to parse URI: %s', Utils::redactUriStringForMessage($uri)));
}
try {
$this->applyParts($parts);
@@ -411,7 +411,10 @@ class Uri implements UriInterface, \JsonSerializable
* components, including the leading slash the string form adds to a
* rootless path when an authority is present; for subclasses and other
* implementations the path is split from the string form per RFC 3986
* Appendix B, without validating or decoding any other component.
* Appendix B, without validating or decoding any other component. The
* scheme is only split off when the instance reports one, as a relative
* reference can begin with a segment containing a colon that the Appendix
* B expression would otherwise read as a scheme.
*
* @throws \RuntimeException If the path cannot be split from the string form.
*
@@ -429,7 +432,10 @@ class Uri implements UriInterface, \JsonSerializable
return $uri->path;
}
$count = preg_match('%^(?:[^:/?#]+:)?(?://[^/?#]*)?([^?#]*)%', (string) $uri, $matches);
$pattern = $uri->getScheme() === ''
? '%^(?://[^/?#]*)?([^?#]*)%'
: '%^(?:[^:/?#]+:)?(?://[^/?#]*)?([^?#]*)%';
$count = preg_match($pattern, (string) $uri, $matches);
if ($count === false) {
throw new \RuntimeException('Unable to read the URI path: '.preg_last_error_msg());

View File

@@ -86,7 +86,11 @@ final class UriNormalizer
* second format in the Uri class. See
* `GuzzleHttp\Psr7\Uri::composeComponents`.
*
* When removing the host leaves a URI without an authority whose path
* begins with `//`, the path is serialized with a `/.` prefix.
*
* Example: file://localhost/myfile → file:///myfile
* Example: file://localhost//x → file:///.//x
*/
public const REMOVE_DEFAULT_HOST = 8;
@@ -166,6 +170,13 @@ final class UriNormalizer
* uncommon in reality. So this potential normalization is implied in PSR-7
* as well.
*
* A path the URI cannot hold, such as a `//`-leading path without an
* authority or a relative-path reference whose first segment contains a
* colon, is prefixed with `/.` or `./` respectively instead of throwing, as
* `UriResolver::resolve()` does. The percent-encoding normalizations only
* do so where they rewrote the path. For example, decoding `a%41:` yields
* `./aA:`, since `aA:` would be an absolute URI with the scheme `aa`.
*
* @param UriInterface $uri The URI to normalize
* @param int $flags A bitmask of normalizations to apply, see constants
*
@@ -188,6 +199,14 @@ final class UriNormalizer
}
if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') {
if ($uri->getUserInfo() === '' && $uri->getPort() === null) {
$path = Uri::rawPath($uri);
if (str_starts_with($path, '//')) {
// "/." keeps a "//" path unambiguous once the authority is gone
$uri = $uri->withPath('/.'.$path);
}
}
$uri = $uri->withHost('');
}
@@ -260,8 +279,7 @@ final class UriNormalizer
$uri = self::withNormalizedUserInfo($uri, $regex, $callback);
$uri = self::withNormalizedHost($uri, $regex, $callback);
return $uri
->withPath(self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
return self::withGuardedPath($uri, self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
}
@@ -284,12 +302,24 @@ final class UriNormalizer
$uri = self::withNormalizedUserInfo($uri, $regex, $callback);
$uri = self::withNormalizedHost($uri, $regex, $hostCallback);
return $uri
->withPath(self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
return self::withGuardedPath($uri, self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
}
/**
* Writes the given path only when it differs from the current one, guarded
* so the write cannot throw.
*/
private static function withGuardedPath(UriInterface $uri, string $path): UriInterface
{
if ($path === Uri::rawPath($uri)) {
return $uri;
}
return $uri->withPath(UriResolver::guardedPath($uri, $path));
}
/**
* @param callable(array): string $callback
*/

View File

@@ -65,8 +65,8 @@ final class UriResolver
}
/**
* Returns the path, prefixed with "/." when it would otherwise start the
* URI's string form with an authority-like "//".
* Returns the path, prefixed with "/." or "./" when the URI could not
* otherwise hold it.
*
* A URI without an authority cannot hold a path beginning with "//" (RFC
* 3986 Section 3.3), but removeDotSegments() can produce one. The "/."
@@ -76,28 +76,47 @@ final class UriResolver
* written, so the path cannot be mistaken for an authority and the prefix
* is not added.
*
* A relative-path reference cannot begin with a segment containing a colon
* (RFC 3986 Section 4.2), as it would be mistaken for a scheme name, but
* reference resolution and percent-encoding normalization can produce one.
* The "./" prefix the RFC prescribes resolves back to the same path.
*
* @see https://url.spec.whatwg.org/#url-serializing
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
*
* @internal
*/
public static function guardedPath(UriInterface $uri, string $path): string
{
if (!str_starts_with($path, '//') || $uri->getAuthority() !== '') {
if ($uri->getAuthority() !== '') {
return $path;
}
if ($uri instanceof Uri && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https')) {
return $path;
if (str_starts_with($path, '//')) {
if ($uri instanceof Uri && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https')) {
return $path;
}
return '/.'.$path;
}
return '/.'.$path;
if ($uri->getScheme() === '' && str_contains(explode('/', $path, 2)[0], ':')) {
return './'.$path;
}
return $path;
}
/**
* Converts the relative URI into a new URI that is resolved against the
* base URI.
*
* When the resolved path is a relative-path reference whose first segment
* contains a colon, which would be mistaken for a scheme name (RFC 3986
* Section 4.2), it is prefixed with `./`, e.g. `./a:b`.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
*/
public static function resolve(UriInterface $base, UriInterface $rel): UriInterface
{

View File

@@ -583,6 +583,56 @@ final class Utils
return \str_replace(\substr($authority, 0, $atPosition).'@', '***@', $subject);
}
/**
* Formats a URI for automatic diagnostics.
*
* The whole userinfo component is replaced by "***", and the query and
* fragment are removed. The scheme, host, port, and path are preserved.
* The returned string is diagnostic-escaped and the formatter never
* throws.
*/
public static function redactUriForMessage(
#[\SensitiveParameter]
UriInterface $uri
): string {
try {
$raw = (string) $uri;
} catch (\Throwable $e) {
return '[unavailable URI]';
}
try {
if ($uri->getUserInfo() !== '') {
$uri = $uri->withUserInfo('***');
}
return DiagnosticValue::escape((string) $uri->withQuery('')->withFragment(''));
} catch (\Throwable $e) {
return self::redactUriStringForMessage($raw);
}
}
/**
* Formats a raw URI for automatic diagnostics, including malformed input.
*
* The whole userinfo component is replaced by "***", and the query and
* fragment are removed. The scheme, host, port, and path are preserved
* where their boundaries can be determined safely. The returned string is
* diagnostic-escaped and the formatter never throws.
*/
public static function redactUriStringForMessage(
#[\SensitiveParameter]
string $uri
): string {
try {
$uri = self::redactUserInfoInString($uri, $uri);
return DiagnosticValue::escape(\substr($uri, 0, \strcspn($uri, '?#')));
} catch (\Throwable $e) {
return '[unavailable URI]';
}
}
/**
* Create a new stream based on the input type.
*