v2.1.7
This commit is contained in:
parent
c2ead8e434
commit
77d0cea926
995 changed files with 28142 additions and 8603 deletions
166
vendor/guzzlehttp/psr7/CHANGELOG.md
vendored
166
vendor/guzzlehttp/psr7/CHANGELOG.md
vendored
|
|
@ -5,6 +5,172 @@ 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.0.0 - 2026-07-20
|
||||
|
||||
### Added
|
||||
|
||||
- Add `DiagnosticValue::escape()` to escape controls and malformed UTF-8 in diagnostics
|
||||
- Add `GuzzleHttp\Psr7\Exception\TimeoutException` for timed-out stream operations
|
||||
- Add `GuzzleHttp\Psr7\Utils::redactUserInfoInString()` to redact the userinfo of a raw URI string within text
|
||||
- Promote `GuzzleHttp\Psr7\Rfc3986` to public API with `isValid*()` predicates and `canonicalizeIpv6()`
|
||||
- Add `GuzzleHttp\Psr7\UriNormalizer::CANONICALIZE_IPV6_HOST` to `PRESERVING_NORMALIZATIONS`
|
||||
|
||||
### Changed
|
||||
|
||||
- Require `psr/http-message:^2.0` and add native parameter and return types
|
||||
- Require `psr/http-factory:^1.1`
|
||||
- Reject native PHP serialization of stream implementations
|
||||
- Preserve request method casing, except `ServerRequest::fromGlobals()` still uppercases
|
||||
- Reject empty arrays and non-string values as header values
|
||||
- Reject invalid uploaded file trees and invalid parsed body values
|
||||
- Reject uploaded file specs missing `tmp_name`, `size`, or `error`
|
||||
- Reject non-integer and negative uploaded file `error` values
|
||||
- Reject invalid stream/upload sizes, buffer high-water marks, and dropping-stream limits
|
||||
- Rewind seekable uploaded-file streams before copying in `UploadedFile::moveTo()`
|
||||
- Reject negative `read()` lengths across all stream implementations
|
||||
- Detect the `+` flag anywhere in a mode for `Stream::isReadable()`/`isWritable()`
|
||||
- Reject empty strings returned by `PumpStream` source callables
|
||||
- Discard buffered bytes on `PumpStream` close and detach
|
||||
- Restore the original stream position after `Message::bodySummary()`
|
||||
- Allow `null` for the `Message::bodySummary()` truncation length to use the default
|
||||
- Validate `LimitStream` offset/limit and track non-seekable offset by bytes skipped
|
||||
- Make `FnStream` close and detach terminal, calling close callbacks at most once
|
||||
- Suppress exceptions from `FnStream` close callbacks during destructor cleanup
|
||||
- Make `CachingStream::close()` idempotent, preserving remote cleanup after detach
|
||||
- Do not move the `CachingStream` cursor when a `SEEK_END` target on an unknown-size stream is rejected
|
||||
- Normalize multiple leading slashes in `Uri::getPath()` and origin-form request targets
|
||||
- Serialize authority-less `file` URIs with rootless paths without the `//` separator
|
||||
- Serialize authority-less `file` URIs with empty paths as `file:` instead of the unparseable `file://`
|
||||
- Remove dot segments above the root per RFC 3986, prefixing authority-less `//` paths with `/.`
|
||||
- Return a network-path reference from `UriResolver::relativize()` when an empty-path target requires one
|
||||
- Stop returning an empty reference from `UriResolver::relativize()` when it would inherit the base fragment
|
||||
- Stop throwing from `UriResolver::relativize()` when an equal-path target's last path segment contains a colon
|
||||
- Harden URI host validation (delimiters, backslashes, IPv6, embedded ports); require schemes to start with a letter
|
||||
- Validate `Uri::fromParts()` ports instead of casting them
|
||||
- Treat ports 80 and 443 as defaults for the `ws` and `wss` schemes
|
||||
- Use the `ws` and `wss` default ports in `UriComparator::isCrossOrigin()` port comparisons
|
||||
- Redact all non-empty URI userinfo in `Utils::redactUserInfo()`
|
||||
- Rebuild server request URIs from `$_SERVER` by `REQUEST_METHOD`, using target authority before `SERVER_PORT`
|
||||
- Remove userinfo from absolute-form `REQUEST_URI` targets in `ServerRequest::fromGlobals()`
|
||||
- Reject zero-port `HTTP_HOST` and malformed `SERVER_PORT` in `ServerRequest::getUriFromGlobals()`
|
||||
- Reject malformed `REQUEST_METHOD` and `SERVER_PROTOCOL` server values in `ServerRequest::fromGlobals()`
|
||||
- Reject zero-port `Host` and normalize leading-zero ports in `Message::parseRequest()`
|
||||
- Reject duplicate `Host` headers and validate present values for all request-target forms
|
||||
- Reject zero-port, hostless, and userinfo absolute-form request targets in `Message::parseRequest()`
|
||||
- Synchronize the `Host` header in `Request::withUri()` when the URI changes or Host is empty
|
||||
- Include the URI port in `Host` headers synthesized by `Message::toString()`
|
||||
- Validate the Host header synthesized by `Message::toString()` from the request URI
|
||||
- Include non-default URI ports in `Host` headers set by `Utils::modifyRequest()` URI changes
|
||||
- Accept `OPTIONS *` and `CONNECT` authority-form request targets in `Message::parseRequest()`
|
||||
- Hide credential-bearing URI, server, and cookie arguments in stack traces on PHP 8.2+
|
||||
- Reject malformed HTTP request/response start-lines
|
||||
- Unfold obsolete HTTP/1.0 line folding for all valid request method tokens
|
||||
- Reject empty and control-character request targets in `Request::withRequestTarget()`
|
||||
- Validate iterator chunks passed to `Utils::streamFor()`
|
||||
- Validate unsupported values passed to `Query::build()`
|
||||
- Reject non-finite float values in `Query::build()` and `MultipartStream` contents
|
||||
- Reject non-finite float values in iterator chunks passed to `Utils::streamFor()`
|
||||
- `Utils::streamFor()` now rejects non-string scalar bodies
|
||||
- `Uri::withQueryValues()` now rejects non-string values
|
||||
- Reject invalid `Utils::modifyRequest()` change values
|
||||
- Use PHP debug type names in type error messages
|
||||
- Changed `Utils::copyToStream()` to throw when destination streams cannot make progress
|
||||
- Throw `TimeoutException` from `Stream` read/write and `Utils` copy/hash/readLine on stream timeouts
|
||||
- Throw `TimeoutException` from `AppendStream::read()`, `CachingStream::read()`, and `Utils::tryGetContents()` on stream timeouts
|
||||
- Re-throw `TimeoutException` from `InflateStream` when the decoded source stream times out
|
||||
- Close the compressed source stream from `InflateStream::close()`
|
||||
- Return the number of bytes copied from `Utils::copyToStream()`
|
||||
- Throw `OverflowException` when stream byte counts or offsets exceed `PHP_INT_MAX`
|
||||
- Translate `StreamWrapper` runtime failures to PHP stream failure values
|
||||
- Stop adding default `Content-Length` to `multipart/form-data` parts (RFC 7578 §4.8)
|
||||
- Escape multipart `Content-Disposition` parameters and reject unsafe boundaries and part headers
|
||||
- Preserve trailing whitespace in custom `MultipartStream` part header values
|
||||
- Preserve explicit custom `MultipartStream` boundary `'0'` instead of replacing it with a generated boundary
|
||||
- Made static utility classes non-instantiable
|
||||
- Validate bracketed IP-literal hosts consistently between parsing and `withHost()`, including userinfo forms
|
||||
- Percent-encode raw control bytes in userinfo before bracketed IP-literal hosts instead of parsing mutated values
|
||||
- Reject invalid UTF-8 and preserve percent-sequences in userinfo before bracketed IP-literal hosts
|
||||
- Normalize percent-encoded octets in the URI host to uppercase hex
|
||||
- Canonicalize IPv6 hosts to RFC 5952 form in `Uri` construction, `fromParts()`, and `withHost()`
|
||||
- Canonicalize bracketed IPv6 hosts in `UriComparator::isCrossOrigin()`
|
||||
- Reject malformed percent-sequences and percent-encoded bytes forbidden by the URI host policy
|
||||
- Extend `CAPITALIZE_PERCENT_ENCODING` and `DECODE_UNRESERVED_CHARACTERS` to userinfo and host
|
||||
- Trim header list elements with only spaces, horizontal tabs, and line terminators in `Header::splitList()`
|
||||
- Report header parameter PCRE failures explicitly in `Header::parse()`
|
||||
- Escape controls and malformed UTF-8 consistently in generated exception messages
|
||||
|
||||
### Removed
|
||||
|
||||
- Dropped support for PHP 7.2 and 7.3
|
||||
- Removed the `ralouphie/getallheaders` dependency
|
||||
- Removed deprecated `Header::normalize()` method
|
||||
|
||||
## 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
|
||||
|
||||
- Validate the URI host so `getHost()` matches the URI authority (GHSA-c2w2-prh8-qm98)
|
||||
|
||||
## 2.12.2 - 2026-06-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Report URI parsing, filtering, and normalization PCRE failures explicitly
|
||||
- Report HTTP message parser PCRE failures explicitly
|
||||
- Fail closed when PCRE validation fails for request targets and hosts
|
||||
|
||||
## 2.12.1 - 2026-06-18
|
||||
|
||||
### Security
|
||||
|
||||
- Reject CR/LF in HTTP method, protocol version, and reason phrase (GHSA-vm85-hxw5-5432)
|
||||
|
||||
## 2.12.0 - 2026-06-16
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated non-finite float values in `Query::build()` that guzzlehttp/psr7 3.0 rejects
|
||||
- Deprecated non-finite float multipart contents that guzzlehttp/psr7 3.0 rejects
|
||||
- Deprecated non-string scalar bodies in `Utils::streamFor()`; cast them to a string for 3.0
|
||||
- Deprecated non-string `Uri::withQueryValues()` values; cast them to a string for 3.0
|
||||
|
||||
## 2.11.1 - 2026-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed non-finite float values emitting coercion warnings on PHP 8.5
|
||||
|
||||
## 2.11.0 - 2026-06-02
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
904
vendor/guzzlehttp/psr7/README.md
vendored
904
vendor/guzzlehttp/psr7/README.md
vendored
|
|
@ -1,880 +1,88 @@
|
|||
# PSR-7 Message Implementation
|
||||
# Guzzle PSR-7
|
||||
|
||||
This repository contains a full [PSR-7](https://www.php-fig.org/psr/psr-7/)
|
||||
message implementation, several stream decorators, and some helpful
|
||||
functionality like query string parsing.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
||||
This package comes with a number of stream implementations and stream
|
||||
decorators.
|
||||
`guzzlehttp/psr7` is a PSR-7 HTTP message implementation for PHP. It provides
|
||||
request, response, URI, uploaded file, and stream objects that work with Guzzle
|
||||
and any other library using the PSR-7 interfaces.
|
||||
|
||||
Use this package directly when you need to create or inspect PSR-7 messages
|
||||
without sending HTTP requests. If you only want to make HTTP requests, install
|
||||
[`guzzlehttp/guzzle`](https://github.com/guzzle/guzzle/blob/8.0/README.md)
|
||||
instead; it already depends on this package.
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
```bash
|
||||
composer require guzzlehttp/psr7
|
||||
```
|
||||
|
||||
## Version Guidance
|
||||
|
||||
| Version | Status | PHP Version |
|
||||
|---------|---------------------|--------------|
|
||||
| 1.x | EOL (2024-06-30) | >=5.4,<8.2 |
|
||||
| 2.x | Latest | >=7.2.5,<8.6 |
|
||||
| Version | Status | PHP Version |
|
||||
|---------|--------------|--------------|
|
||||
| 3.0 | Latest | >=7.4,<8.6 |
|
||||
| 2.13 | Maintenance | >=7.2.5,<8.6 |
|
||||
| 1.9 | End of Life | >=5.4,<8.2 |
|
||||
|
||||
See [UPGRADING.md](UPGRADING.md) for notes on upgrading from 1.x to 2.0.
|
||||
|
||||
|
||||
## AppendStream
|
||||
|
||||
`GuzzleHttp\Psr7\AppendStream`
|
||||
|
||||
Reads from multiple streams, one after the other.
|
||||
## Quick Start
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
|
||||
$a = Psr7\Utils::streamFor('abc, ');
|
||||
$b = Psr7\Utils::streamFor('123.');
|
||||
$composed = new Psr7\AppendStream([$a, $b]);
|
||||
$request = new Request('GET', 'https://example.com/api');
|
||||
$response = new Response(200, ['Content-Type' => 'text/plain'], 'OK');
|
||||
$stream = Utils::streamFor('request or response body');
|
||||
|
||||
$composed->addStream(Psr7\Utils::streamFor(' Above all listen to me'));
|
||||
|
||||
echo $composed; // abc, 123. Above all listen to me.
|
||||
echo $request->getMethod();
|
||||
echo $response->getStatusCode();
|
||||
echo $stream;
|
||||
```
|
||||
|
||||
|
||||
## BufferStream
|
||||
|
||||
`GuzzleHttp\Psr7\BufferStream`
|
||||
|
||||
Provides a buffer stream that can be written to fill a buffer, and read
|
||||
from to remove bytes from the buffer.
|
||||
|
||||
This stream returns a "hwm" metadata value that tells upstream consumers
|
||||
what the configured high water mark of the stream is, or the maximum
|
||||
preferred size of the buffer.
|
||||
PSR-7 messages and URIs are immutable. Methods such as `withHeader()` and
|
||||
`withUri()` return a changed copy instead of modifying the original object.
|
||||
Streams are mutable body handles; reading, writing, seeking, and closing a
|
||||
stream can change its cursor, contents, or usability.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
// When more than 1024 bytes are in the buffer, it will begin returning
|
||||
// 0 to writes. This is an indication that writers should slow down.
|
||||
$buffer = new Psr7\BufferStream(1024);
|
||||
$request = $request->withHeader('Accept', 'application/json');
|
||||
```
|
||||
|
||||
|
||||
## CachingStream
|
||||
|
||||
The CachingStream is used to allow seeking over previously read bytes on
|
||||
non-seekable streams. This can be useful when transferring a non-seekable
|
||||
entity body fails due to needing to rewind the stream (for example, resulting
|
||||
from a redirect). Data that is read from the remote stream will be buffered in
|
||||
a PHP temp stream so that previously read bytes are cached first in memory,
|
||||
then on disk.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
$original = Psr7\Utils::streamFor(fopen('http://www.google.com', 'r'));
|
||||
$stream = new Psr7\CachingStream($original);
|
||||
|
||||
$stream->read(1024);
|
||||
echo $stream->tell();
|
||||
// 1024
|
||||
|
||||
$stream->seek(0);
|
||||
echo $stream->tell();
|
||||
// 0
|
||||
```
|
||||
|
||||
|
||||
## DroppingStream
|
||||
|
||||
`GuzzleHttp\Psr7\DroppingStream`
|
||||
|
||||
Stream decorator that begins dropping data once the size of the underlying
|
||||
stream becomes too full.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
// Create an empty stream
|
||||
$stream = Psr7\Utils::streamFor();
|
||||
|
||||
// Start dropping data when the stream has more than 10 bytes
|
||||
$dropping = new Psr7\DroppingStream($stream, 10);
|
||||
|
||||
$dropping->write('01234567890123456789');
|
||||
echo $stream; // 0123456789
|
||||
```
|
||||
|
||||
|
||||
## FnStream
|
||||
|
||||
`GuzzleHttp\Psr7\FnStream`
|
||||
|
||||
Compose stream implementations based on a hash of callables.
|
||||
|
||||
Allows for easy testing and extension of a provided stream without needing
|
||||
to create a concrete class for a simple extension point.
|
||||
|
||||
```php
|
||||
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
$stream = Psr7\Utils::streamFor('hi');
|
||||
$fnStream = Psr7\FnStream::decorate($stream, [
|
||||
'rewind' => function () use ($stream) {
|
||||
echo 'About to rewind - ';
|
||||
$stream->rewind();
|
||||
echo 'rewound!';
|
||||
}
|
||||
]);
|
||||
|
||||
$fnStream->rewind();
|
||||
// Outputs: About to rewind - rewound!
|
||||
```
|
||||
|
||||
|
||||
## InflateStream
|
||||
|
||||
`GuzzleHttp\Psr7\InflateStream`
|
||||
|
||||
Uses PHP's zlib.inflate filter to inflate zlib (HTTP deflate, RFC1950) or gzipped (RFC1952) content.
|
||||
|
||||
This stream decorator converts the provided stream to a PHP stream resource,
|
||||
then appends the zlib.inflate filter. The stream is then converted back
|
||||
to a Guzzle stream resource to be used as a Guzzle stream.
|
||||
|
||||
|
||||
## LazyOpenStream
|
||||
|
||||
`GuzzleHttp\Psr7\LazyOpenStream`
|
||||
|
||||
Lazily reads or writes to a file that is opened only after an IO operation
|
||||
take place on the stream.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
$stream = new Psr7\LazyOpenStream('/path/to/file', 'r');
|
||||
// The file has not yet been opened...
|
||||
|
||||
echo $stream->read(10);
|
||||
// The file is opened and read from only when needed.
|
||||
```
|
||||
|
||||
|
||||
## LimitStream
|
||||
|
||||
`GuzzleHttp\Psr7\LimitStream`
|
||||
|
||||
LimitStream can be used to read a subset or slice of an existing stream object.
|
||||
This can be useful for breaking a large file into smaller pieces to be sent in
|
||||
chunks (e.g. Amazon S3's multipart upload API).
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
$original = Psr7\Utils::streamFor(fopen('/tmp/test.txt', 'r+'));
|
||||
echo $original->getSize();
|
||||
// >>> 1048576
|
||||
|
||||
// Limit the size of the body to 1024 bytes and start reading from byte 2048
|
||||
$stream = new Psr7\LimitStream($original, 1024, 2048);
|
||||
echo $stream->getSize();
|
||||
// >>> 1024
|
||||
echo $stream->tell();
|
||||
// >>> 0
|
||||
```
|
||||
|
||||
|
||||
## MultipartStream
|
||||
|
||||
`GuzzleHttp\Psr7\MultipartStream`
|
||||
|
||||
Stream that when read returns bytes for a streaming multipart or
|
||||
multipart/form-data stream.
|
||||
|
||||
Each multipart element must contain a `name` and `contents` key. `contents` may
|
||||
be any non-array value accepted by `GuzzleHttp\Psr7\Utils::streamFor()`,
|
||||
including closures and invokable objects. Array contents are recursively
|
||||
expanded into nested form fields.
|
||||
|
||||
|
||||
## NoSeekStream
|
||||
|
||||
`GuzzleHttp\Psr7\NoSeekStream`
|
||||
|
||||
NoSeekStream wraps a stream and does not allow seeking.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
$original = Psr7\Utils::streamFor('foo');
|
||||
$noSeek = new Psr7\NoSeekStream($original);
|
||||
|
||||
echo $noSeek->read(3);
|
||||
// foo
|
||||
var_export($noSeek->isSeekable());
|
||||
// false
|
||||
$noSeek->seek(0);
|
||||
var_export($noSeek->read(3));
|
||||
// NULL
|
||||
```
|
||||
|
||||
|
||||
## PumpStream
|
||||
|
||||
`GuzzleHttp\Psr7\PumpStream`
|
||||
|
||||
Provides a read only stream that pumps data from a PHP callable.
|
||||
|
||||
When invoking the provided callable, the PumpStream will pass the suggested
|
||||
number of bytes to read to the callable. The callable can choose to ignore
|
||||
this value and return fewer or more bytes than requested. Any extra data
|
||||
returned by the provided callable is buffered internally until drained using
|
||||
the read() function of the PumpStream. The provided callable MUST return
|
||||
false or null when there is no more data to read.
|
||||
|
||||
Userland callables that declare no parameters are tolerated by PHP, but
|
||||
length-aware callables remain the recommended formal shape.
|
||||
|
||||
|
||||
## Implementing stream decorators
|
||||
|
||||
Creating a stream decorator is very easy thanks to the
|
||||
`GuzzleHttp\Psr7\StreamDecoratorTrait`. This trait provides methods that
|
||||
implement `Psr\Http\Message\StreamInterface` by proxying to an underlying
|
||||
stream. Just `use` the `StreamDecoratorTrait` and implement your custom
|
||||
methods.
|
||||
|
||||
For example, let's say we wanted to call a specific function each time the last
|
||||
byte is read from a stream. This could be implemented by overriding the
|
||||
`read()` method.
|
||||
|
||||
```php
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use GuzzleHttp\Psr7\StreamDecoratorTrait;
|
||||
|
||||
class EofCallbackStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
|
||||
private $callback;
|
||||
|
||||
private $stream;
|
||||
|
||||
public function __construct(StreamInterface $stream, callable $cb)
|
||||
{
|
||||
$this->stream = $stream;
|
||||
$this->callback = $cb;
|
||||
}
|
||||
|
||||
public function read($length)
|
||||
{
|
||||
$result = $this->stream->read($length);
|
||||
|
||||
// Invoke the callback when EOF is hit.
|
||||
if ($this->eof()) {
|
||||
($this->callback)();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This decorator could be added to any existing stream and used like so:
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7;
|
||||
|
||||
$original = Psr7\Utils::streamFor('foo');
|
||||
|
||||
$eofStream = new EofCallbackStream($original, function () {
|
||||
echo 'EOF!';
|
||||
});
|
||||
|
||||
$eofStream->read(2);
|
||||
$eofStream->read(1);
|
||||
// echoes "EOF!"
|
||||
$eofStream->seek(0);
|
||||
$eofStream->read(3);
|
||||
// echoes "EOF!"
|
||||
```
|
||||
|
||||
|
||||
## PHP StreamWrapper
|
||||
|
||||
You can use the `GuzzleHttp\Psr7\StreamWrapper` class if you need to use a
|
||||
PSR-7 stream as a PHP stream resource.
|
||||
|
||||
Use the `GuzzleHttp\Psr7\StreamWrapper::getResource()` method to create a PHP
|
||||
stream from a PSR-7 stream.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7\StreamWrapper;
|
||||
|
||||
$stream = GuzzleHttp\Psr7\Utils::streamFor('hello!');
|
||||
$resource = StreamWrapper::getResource($stream);
|
||||
echo fread($resource, 6); // outputs hello!
|
||||
```
|
||||
|
||||
|
||||
# Static API
|
||||
|
||||
There are various static methods available under the `GuzzleHttp\Psr7` namespace.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::toString`
|
||||
|
||||
`public static function toString(MessageInterface $message): string`
|
||||
|
||||
Returns the string representation of an HTTP message.
|
||||
|
||||
```php
|
||||
$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com');
|
||||
echo GuzzleHttp\Psr7\Message::toString($request);
|
||||
```
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::bodySummary`
|
||||
|
||||
`public static function bodySummary(MessageInterface $message, int $truncateAt = 120): string|null`
|
||||
|
||||
Get a short summary of the message body.
|
||||
|
||||
Will return `null` if the response is not printable.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::rewindBody`
|
||||
|
||||
`public static function rewindBody(MessageInterface $message): void`
|
||||
|
||||
Attempts to rewind a message body and throws an exception on failure.
|
||||
|
||||
The body of the message will only be rewound if a call to `tell()`
|
||||
returns a value other than `0`.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::parseMessage`
|
||||
|
||||
`public static function parseMessage(string $message): array`
|
||||
|
||||
Parses an HTTP message into an associative array.
|
||||
|
||||
The array contains the "start-line" key containing the start line of
|
||||
the message, "headers" key containing an associative array of header
|
||||
array values, and a "body" key containing the body of the message.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::parseRequestUri`
|
||||
|
||||
`public static function parseRequestUri(string $path, array $headers): string`
|
||||
|
||||
Constructs a URI for an HTTP request message.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::parseRequest`
|
||||
|
||||
`public static function parseRequest(string $message): Request`
|
||||
|
||||
Parses a request message string into a request object.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Message::parseResponse`
|
||||
|
||||
`public static function parseResponse(string $message): Response`
|
||||
|
||||
Parses a response message string into a response object.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Header::parse`
|
||||
|
||||
`public static function parse(string|array $header): array`
|
||||
|
||||
Parse an array of header values containing ";" separated data into an
|
||||
array of associative arrays representing the header key value pair data
|
||||
of the header. When a parameter does not contain a value, but just
|
||||
contains a key, this function will inject a key with a '' string value.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Header::splitList`
|
||||
|
||||
`public static function splitList(string|string[] $header): string[]`
|
||||
|
||||
Splits a HTTP header defined to contain a comma-separated list into
|
||||
each individual value:
|
||||
|
||||
```
|
||||
$knownEtags = Header::splitList($request->getHeader('if-none-match'));
|
||||
```
|
||||
|
||||
Example headers include `accept`, `cache-control` and `if-none-match`.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Header::normalize` (deprecated)
|
||||
|
||||
`public static function normalize(string|array $header): array`
|
||||
|
||||
`Header::normalize()` is deprecated in favor of [`Header::splitList()`](README.md#guzzlehttppsr7headersplitlist)
|
||||
which performs the same operation with a cleaned up API and improved
|
||||
documentation.
|
||||
|
||||
Converts an array of header values that may contain comma separated
|
||||
headers into an array of headers with no comma separated values.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Query::parse`
|
||||
|
||||
`public static function parse(string $str, int|bool $urlEncoding = true): array`
|
||||
|
||||
Parse a query string into an associative array.
|
||||
|
||||
If multiple values are found for the same key, the value of that key
|
||||
value pair will become an array. This function does not parse nested
|
||||
PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2`
|
||||
will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Query::build`
|
||||
|
||||
`public static function build(array $params, int|false $encoding = PHP_QUERY_RFC3986, bool $treatBoolsAsInts = true): string`
|
||||
|
||||
Build a query string from an array of key value pairs.
|
||||
|
||||
This function can use the return value of `parse()` to build a query
|
||||
string. This function does not modify the provided keys when an array is
|
||||
encountered (like `http_build_query()` would).
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessRemove`
|
||||
|
||||
`public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`
|
||||
|
||||
Remove the items given by the keys, case insensitively from the data.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::copyToStream`
|
||||
|
||||
`public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void`
|
||||
|
||||
Copy the contents of a stream into another stream until the given number
|
||||
of bytes have been read.
|
||||
|
||||
The copy stops if the destination `write()` returns 0, for example a
|
||||
`BufferStream` at its high water mark or a full `DroppingStream`. For a
|
||||
guaranteed full copy, use a normal writable stream such as a file or
|
||||
`php://temp` stream.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::copyToString`
|
||||
|
||||
`public static function copyToString(StreamInterface $stream, int $maxLen = -1): string`
|
||||
|
||||
Copy the contents of a stream into a string until the given number of
|
||||
bytes have been read.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::hash`
|
||||
|
||||
`public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string`
|
||||
|
||||
Calculate a hash of a stream.
|
||||
|
||||
This method reads the entire stream to calculate a rolling hash, based on
|
||||
PHP's `hash_init` functions.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::modifyRequest`
|
||||
|
||||
`public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface`
|
||||
|
||||
Clone and modify a request with the given changes.
|
||||
|
||||
This method is useful for reducing the number of clones needed to mutate
|
||||
a message.
|
||||
|
||||
- method: (string) Changes the HTTP method.
|
||||
- set_headers: (array) Sets the given headers.
|
||||
- remove_headers: (array) Remove the given headers.
|
||||
- body: (mixed) Sets the given body. Present non-null values are converted with
|
||||
`GuzzleHttp\Psr7\Utils::streamFor()`, including scalar values, resources,
|
||||
streams, iterators, callable arrays, closures, invokable objects, and
|
||||
objects with `__toString()`. String inputs remain literal bodies.
|
||||
- uri: (UriInterface) Set the URI.
|
||||
- query: (string) Set the query string value of the URI.
|
||||
- version: (string) Set the protocol version.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::readLine`
|
||||
|
||||
`public static function readLine(StreamInterface $stream, ?int $maxLength = null): string`
|
||||
|
||||
Read a line from the stream up to the maximum allowed buffer length.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::redactUserInfo`
|
||||
|
||||
`public static function redactUserInfo(UriInterface $uri): UriInterface`
|
||||
|
||||
Redact the password in the user info part of a URI.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::streamFor`
|
||||
|
||||
`public static function streamFor(resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource = '', array $options = []): StreamInterface`
|
||||
|
||||
Create a new stream based on the input type.
|
||||
|
||||
Options is an associative array that can contain the following keys:
|
||||
|
||||
- metadata: Array of custom metadata.
|
||||
- size: Size of the stream.
|
||||
|
||||
This method accepts the following `$resource` types:
|
||||
|
||||
- `Psr\Http\Message\StreamInterface`: Returns the value as-is.
|
||||
- `string`: Creates a stream object that uses the given string as the contents.
|
||||
- `resource`: Creates a stream object that wraps the given PHP stream resource.
|
||||
- `Iterator`: If the provided value implements `Iterator`, then a read-only
|
||||
stream object will be created that wraps the given iterable. Each time the
|
||||
stream is read from, data from the iterator will fill a buffer and will be
|
||||
continuously called until the buffer is equal to the requested read size.
|
||||
Subsequent read calls will first read from the buffer and then call `next`
|
||||
on the underlying iterator until it is exhausted.
|
||||
- `object` with `__toString()`: If the object has the `__toString()` method,
|
||||
the object will be cast to a string and then a stream will be returned that
|
||||
uses the string value.
|
||||
- `NULL`: When `null` is passed, an empty stream object is returned.
|
||||
- `callable`: When a callable array, closure, or invokable object is passed and
|
||||
no earlier resource or object rule applies, a read-only stream object will be
|
||||
created that invokes the given callable. The callable is invoked with the
|
||||
suggested number of bytes to read. The callable can return fewer or more bytes
|
||||
than requested, but MUST return `false` or `null` when there is no more data
|
||||
to return. Any additional bytes will be buffered and used in subsequent reads.
|
||||
String inputs are always treated as string bodies, even when they name
|
||||
callable functions.
|
||||
|
||||
```php
|
||||
$stream = GuzzleHttp\Psr7\Utils::streamFor('foo');
|
||||
$stream = GuzzleHttp\Psr7\Utils::streamFor(fopen('/path/to/file', 'r'));
|
||||
|
||||
$generator = function ($bytes) {
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
yield ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$stream = GuzzleHttp\Psr7\Utils::streamFor($generator(100));
|
||||
```
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::tryFopen`
|
||||
|
||||
`public static function tryFopen(string $filename, string $mode): resource`
|
||||
|
||||
Safely opens a PHP stream resource using a filename.
|
||||
|
||||
When fopen fails, PHP normally raises a warning. This function adds an
|
||||
error handler that checks for errors and throws an exception instead.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::tryGetContents`
|
||||
|
||||
`public static function tryGetContents(resource $stream): string`
|
||||
|
||||
Safely gets the contents of a given stream.
|
||||
|
||||
When stream_get_contents fails, PHP normally raises a warning. This
|
||||
function adds an error handler that checks for errors and throws an
|
||||
exception instead.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::uriFor`
|
||||
|
||||
`public static function uriFor(string|UriInterface $uri): UriInterface`
|
||||
|
||||
Returns a UriInterface for the given value.
|
||||
|
||||
This function accepts a string or UriInterface and returns a
|
||||
UriInterface for the given value. If the value is already a
|
||||
UriInterface, it is returned as-is.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\MimeType::fromFilename`
|
||||
|
||||
`public static function fromFilename(string $filename): string|null`
|
||||
|
||||
Determines the mimetype of a file by looking at its extension.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\MimeType::fromExtension`
|
||||
|
||||
`public static function fromExtension(string $extension): string|null`
|
||||
|
||||
Maps a file extensions to a mimetype.
|
||||
|
||||
|
||||
# Additional URI Methods
|
||||
|
||||
Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class,
|
||||
this library also provides additional functionality when working with URIs as static methods.
|
||||
|
||||
## URI Types
|
||||
|
||||
An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or a relative reference.
|
||||
An absolute URI has a scheme. A relative reference is used to express a URI relative to another URI,
|
||||
the base URI. Relative references can be divided into several forms according to
|
||||
[RFC 3986 Section 4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2):
|
||||
|
||||
- network-path references, e.g. `//example.com/path`
|
||||
- absolute-path references, e.g. `/path`
|
||||
- relative-path references, e.g. `subpath`
|
||||
|
||||
The following methods can be used to identify the type of the URI.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::isAbsolute`
|
||||
|
||||
`public static function isAbsolute(UriInterface $uri): bool`
|
||||
|
||||
Whether the URI is absolute, i.e. it has a scheme.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::isNetworkPathReference`
|
||||
|
||||
`public static function isNetworkPathReference(UriInterface $uri): bool`
|
||||
|
||||
Whether the URI is a network-path reference. A relative reference that begins with two slash characters is
|
||||
termed an network-path reference.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference`
|
||||
|
||||
`public static function isAbsolutePathReference(UriInterface $uri): bool`
|
||||
|
||||
Whether the URI is a absolute-path reference. A relative reference that begins with a single slash character is
|
||||
termed an absolute-path reference.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::isRelativePathReference`
|
||||
|
||||
`public static function isRelativePathReference(UriInterface $uri): bool`
|
||||
|
||||
Whether the URI is a relative-path reference. A relative reference that does not begin with a slash character is
|
||||
termed a relative-path reference.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::isSameDocumentReference`
|
||||
|
||||
`public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool`
|
||||
|
||||
Whether the URI is a same-document reference. A same-document reference refers to a URI that is, aside from its
|
||||
fragment component, identical to the base URI. When no base URI is given, only an empty URI reference
|
||||
(apart from its fragment) is considered a same-document reference.
|
||||
|
||||
## URI Components
|
||||
|
||||
Additional methods to work with URI components.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::isDefaultPort`
|
||||
|
||||
`public static function isDefaultPort(UriInterface $uri): bool`
|
||||
|
||||
Whether the URI has the default port of the current scheme. `Psr\Http\Message\UriInterface::getPort` may return null
|
||||
or the standard port. This method can be used independently of the implementation.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::composeComponents`
|
||||
|
||||
`public static function composeComponents($scheme, $authority, $path, $query, $fragment): string`
|
||||
|
||||
Composes a URI reference string from its various components according to
|
||||
[RFC 3986 Section 5.3](https://datatracker.ietf.org/doc/html/rfc3986#section-5.3). Usually this method does not need
|
||||
to be called manually but instead is used indirectly via `Psr\Http\Message\UriInterface::__toString`.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::fromParts`
|
||||
|
||||
`public static function fromParts(array $parts): UriInterface`
|
||||
|
||||
Creates a URI from a hash of [`parse_url`](https://www.php.net/manual/en/function.parse-url.php) components.
|
||||
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::withQueryValue`
|
||||
|
||||
`public static function withQueryValue(UriInterface $uri, $key, $value): UriInterface`
|
||||
|
||||
Creates a new URI with a specific query string value. Any existing query string values that exactly match the
|
||||
provided key are removed and replaced with the given key value pair. A value of null will set the query string
|
||||
key without a value, e.g. "key" instead of "key=value".
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::withQueryValues`
|
||||
|
||||
`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface`
|
||||
|
||||
Creates a new URI with multiple query string values. It has the same behavior as `withQueryValue()` but for an
|
||||
associative array of key => value.
|
||||
|
||||
### `GuzzleHttp\Psr7\Uri::withoutQueryValue`
|
||||
|
||||
`public static function withoutQueryValue(UriInterface $uri, $key): UriInterface`
|
||||
|
||||
Creates a new URI with a specific query string value removed. Any existing query string values that exactly match the
|
||||
provided key are removed.
|
||||
|
||||
## Cross-Origin Detection
|
||||
|
||||
`GuzzleHttp\Psr7\UriComparator` provides methods to determine if a modified URL should be considered cross-origin.
|
||||
|
||||
### `GuzzleHttp\Psr7\UriComparator::isCrossOrigin`
|
||||
|
||||
`public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool`
|
||||
|
||||
Determines if a modified URL should be considered cross-origin with respect to an original URL.
|
||||
|
||||
Two URLs are cross-origin when their scheme, host, or effective port differ. Host comparison is case-insensitive, and missing ports use the default port for `http` or `https`. Other schemes do not receive implicit default ports.
|
||||
|
||||
This helper only compares URI origins. It does not implement redirect handling or credential policy.
|
||||
|
||||
## Reference Resolution
|
||||
|
||||
`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according
|
||||
to [RFC 3986 Section 5](https://datatracker.ietf.org/doc/html/rfc3986#section-5). This is for example also what web
|
||||
browsers do when resolving a link in a website based on the current request URI.
|
||||
|
||||
### `GuzzleHttp\Psr7\UriResolver::resolve`
|
||||
|
||||
`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface`
|
||||
|
||||
Converts the relative URI into a new URI that is resolved against the base URI.
|
||||
|
||||
### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`
|
||||
|
||||
`public static function removeDotSegments(string $path): string`
|
||||
|
||||
Removes dot segments from a path and returns the new path according to
|
||||
[RFC 3986 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4).
|
||||
|
||||
### `GuzzleHttp\Psr7\UriResolver::relativize`
|
||||
|
||||
`public static function relativize(UriInterface $base, UriInterface $target): UriInterface`
|
||||
|
||||
Returns the target URI as a relative reference from the base URI. This method is the counterpart to resolve():
|
||||
|
||||
```php
|
||||
(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
|
||||
```
|
||||
|
||||
One use-case is to use the current request URI as base URI and then generate relative links in your documents
|
||||
to reduce the document size or offer self-contained downloadable document archives.
|
||||
|
||||
```php
|
||||
$base = new Uri('http://example.com/a/b/');
|
||||
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
|
||||
echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
|
||||
echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
|
||||
echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
|
||||
```
|
||||
|
||||
## Normalization and Comparison
|
||||
|
||||
`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs according to
|
||||
[RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6).
|
||||
|
||||
### `GuzzleHttp\Psr7\UriNormalizer::normalize`
|
||||
|
||||
`public static function normalize(UriInterface $uri, $flags = self::PRESERVING_NORMALIZATIONS): UriInterface`
|
||||
|
||||
Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface.
|
||||
This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask
|
||||
of normalizations to apply. The following normalizations are available:
|
||||
|
||||
- `UriNormalizer::PRESERVING_NORMALIZATIONS`
|
||||
|
||||
Default normalizations which only include the ones that preserve semantics.
|
||||
|
||||
- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING`
|
||||
|
||||
All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized.
|
||||
|
||||
Example: `http://example.org/a%c2%b1b` → `http://example.org/a%C2%B1b`
|
||||
|
||||
- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS`
|
||||
|
||||
Decodes percent-encoded octets of unreserved characters. For consistency, percent-encoded octets in the ranges of
|
||||
ALPHA (%41–%5A and %61–%7A), DIGIT (%30–%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should
|
||||
not be created by URI producers and, when found in a URI, should be decoded to their corresponding unreserved
|
||||
characters by URI normalizers.
|
||||
|
||||
Example: `http://example.org/%7Eusern%61me/` → `http://example.org/~username/`
|
||||
|
||||
- `UriNormalizer::CONVERT_EMPTY_PATH`
|
||||
|
||||
Converts the empty path to "/" for http and https URIs.
|
||||
|
||||
Example: `http://example.org` → `http://example.org/`
|
||||
|
||||
- `UriNormalizer::REMOVE_DEFAULT_HOST`
|
||||
|
||||
Removes the default host of the given URI scheme from the URI. Only the "file" scheme defines the default host
|
||||
"localhost". All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are equivalent according to
|
||||
RFC 3986.
|
||||
|
||||
Example: `file://localhost/myfile` → `file:///myfile`
|
||||
|
||||
- `UriNormalizer::REMOVE_DEFAULT_PORT`
|
||||
|
||||
Removes the default port of the given URI scheme from the URI.
|
||||
|
||||
Example: `http://example.org:80/` → `http://example.org/`
|
||||
|
||||
- `UriNormalizer::REMOVE_DOT_SEGMENTS`
|
||||
|
||||
Removes unnecessary dot-segments. Dot-segments in relative-path references are not removed as it would
|
||||
change the semantics of the URI reference.
|
||||
|
||||
Example: `http://example.org/../a/b/../c/./d.html` → `http://example.org/a/c/d.html`
|
||||
|
||||
- `UriNormalizer::REMOVE_DUPLICATE_SLASHES`
|
||||
|
||||
Paths which include two or more adjacent slashes are converted to one. Webservers usually ignore duplicate slashes
|
||||
and treat those URIs equivalent. But in theory those URIs do not need to be equivalent. So this normalization
|
||||
may change the semantics. Encoded slashes (%2F) are not removed.
|
||||
|
||||
Example: `http://example.org//foo///bar.html` → `http://example.org/foo/bar.html`
|
||||
|
||||
- `UriNormalizer::SORT_QUERY_PARAMETERS`
|
||||
|
||||
Sort query parameters with their values in alphabetical order. However, the order of parameters in a URI may be
|
||||
significant (this is not defined by the standard). So this normalization is not safe and may change the semantics
|
||||
of the URI.
|
||||
|
||||
Example: `?lang=en&article=fred` → `?article=fred&lang=en`
|
||||
|
||||
### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent`
|
||||
|
||||
`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS): bool`
|
||||
|
||||
Whether two URIs can be considered equivalent. Both URIs are normalized automatically before comparison with the given
|
||||
`$normalizations` bitmask. The method also accepts relative URI references and returns true when they are equivalent.
|
||||
This of course assumes they will be resolved against the same base URI. If this is not the case, determination of
|
||||
equivalence or difference of relative references does not mean anything.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [PSR-7 Messages](docs/psr-7-messages.md)
|
||||
- [Streams and Decorators](docs/streams-and-decorators.md)
|
||||
- [URI Helpers](docs/uri-helpers.md)
|
||||
- [PSR-17 Factories](docs/psr-17-factories.md)
|
||||
- [Message Helpers](docs/message-helpers.md)
|
||||
- [Diagnostic Values](docs/diagnostic-values.md)
|
||||
- [Header and Query Helpers](docs/header-and-query-helpers.md)
|
||||
- [Stream Helpers](docs/stream-helpers.md)
|
||||
- [URI and MIME Helpers](docs/uri-and-mime-helpers.md)
|
||||
- [Upgrade Guide](UPGRADING.md)
|
||||
- [Changelog](CHANGELOG.md)
|
||||
|
||||
## Security
|
||||
|
||||
If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/psr7/security/policy) for more information.
|
||||
|
||||
If you discover a security vulnerability within this package, please send an
|
||||
email to security@tidelift.com. All security vulnerabilities will be promptly
|
||||
addressed. Please do not disclose security-related issues publicly until a fix
|
||||
has been announced. Please see
|
||||
[Security Policy](https://github.com/guzzle/psr7/security/policy) for more
|
||||
information.
|
||||
|
||||
## License
|
||||
|
||||
Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information.
|
||||
|
||||
Guzzle is made available under the MIT License (MIT). Please see
|
||||
[License File](LICENSE) for more information.
|
||||
|
||||
## For Enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription
|
||||
|
||||
The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-psr7?utm_source=packagist-guzzlehttp-psr7&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
The maintainers of Guzzle and thousands of other packages are working with
|
||||
Tidelift to deliver commercial support and maintenance for the open source
|
||||
dependencies you use to build your applications. Save time, reduce risk, and
|
||||
improve code health, while paying the maintainers of the exact dependencies you
|
||||
use.
|
||||
[Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-psr7?utm_source=packagist-guzzlehttp-psr7&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
|
|
|
|||
886
vendor/guzzlehttp/psr7/UPGRADING.md
vendored
886
vendor/guzzlehttp/psr7/UPGRADING.md
vendored
|
|
@ -1,6 +1,892 @@
|
|||
Guzzle PSR-7 Upgrade Guide
|
||||
==========================
|
||||
|
||||
2.x to 3.0
|
||||
----------
|
||||
|
||||
Guzzle PSR-7 3.0 is a major release that raises the minimum PHP version,
|
||||
updates to the PSR-7 v2 interfaces, validates header values more strictly,
|
||||
preserves explicit request method casing, and rejects several invalid URI,
|
||||
request, response, upload, query, stream, and multipart values that 2.x
|
||||
previously accepted or cast.
|
||||
|
||||
#### PHP Version and Dependencies
|
||||
|
||||
Guzzle PSR-7 3.0 requires PHP `^7.4 || ^8.0`. Guzzle PSR-7 2.x supported PHP
|
||||
`^7.2.5 || ^8.0`.
|
||||
|
||||
If your application still supports PHP 7.2 or 7.3, continue using Guzzle PSR-7
|
||||
2.x until your minimum PHP version is raised.
|
||||
|
||||
Guzzle PSR-7 3.0 requires `psr/http-message:^2.0` and
|
||||
`psr/http-factory:^1.1`. Guzzle PSR-7 2.x supported
|
||||
`psr/http-message:^1.1 || ^2.0` and `psr/http-factory:^1.0`. If your dependency
|
||||
constraints pin `psr/http-message` to v1, update them before upgrading.
|
||||
|
||||
Guzzle PSR-7 no longer depends on `ralouphie/getallheaders` and no longer
|
||||
provides a transitive global `getallheaders()` polyfill.
|
||||
`ServerRequest::fromGlobals()` continues to collect request headers internally.
|
||||
Applications that call `getallheaders()` directly on SAPIs where PHP does not
|
||||
provide it should require their own polyfill.
|
||||
|
||||
#### Header Values
|
||||
|
||||
Header values must now be strings or non-empty arrays of strings. Empty strings
|
||||
remain valid explicit header values, but empty arrays, `null`, `false`, integers,
|
||||
floats, and other non-string values are no longer cast or accepted.
|
||||
|
||||
```php
|
||||
// 2.x, no longer accepted in 3.0
|
||||
$response = $response->withHeader('Api-Version', 1);
|
||||
$response = $response->withHeader('Empty-List', []);
|
||||
|
||||
// 3.0
|
||||
$response = $response->withHeader('Api-Version', '1');
|
||||
$response = $response->withHeader('Empty-Value', '');
|
||||
```
|
||||
|
||||
Use `withoutHeader()` to remove a header.
|
||||
|
||||
#### Request Method Casing
|
||||
|
||||
Request methods passed explicitly to `Request`, `ServerRequest`, `withMethod()`,
|
||||
`Message::parseRequest()`, and the PSR-17 factories are no longer uppercased.
|
||||
PSR-7 treats method names as case-sensitive, so these APIs now preserve the
|
||||
method exactly as provided. If your application requires uppercase methods,
|
||||
normalize methods before constructing or modifying requests.
|
||||
|
||||
`ServerRequest::fromGlobals()` is the compatibility-oriented exception. It
|
||||
continues to uppercase string `REQUEST_METHOD` values read from PHP server
|
||||
globals, matching Guzzle PSR-7 2.x and common server request behavior. This
|
||||
normalization only applies when hydrating from globals; it does not apply to
|
||||
methods passed explicitly to constructors, factories, or `withMethod()`.
|
||||
|
||||
```php
|
||||
// 2.x
|
||||
$request = new Request('get', '/');
|
||||
$request->getMethod(); // GET
|
||||
|
||||
// 3.0
|
||||
$request = new Request('get', '/');
|
||||
$request->getMethod(); // get
|
||||
|
||||
// 3.0, server globals
|
||||
$_SERVER['REQUEST_METHOD'] = 'post';
|
||||
$request = ServerRequest::fromGlobals();
|
||||
$request->getMethod(); // POST
|
||||
```
|
||||
|
||||
#### Native PSR-7 Parameter Types
|
||||
|
||||
Guzzle PSR-7 3.0 requires the argument types documented by PSR-7 more strictly.
|
||||
It adds the native parameter types from `psr/http-message` v2. Code passing
|
||||
invalid argument types may now receive PHP `TypeError` exceptions instead of
|
||||
package-specific `InvalidArgumentException` exceptions or implicit casts.
|
||||
|
||||
Native parameter type changes include:
|
||||
|
||||
- `MessageInterface::withProtocolVersion()` now requires `string`.
|
||||
- Message header names now require `string`.
|
||||
- `RequestInterface::withRequestTarget()` and `withMethod()` now require `string`.
|
||||
- `RequestInterface::withUri()` now requires `bool` for `$preserveHost`.
|
||||
- `ResponseInterface::withStatus()` now requires `int` status codes and `string` reason phrases.
|
||||
- Server request attribute names now require `string`.
|
||||
- `UriInterface::withPort()` now requires `int|null`.
|
||||
- URI scheme, user info, host, path, query, and fragment mutators now require strings.
|
||||
- Stream `seek()`, `read()`, `write()`, and `getMetadata()` now require their PSR-7 v2 parameter types.
|
||||
- `UploadedFileInterface::moveTo()` now requires a string target path.
|
||||
|
||||
Update callers to pass values of the documented type before calling these
|
||||
methods:
|
||||
|
||||
```php
|
||||
// 2.x, no longer supported in 3.0
|
||||
$response = $response->withStatus('201');
|
||||
$uri = $uri->withPort('8080');
|
||||
|
||||
// 3.0
|
||||
$response = $response->withStatus(201);
|
||||
$uri = $uri->withPort(8080);
|
||||
```
|
||||
|
||||
#### Request Modification Changes
|
||||
|
||||
`Utils::modifyRequest()` now validates recognized change values before applying
|
||||
request modifications. Unknown change keys are still ignored. Explicit `null`
|
||||
values are no longer treated as omitted recognized changes; omit the key instead.
|
||||
|
||||
Recognized change values must use the documented types:
|
||||
|
||||
- `method`: `string`
|
||||
- `uri`: `UriInterface`
|
||||
- `query`: `string`
|
||||
- `version`: `string`
|
||||
- `body`: `resource|string|StreamInterface|callable|\Iterator|\Stringable`
|
||||
- `set_headers`: `array<array-key, string|non-empty-array<array-key, string>>`
|
||||
- `remove_headers`: `array<array-key, string|int>`
|
||||
|
||||
When a `uri` change contains a host, the synthesized `Host` header now
|
||||
includes any non-default URI port, matching the `Request` constructor. 2.x
|
||||
omitted port zero and every port on schemes other than HTTP and HTTPS.
|
||||
|
||||
#### Uploaded Files
|
||||
|
||||
`ServerRequestInterface::withUploadedFiles()` now rejects invalid nested upload
|
||||
trees. Every leaf must be an `UploadedFileInterface` instance.
|
||||
|
||||
`ServerRequest::normalizeFiles()` and `ServerRequest::fromGlobals()` now reject
|
||||
malformed `$_FILES` specifications earlier. Single-file specifications must
|
||||
contain non-null `tmp_name`, `size`, and `error` values. Single-file and nested
|
||||
file `size` values and `error` values must be non-negative PHP integers;
|
||||
numeric strings are no longer cast. If PHP supplies an upload size as a string
|
||||
because the byte count cannot fit in `PHP_INT_MAX`, it is rejected rather than
|
||||
truncated or cast. Nested specifications must provide `tmp_name`, `size`, and
|
||||
`error` as arrays. Every key in `tmp_name` must also exist in `size` and
|
||||
`error`; additional metadata entries without a matching `tmp_name` entry are
|
||||
ignored. When nested `name` or `type` metadata is provided, it must also be an
|
||||
array.
|
||||
|
||||
If your tests or adapters build `$_FILES` arrays manually, populate the full
|
||||
shape or create `UploadedFile` instances directly.
|
||||
|
||||
```php
|
||||
// 2.x, no longer accepted in 3.0
|
||||
$files = ['file' => ['tmp_name' => '/tmp/php123', 'error' => '0']];
|
||||
|
||||
// 3.0
|
||||
$files = ['file' => ['tmp_name' => '/tmp/php123', 'size' => 123, 'error' => UPLOAD_ERR_OK]];
|
||||
```
|
||||
|
||||
For stream-backed uploads, `UploadedFile::moveTo()` now rewinds seekable streams
|
||||
before copying them. If application code reads from a seekable uploaded stream
|
||||
before calling `moveTo()`, 3.0 writes the full stream contents to the target
|
||||
instead of only the unread suffix. Non-seekable stream-backed uploads continue to
|
||||
copy from their current position because consumed bytes cannot be replayed.
|
||||
|
||||
#### Parsed Body Values
|
||||
|
||||
`ServerRequestInterface::withParsedBody()` now rejects values other than
|
||||
`array`, `object`, or `null`.
|
||||
|
||||
```php
|
||||
// 2.x, no longer accepted in 3.0
|
||||
$request = $request->withParsedBody('name=value');
|
||||
|
||||
// 3.0
|
||||
$request = $request->withParsedBody(['name' => 'value']);
|
||||
```
|
||||
|
||||
#### URI Host and Scheme Validation
|
||||
|
||||
URI hosts containing URI delimiters, backslashes, embedded ports passed to
|
||||
`withHost()`, malformed IP-literal brackets, unbracketed IPv6, or other
|
||||
malformed host forms are no longer accepted. URI schemes containing whitespace
|
||||
or control characters are also no longer accepted.
|
||||
|
||||
If you previously passed a host and port together to `withHost()`, split them
|
||||
between `withHost()` and `withPort()`:
|
||||
|
||||
```php
|
||||
// 2.x, no longer accepted in 3.0
|
||||
$uri = $uri->withHost('example.com:8080');
|
||||
|
||||
// 3.0
|
||||
$uri = $uri->withHost('example.com')->withPort(8080);
|
||||
```
|
||||
|
||||
Normal URI strings with ports are still supported:
|
||||
|
||||
```php
|
||||
$uri = new Uri('https://example.com:8080/path');
|
||||
```
|
||||
|
||||
URI parsing now accepts bracketed IPv6 and IPvFuture hosts consistently with
|
||||
`withHost()` for userinfo and network-path authorities such as
|
||||
`http://user@[::1]/`, `//[::1]`, and `http://[v7.a:b]/`. Invalid
|
||||
delimiter-free bracketed literals, such as `[gggg::1]`, are also reported with
|
||||
the intact host. Bracketed literals containing authority/path delimiters, such
|
||||
as `[a@b]` or `[v1.a/b]`, still reject after fallback parsing and may report the
|
||||
mangled parsed host.
|
||||
|
||||
Userinfo before a bracketed IP-literal host is now percent-encoded ahead of
|
||||
parsing, so raw control bytes yield encoded userinfo, such as `us%01er`,
|
||||
instead of a silently mutated value, and raw DEL bytes in bracketed hosts are
|
||||
rejected instead of parsed as a mutated host. Consistent with registered-name
|
||||
authorities, such userinfo containing invalid UTF-8 is now rejected,
|
||||
percent-sequences such as `u%41` are preserved rather than decoded, and a
|
||||
literal `+` is preserved rather than decoded to a space.
|
||||
|
||||
Only an optional numeric port (which may be empty) and a path, query, or
|
||||
fragment may follow a bracketed IP-literal host. Trailing bytes that are
|
||||
neither, such as `http://[::1]:80@evil/` or `http://[::1]:80x/`, are now
|
||||
rejected instead of being reparsed into a different host.
|
||||
|
||||
Parsing still URL-decodes bracketed IP-literal hosts before validation
|
||||
(registered-name hosts round-trip unchanged), so a literal `+` in a bracketed
|
||||
IP-literal decodes to a space and is rejected: `withHost('[v1.fe80::a+en1]')`
|
||||
accepts the literal while parsing `http://[v1.fe80::a+en1]/` rejects it.
|
||||
Percent-encoding inside a bracketed IP-literal is now rejected during parsing as
|
||||
well, since RFC 3986 IP-literals contain no percent-encoding, so
|
||||
`http://[%3A%3A1]/` no longer decodes to `[::1]`; this matches `withHost()` and
|
||||
`Rfc3986::isValidHost()`.
|
||||
|
||||
Percent-encoded octets in a registered-name host are normalized to uppercase
|
||||
hex, so a host such as `a%c3%a9b` is represented as `a%C3%A9b`. Malformed
|
||||
percent sequences and percent-encoded octets that decode to a byte forbidden in
|
||||
a host, such as `ex%zz` and `%2fhost`, are rejected.
|
||||
|
||||
`Uri::fromParts()` accepts integer and decimal digit string ports, but floats and
|
||||
other port values are no longer cast.
|
||||
|
||||
Common host forms such as `localhost`, single-label hosts, underscores, Unicode
|
||||
hosts, valid IPv6 literals, and normal host and port URI strings remain
|
||||
supported.
|
||||
|
||||
URI schemes must now match RFC 3986 syntax and begin with a letter.
|
||||
|
||||
```php
|
||||
// 2.x, no longer accepted in 3.0
|
||||
$uri = (new Uri())->withScheme('0');
|
||||
|
||||
// 3.0
|
||||
$uri = (new Uri())->withScheme('https');
|
||||
```
|
||||
|
||||
The stricter validation also applies when a request is created or modified from
|
||||
a custom `UriInterface` implementation and its host is used to generate or
|
||||
update a `Host` header.
|
||||
|
||||
`ServerRequest::getUriFromGlobals()` now falls back to `SERVER_NAME`, then
|
||||
`SERVER_ADDR`, then the existing default host behavior for malformed
|
||||
`HTTP_HOST` values. It also rejects zero-port `HTTP_HOST` authorities and
|
||||
malformed `SERVER_PORT` values when fallback authority reconstruction needs the
|
||||
server port. When `REQUEST_URI` is absolute-form or CONNECT authority-form and
|
||||
supplies a valid authority, that authority is used before fallback `SERVER_PORT`
|
||||
validation. Origin-form, asterisk-form, missing `REQUEST_URI`, and fallback
|
||||
reconstruction paths still reject malformed `SERVER_PORT` values when fallback
|
||||
authority reconstruction needs the server port.
|
||||
|
||||
Absolute-form `REQUEST_URI` userinfo is removed when reconstructing the URI and
|
||||
request target from globals, including empty userinfo such as
|
||||
`http://@example.com/`. The host after the last raw `@` remains the URI host.
|
||||
|
||||
`Message::parseRequest()` now applies 3.0 authority rules when deriving a URI
|
||||
from an origin-form or asterisk-form request target. Host ports with leading
|
||||
zeroes are normalized for URI reconstruction, and port zero is rejected.
|
||||
It also rejects duplicate `Host` field lines, including case-insensitive
|
||||
duplicates. Any present raw `Host` field is validated before returning a parsed
|
||||
request, even when the request target supplies the URI authority, such as
|
||||
absolute-form and CONNECT requests. Valid `Host` values may still differ from
|
||||
the absolute-form or CONNECT request-target authority.
|
||||
|
||||
For server globals, applications that need to reject malformed inbound `Host`
|
||||
headers should validate the original server parameters before calling
|
||||
`getUriFromGlobals()` or inspect them afterward.
|
||||
|
||||
`Message::parseRequest()` now applies the same HTTP authority validation to
|
||||
absolute-form request targets. A zero or padded-zero port
|
||||
(`http://example.com:0/admin`) is rejected instead of producing a request
|
||||
whose synthesized `Host` header the same parser rejects elsewhere.
|
||||
Absolute-form targets with no URI host, such as `file:///etc/passwd`, are
|
||||
also rejected instead of producing a hostless request URI.
|
||||
|
||||
Absolute-form targets whose authority contains userinfo are also rejected,
|
||||
including empty userinfo such as `http://@example.com/`. RFC 9110 deprecates
|
||||
userinfo in http(s) target URIs and directs recipients to treat its presence
|
||||
as an error; `Host` headers and CONNECT targets already reject it. These
|
||||
rules apply to absolute-form targets of every scheme.
|
||||
`ServerRequest::fromGlobals()` is unchanged and continues to strip
|
||||
`REQUEST_URI` userinfo.
|
||||
|
||||
URI hosts now validate percent-encoding. Malformed sequences such as `ex%zz`,
|
||||
and percent-encoded octets that decode to bytes the raw host grammar already
|
||||
rejects (controls, space, DEL, `/`, `?`, `#`, `@`, `\`, `:`, `[`, `]`, and `%`
|
||||
itself) throw `MalformedUriException` from URI parsing and
|
||||
`InvalidArgumentException` from `Uri::withHost()`, and are rejected wherever
|
||||
hosts are validated, including `Host` headers and request targets in
|
||||
`Message::parseRequest()`. WHATWG-conformant browsers reject all of these hosts;
|
||||
curl rejects them too, except encoded DEL (`%7F`), which it decodes and forwards
|
||||
to name resolution. Other percent-encoded octets, including UTF-8 data such as
|
||||
`a%C3%A9b`, remain accepted and are normalized to uppercase hex.
|
||||
|
||||
IPv6 hosts are now canonicalized to their RFC 5952 form when a URI is
|
||||
constructed, so `getHost()`, `getAuthority()`, and `(string) $uri` return the
|
||||
canonical spelling and synthesized `Host` headers use it. Leading zeros are
|
||||
suppressed, hexadecimal fields are lowercase, and the longest run of two or
|
||||
more zero fields is collapsed with `::`. Embedded dotted-decimal notation
|
||||
follows the rendering policy of BIND-derived `inet_ntop()` implementations and
|
||||
curl 8.11 and newer: exactly the IPv4-mapped (`::ffff:0:0/96`) and deprecated
|
||||
IPv4-compatible (`::/96`) layouts use it, while other embedded-IPv4 forms,
|
||||
including translated (NAT64) well-known prefixes such as `64:ff9b::/96`
|
||||
(RFC 6052), serialize in pure hexadecimal fields.
|
||||
|
||||
```php
|
||||
// 2.x preserved the spelling as given
|
||||
(string) new Uri('http://[0:0:0:0:0:0:0:1]/'); // http://[0:0:0:0:0:0:0:1]/
|
||||
|
||||
// 3.0
|
||||
(string) new Uri('http://[0:0:0:0:0:0:0:1]/'); // http://[::1]/
|
||||
(string) new Uri('http://[::FFFF:7F00:1]/'); // http://[::ffff:127.0.0.1]/
|
||||
(string) new Uri('http://[2001:db8:3:4::192.0.2.33]/'); // http://[2001:db8:3:4::c000:221]/
|
||||
```
|
||||
|
||||
Applications that persist URI strings, for example as cache keys, will observe
|
||||
the new canonical form for previously non-canonical IPv6 spellings. Equivalent
|
||||
spellings of the same address now compare as same-origin in
|
||||
`UriComparator::isCrossOrigin()`, which canonicalizes bracketed IPv6 literals
|
||||
from any PSR-7 implementation before comparing hosts, and as equivalent in
|
||||
`UriNormalizer::isEquivalent()`. The new
|
||||
`UriNormalizer::CANONICALIZE_IPV6_HOST` flag, included in the default
|
||||
`UriNormalizer::PRESERVING_NORMALIZATIONS`, requests the canonical host from
|
||||
other PSR-7 implementations through `withHost()` and keeps the result only
|
||||
when the returned `getHost()` exactly matches the requested spelling; a
|
||||
nonexact result leaves that step unchanged while other selected normalizations
|
||||
still apply, and setter exceptions propagate. `UriComparator` does not share
|
||||
this limitation, since it canonicalizes the extracted host text directly. The
|
||||
public helper `Rfc3986::canonicalizeIpv6()` exposes the underlying
|
||||
transformation.
|
||||
|
||||
#### Request Host Synchronization
|
||||
|
||||
`Request::withUri()` now applies PSR-7 Host header synchronization before using
|
||||
the same-URI no-op shortcut. When the provided URI is the same object already
|
||||
attached to the request, the method may still return a new request if the URI
|
||||
has a host and the current Host header is missing, empty, or stale.
|
||||
|
||||
With `$preserveHost = true`, a non-empty Host header is still preserved. Missing
|
||||
or empty Host headers are treated as absent and are populated from the URI when
|
||||
the URI contains a host.
|
||||
|
||||
```php
|
||||
$request = (new Request('GET', 'http://example.com:8124/'))->withoutHeader('Host');
|
||||
|
||||
$updated = $request->withUri($request->getUri());
|
||||
|
||||
$updated->getHeaderLine('Host'); // example.com:8124
|
||||
```
|
||||
|
||||
If your application intentionally sends an empty or stale Host header, set it
|
||||
after calling `withUri()` or preserve a non-empty Host header explicitly.
|
||||
|
||||
`Message::toString()` now applies the same URI host and port synthesis when
|
||||
serializing a request without a `Host` header. Generated `Host` lines include
|
||||
non-null URI ports.
|
||||
|
||||
`Message::toString()` also validates the host it synthesizes from the request
|
||||
URI and throws `InvalidArgumentException` for an invalid host, closing a header-
|
||||
injection vector. This affects only a custom `UriInterface` implementation that
|
||||
returns an invalid host when the request has no stored `Host` header; first-
|
||||
party `Uri` instances always carry a valid host and are unaffected.
|
||||
|
||||
#### URI Paths and Request Targets
|
||||
|
||||
`Uri::getPath()` now normalizes multiple leading slashes to one slash when
|
||||
returning the path in isolation. Casting the URI to string still preserves the
|
||||
original URI representation.
|
||||
|
||||
```php
|
||||
$uri = new Uri('http://example.org//valid///path');
|
||||
|
||||
$uri->getPath(); // /valid///path
|
||||
(string) $uri; // http://example.org//valid///path
|
||||
```
|
||||
|
||||
`Request::getRequestTarget()` applies the same normalization for URI-derived
|
||||
origin-form request targets.
|
||||
|
||||
Reference resolution and normalization (`UriResolver`, `UriNormalizer`, and
|
||||
`Uri::isSameDocumentReference()`) operate on the raw path from the URI string
|
||||
form and are therefore unaffected by this normalization.
|
||||
|
||||
Authority-less `file` URIs with rootless paths now serialize without the `//`
|
||||
authority separator: `(string) new Uri('file:foo/bar')` returns `file:foo/bar`
|
||||
instead of `file://foo/bar`, which reparses with host `foo` and path `/bar`.
|
||||
Rooted paths such as `file:///myfile` keep their existing serialization.
|
||||
|
||||
Authority-less `file` URIs with empty paths now serialize as `file:` instead of
|
||||
`file://`, which `new Uri()` itself rejects as unparseable. This affects
|
||||
degenerate URIs such as `new Uri('file:')` or
|
||||
`Uri::fromParts(['scheme' => 'file'])`; the serialization of every file URI
|
||||
with a non-empty path is unchanged.
|
||||
|
||||
`UriResolver::removeDotSegments()` now applies RFC 3986 Section 5.2.4 to `..`
|
||||
segments above the root of an absolute path: excess `..` segments no longer
|
||||
consume the root, so a following empty segment is preserved. Resolving `/..//a`
|
||||
against `http://example.org/base` yields `http://example.org//a` where 2.x
|
||||
produced `http://example.org/a`. When the resulting URI has no authority,
|
||||
`UriResolver::resolve()` and `UriNormalizer::normalize()` serialize such a
|
||||
`//`-leading path with a `/.` prefix (`mailto:/.//a`), like the WHATWG URL
|
||||
Standard, instead of collapsing the slashes or throwing.
|
||||
|
||||
#### URI Reference Relativization
|
||||
|
||||
`UriResolver::relativize()` now returns a network-path reference (for example
|
||||
`//example.com`) when the target URI has the same authority as the base URI
|
||||
but an empty path that no other relative reference round-trips. No path
|
||||
reference can express such a target, as resolving one always produces a path
|
||||
of at least `/`, and an empty reference would keep the base path or inherit
|
||||
the base query or fragment. The returned reference resolves back to the
|
||||
exact target string, restoring the documented round-trip guarantee for these
|
||||
targets.
|
||||
|
||||
```php
|
||||
$base = new Uri('http://example.com/a');
|
||||
$target = new Uri('http://example.com');
|
||||
|
||||
// 2.x
|
||||
(string) UriResolver::relativize($base, $target); // ../
|
||||
// which resolved back to http://example.com/
|
||||
|
||||
// 3.0
|
||||
(string) UriResolver::relativize($base, $target); // //example.com
|
||||
```
|
||||
|
||||
The same applies when the base URI has a query or fragment component that an
|
||||
empty relative reference would otherwise inherit. When the base URI has an
|
||||
empty path as well and nothing would be inherited, shorter references such
|
||||
as the empty reference, `#fragment` or `?query` are still returned.
|
||||
|
||||
`relativize()` also no longer returns the empty reference when the target
|
||||
path equals the base path but the base has a fragment the target lacks, as
|
||||
the empty reference would reintroduce that fragment. A relative-path
|
||||
reference, or a query reference when the target has a query, is returned
|
||||
instead. When the relative-path reference would be a single path segment
|
||||
containing a colon, which would be mistaken for a scheme name, it is
|
||||
prefixed with `./` (for example `./a:b`); 2.x threw a `MalformedUriException`
|
||||
for such targets when the base had a query the target lacked.
|
||||
|
||||
#### HTTP Start-line Parsing
|
||||
|
||||
`Message::parseRequest()` and `Message::parseResponse()` now validate HTTP
|
||||
start-line fields more strictly. Malformed request methods, request targets
|
||||
containing whitespace or control characters, malformed protocol versions,
|
||||
invalid response status codes, invalid response spacing, and reason phrases
|
||||
containing invalid control characters now throw `InvalidArgumentException`.
|
||||
|
||||
`Request` and `Response` constructors and mutators apply the same validation to
|
||||
protocol versions, request targets, status codes, and reason phrases. If you
|
||||
parse raw HTTP messages or construct messages from partially validated input,
|
||||
normalize or reject invalid values before passing them to Guzzle PSR-7.
|
||||
|
||||
```php
|
||||
// 2.x-style tolerant input, no longer accepted in 3.0
|
||||
Message::parseRequest("GET /foo bar HTTP/1.1\r\nHost: example.com\r\n\r\n");
|
||||
new Response(200, [], null, 'HTTP/1.1');
|
||||
|
||||
// 3.0
|
||||
Message::parseRequest("GET /foo%20bar HTTP/1.1\r\nHost: example.com\r\n\r\n");
|
||||
new Response(200, [], null, '1.1');
|
||||
```
|
||||
|
||||
`ServerRequest::fromGlobals()` applies the same validation to the
|
||||
`REQUEST_METHOD` and `SERVER_PROTOCOL` server values. Malformed values that 2.x
|
||||
hydrated, such as the `SERVER_PROTOCOL` value `INCLUDED` that Apache sets for
|
||||
server-side include subrequests, now throw `InvalidArgumentException`. Sanitize
|
||||
`$_SERVER` before calling `fromGlobals()` if such environments must be
|
||||
tolerated.
|
||||
|
||||
`Request::withRequestTarget('')` throws `InvalidArgumentException`; omit the
|
||||
explicit request target to derive `/` or the URI-derived target automatically.
|
||||
|
||||
`Message::parseMessage()` no longer unfolds folded HTTP/1.0 messages whose
|
||||
start line carries control bytes in the request target; such messages now
|
||||
throw the obsolete-line-folding `InvalidArgumentException`.
|
||||
`Message::parseRequest()` and `Message::parseResponse()` rejected these
|
||||
messages either way.
|
||||
|
||||
#### Query Builder Values
|
||||
|
||||
`Query::build()` now rejects unsupported values instead of relying on PHP string
|
||||
casts. Query values must be scalar, `null`, stringable objects, or flat arrays of
|
||||
those values.
|
||||
|
||||
Nested arrays, resources, and objects without `__toString()` now throw
|
||||
`InvalidArgumentException`.
|
||||
|
||||
```php
|
||||
// Before: could produce warnings or silently mangle the value.
|
||||
Query::build(['filter' => ['name' => ['value']]]);
|
||||
|
||||
// After: use explicit query keys for nested query shapes.
|
||||
Query::build(['filter[name]' => 'value']);
|
||||
```
|
||||
|
||||
Flat arrays are still supported for repeated query parameters:
|
||||
|
||||
```php
|
||||
Query::build(['tag' => ['a', 'b']]);
|
||||
// tag=a&tag=b
|
||||
```
|
||||
|
||||
`Uri::withQueryValues()` is stricter than `Query::build()` and requires `string`
|
||||
or `null` values; cast numeric and boolean query values to string.
|
||||
|
||||
#### Non-string Scalar Bodies
|
||||
|
||||
`Utils::streamFor()` and message bodies no longer accept `int`, `float`, or
|
||||
`bool` values. Cast them to strings first.
|
||||
|
||||
```php
|
||||
// 2.x, no longer accepted in 3.0
|
||||
$response = new Response(200, [], 404);
|
||||
|
||||
// 3.0
|
||||
$response = new Response(200, [], '404');
|
||||
```
|
||||
|
||||
#### PumpStream Source Callables
|
||||
|
||||
`PumpStream` source callables must now return a non-empty string when producing
|
||||
data. Returning an empty string now throws `RuntimeException` instead of being
|
||||
retried indefinitely. Return `false` or `null` to signal EOF.
|
||||
|
||||
If your callable used `''` to mean "temporarily no data", update it to wait
|
||||
until data is available, return a non-empty string, or return `false` or `null`
|
||||
when the stream is complete.
|
||||
|
||||
#### Iterator-backed Streams
|
||||
|
||||
`Utils::streamFor()` now validates values yielded by `Iterator` instances before
|
||||
passing them to the internal `PumpStream`. Strings, integers, finite floats,
|
||||
booleans, `null`, and stringable objects are converted to string chunks.
|
||||
Non-finite floats, arrays, resources, and non-stringable objects now throw
|
||||
`UnexpectedValueException` when the stream is read.
|
||||
|
||||
Iterator exhaustion is now the only EOF signal for iterator-backed streams.
|
||||
Yielding `false`, `null`, or an empty string no longer ends the stream; those
|
||||
values are zero-length chunks and are skipped while the iterator advances. If
|
||||
your iterator yielded `false` or `null` to stop streaming, update it to finish
|
||||
iteration instead.
|
||||
|
||||
Avoid iterators that yield only zero-length chunks indefinitely. Such iterators
|
||||
never produce bytes and never reach EOF, so they cannot satisfy stream reads.
|
||||
|
||||
```php
|
||||
// Before: yielding false or null could stop an iterator-backed stream early.
|
||||
$stream = Utils::streamFor(new ArrayIterator([false, 'body']));
|
||||
|
||||
// After: false and null are skipped chunks. End the iterator to signal EOF.
|
||||
$stream = Utils::streamFor(new ArrayIterator(['body']));
|
||||
```
|
||||
|
||||
#### Stream Behavior Changes
|
||||
|
||||
All stream implementations now reject negative `read()` lengths with
|
||||
`RuntimeException`. In 2.x, some decorators passed negative lengths through,
|
||||
some returned sliced data, and some behavior varied by PHP version.
|
||||
|
||||
`LimitStream` now rejects negative offsets and limits below `-1`. For
|
||||
non-seekable streams, offsets are tracked by the number of bytes actually
|
||||
skipped. Short reads are retried until the offset is reached, EOF is reached, or
|
||||
the decorated stream stops making progress.
|
||||
|
||||
`StreamWrapper` now translates `RuntimeException` failures from the wrapped
|
||||
PSR-7 stream into PHP stream-wrapper failure values. When using a resource from
|
||||
`StreamWrapper::getResource()`, functions such as `fread()`, `fwrite()`,
|
||||
`fseek()`, `feof()`, and `fstat()` may now return normal PHP failure values
|
||||
instead of propagating the PSR-7 stream exception. Call the PSR-7 stream directly
|
||||
if you need exception-based failure handling.
|
||||
|
||||
The `StreamWrapper::stream_read()` callback no longer declares a native return
|
||||
type so read failures can return `false`. The `StreamWrapper::stream_tell()`
|
||||
callback no longer declares a native return type so post-seek position lookup
|
||||
failures can make `fseek()` fail.
|
||||
|
||||
#### Stream Mode Capabilities
|
||||
|
||||
`Stream::isReadable()` and `Stream::isWritable()` now follow PHP stream mode
|
||||
semantics more closely. Update modes are detected by the presence of `+`,
|
||||
including valid modes such as `rt+`, `wt+`, `at+`, `xt+`, and `ct+`.
|
||||
|
||||
Literal `rw` metadata is now treated as read-only, matching PHP real-file
|
||||
streams. If a custom stream wrapper previously exposed `rw` for a writable
|
||||
resource, open it with a valid update mode such as `r+`, `w+`, or `a+` instead.
|
||||
|
||||
#### Stream Copy Behavior
|
||||
|
||||
Stream sizes, offsets, high-water marks, and byte counts are now validated as
|
||||
non-negative PHP integers where applicable. Operations that would overflow
|
||||
`PHP_INT_MAX` throw `OverflowException` instead of silently wrapping or producing
|
||||
an invalid position or size.
|
||||
|
||||
`Utils::copyToStream()` now returns the number of bytes copied and throws a
|
||||
`RuntimeException` when the destination stream cannot make progress, for example
|
||||
a `BufferStream` at its high-water mark or a full `DroppingStream`. Its
|
||||
signature changed from `: void` to `: int`, but callers that ignore the return
|
||||
value do not need to change anything. In 2.x, the copy stopped silently when the
|
||||
destination could not make progress. For a guaranteed full copy, use a normal
|
||||
writable stream such as a file or `php://temp` stream.
|
||||
|
||||
#### Stream Timeout Detection
|
||||
|
||||
Timed-out stream operations now throw
|
||||
`GuzzleHttp\Psr7\Exception\TimeoutException`, which extends
|
||||
`RuntimeException`. `Stream::read()`, `Stream::write()`,
|
||||
`AppendStream::read()`, `CachingStream::read()`, `InflateStream::read()`,
|
||||
`Utils::copyToStream()`, `Utils::copyToString()`, `Utils::hash()`,
|
||||
`Utils::readLine()`, and `Utils::tryGetContents()` detect PHP-style stream
|
||||
timeout metadata when a read or write operation cannot make progress. Timeout
|
||||
detection is best-effort; custom stream implementations that do not expose
|
||||
`timed_out` metadata continue to behave as before. Previously, timed-out reads
|
||||
could be treated as EOF or return partial results, and timed-out writes could be
|
||||
reported as generic write failures or no-progress writes.
|
||||
|
||||
#### Message Body Summaries
|
||||
|
||||
`Message::bodySummary()` still summarizes seekable bodies from the beginning,
|
||||
even when the body was already partially read. It now restores the body cursor to
|
||||
the position it had before the summary was created. In 2.x, calling
|
||||
`bodySummary()` left seekable bodies rewound to the beginning. The optional
|
||||
`$truncateAt` argument now accepts `null` as an explicit request for the default
|
||||
summary length, matching the behavior of omitting the argument.
|
||||
|
||||
Most applications do not need to change anything. Check your code only if you
|
||||
called `bodySummary()` and then read the same body while relying on
|
||||
`bodySummary()` to leave the body rewound. If you need to read the body from the
|
||||
beginning after summarizing it, call `Message::rewindBody()` explicitly.
|
||||
|
||||
#### Stream Lifecycle
|
||||
|
||||
`FnStream` now treats `close()` and successful `detach()` calls as terminal
|
||||
lifecycle operations. Its configured `close` callback is invoked at most once;
|
||||
repeated `close()` calls are no-ops, destruction after explicit close no longer
|
||||
invokes the close callback, and closed or detached streams no longer forward
|
||||
read, write, seek, metadata, or stringification callbacks. `FnStream` also
|
||||
suppresses exceptions thrown by destructor-triggered close callbacks. Call
|
||||
`close()` explicitly if cleanup failures must be observed.
|
||||
|
||||
`CachingStream::close()` is now idempotent. Calling `close()` after `detach()`
|
||||
still closes the remote stream owned by the `CachingStream`, but it no longer
|
||||
closes the detached cache resource returned to the caller. Repeated `close()`
|
||||
calls are no-ops.
|
||||
|
||||
`InflateStream::close()` now also closes the compressed source stream that was
|
||||
passed to its constructor. In 2.x, closing an `InflateStream` left the source
|
||||
stream open. Call `detach()` instead of `close()` if the compressed source
|
||||
stream must stay open; `close()` after `detach()` no longer closes the source.
|
||||
|
||||
`PumpStream::close()` and `PumpStream::detach()` now discard internally buffered
|
||||
unread bytes. If a callable or iterator source returns more bytes than a read
|
||||
requested, drain the stream before closing it if you need those buffered bytes.
|
||||
|
||||
#### Multipart Part Headers and Metadata
|
||||
|
||||
`MultipartStream` no longer adds default `Content-Length` headers to individual
|
||||
`multipart/form-data` parts. RFC 7578 section 4.8 says multipart form-data
|
||||
parts must not include `Content-*` headers other than the supported multipart
|
||||
part headers, so 3.0 stops generating per-part `Content-Length` by default.
|
||||
|
||||
If your tests compare raw multipart payloads, remove the generated
|
||||
`Content-Length` lines from expected strings:
|
||||
|
||||
```text
|
||||
// 2.x generated:
|
||||
--boundary\r\n
|
||||
Content-Disposition: form-data; name="foo"\r\n
|
||||
Content-Length: 3\r\n
|
||||
\r\n
|
||||
bar\r\n
|
||||
|
||||
// 3.0 generates:
|
||||
--boundary\r\n
|
||||
Content-Disposition: form-data; name="foo"\r\n
|
||||
\r\n
|
||||
bar\r\n
|
||||
```
|
||||
|
||||
Applications can still pass an explicit `Content-Length` header in a multipart
|
||||
element's `headers` array if a non-standard peer requires it:
|
||||
|
||||
```php
|
||||
$body = new MultipartStream([
|
||||
[
|
||||
'name' => 'foo',
|
||||
'contents' => 'bar',
|
||||
'headers' => ['Content-Length' => '3'],
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
`MultipartStream` now escapes generated `Content-Disposition` `name` and
|
||||
`filename` parameters before serializing multipart part headers. Double quotes,
|
||||
carriage returns, and line feeds are encoded as `%22`, `%0D`, and `%0A`. Literal
|
||||
backslashes and other characters are serialized unchanged, matching browser
|
||||
multipart form submission behavior.
|
||||
|
||||
```php
|
||||
// Before: these values were interpolated into the generated part header.
|
||||
$body = new MultipartStream([
|
||||
[
|
||||
'name' => "field\"\r\nname",
|
||||
'filename' => "avatar\"\r\n.txt",
|
||||
'contents' => 'body',
|
||||
],
|
||||
]);
|
||||
|
||||
// After: the generated Content-Disposition parameters contain
|
||||
// field%22%0D%0Aname and avatar%22%0D%0A.txt.
|
||||
```
|
||||
|
||||
Explicit custom boundaries are now validated using RFC 2046 multipart boundary
|
||||
syntax. Omit the boundary or pass `null` to continue using a generated random
|
||||
boundary.
|
||||
|
||||
The string `'0'` is now treated as an explicit custom boundary and is serialized
|
||||
literally. In 2.x, PHP truthiness caused `new MultipartStream($elements, '0')`
|
||||
to use a generated random boundary. Omit the boundary or pass `null` when you
|
||||
want a generated boundary.
|
||||
|
||||
Custom multipart part header names and values are also validated before
|
||||
serialization. Header names must be valid HTTP tokens, and header values must be
|
||||
strings without CR, LF, or other invalid control bytes.
|
||||
|
||||
`MultipartStream` now preserves trailing spaces and tabs in custom multipart
|
||||
part header values when serializing the body. In 2.x, the final serialized part
|
||||
header line was trimmed as a side effect of removing the generated header
|
||||
terminator. Normal multipart parsers treat this optional whitespace as
|
||||
insignificant, but tests, signatures, or snapshots that compare raw multipart
|
||||
body bytes may need updated expectations.
|
||||
|
||||
#### URI Userinfo Redaction
|
||||
|
||||
`Utils::redactUserInfo()` now redacts all non-empty URI userinfo, including
|
||||
username-only userinfo. In 2.x, it only redacted the password portion when
|
||||
userinfo contained a password delimiter.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
|
||||
// 2.x: https://TOKEN@example.com
|
||||
// 3.0: https://***@example.com
|
||||
(string) Utils::redactUserInfo(new Uri('https://TOKEN@example.com'));
|
||||
|
||||
// 2.x: https://user:***@example.com
|
||||
// 3.0: https://***@example.com
|
||||
(string) Utils::redactUserInfo(new Uri('https://user:pass@example.com'));
|
||||
```
|
||||
|
||||
#### Header List Helpers
|
||||
|
||||
The deprecated `Header::normalize()` method was removed. Use
|
||||
`Header::splitList()` to split HTTP headers that are defined as comma-separated
|
||||
lists.
|
||||
|
||||
`Header::splitList()` now trims list elements with spaces, horizontal tabs,
|
||||
carriage returns, and line feeds. 2.x also trimmed null bytes and vertical
|
||||
tabs. Validated header values cannot contain those bytes, so this only affects
|
||||
strings passed to `Header::splitList()` directly.
|
||||
|
||||
#### Non-instantiable Utility Classes
|
||||
|
||||
Static utility and constant classes such as `Header`, `Message`, `MimeType`,
|
||||
`Query`, and `Utils` now have private constructors. Replace any accidental
|
||||
instantiation with static method calls or constant access.
|
||||
|
||||
#### Native PHP Serialization of Streams
|
||||
|
||||
Guzzle PSR-7 stream implementations no longer support native PHP `serialize()`
|
||||
or `unserialize()`. Persist stream contents explicitly and recreate streams with
|
||||
`Utils::streamFor()` when needed.
|
||||
|
||||
#### URI Normalization of Userinfo and Host
|
||||
|
||||
`UriNormalizer::CAPITALIZE_PERCENT_ENCODING` and
|
||||
`UriNormalizer::DECODE_UNRESERVED_CHARACTERS` now also apply to the userinfo and
|
||||
host components. In 2.x, these normalizations only rewrote the path, query, and
|
||||
fragment.
|
||||
|
||||
Since the host is case-insensitive and PSR-7 requires it to be lowercase, octets
|
||||
decoded in the host are lowercased. Reserved percent-encoded octets such as
|
||||
`%3A` are never decoded, so component boundaries cannot change, and these two
|
||||
flags never modify bracketed IP-literal hosts, which only the separate
|
||||
`UriNormalizer::CANONICALIZE_IPV6_HOST` normalization may canonicalize. Both
|
||||
flags are part of `UriNormalizer::PRESERVING_NORMALIZATIONS`, so the output of
|
||||
`UriNormalizer::normalize()` and the result of `UriNormalizer::isEquivalent()`
|
||||
can change for URIs whose userinfo or host contains percent-encoded octets.
|
||||
Custom `UriInterface` implementations now receive `withUserInfo()` or
|
||||
`withHost()` calls from the normalizer when a normalization changes those
|
||||
components; unchanged components are never rewritten. The rewrite is kept only
|
||||
when the value returned by the implementation matches the normalized form, and
|
||||
a userinfo with an empty user segment is never rewritten. If a setter returns a
|
||||
different representation, that rewrite is discarded, while other selected
|
||||
normalizations still apply, and setter exceptions propagate. No percent-encoding
|
||||
normalization is applied to a component with malformed percent syntax, such as a
|
||||
`%` not followed by two hexadecimal digits.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\Psr7\UriNormalizer;
|
||||
|
||||
// 2.x: http://%75ser@ex%61mple.com/
|
||||
// 3.0: http://user@example.com/
|
||||
(string) UriNormalizer::normalize(new Uri('http://%75ser@ex%61mple.com/'));
|
||||
```
|
||||
|
||||
#### URI Ports and Authority Handling
|
||||
|
||||
Several 3.0 changes affect how URI ports are accepted, validated, and rendered.
|
||||
Each is described in its own section above:
|
||||
|
||||
- `UriInterface::withPort()` now requires `int|null`; see "Native PSR-7
|
||||
Parameter Types".
|
||||
- `Uri::fromParts()` validates ports instead of casting them, and `withHost()`
|
||||
rejects embedded `host:port` values; see "URI Host and Scheme Validation".
|
||||
- A generic `Uri` can represent ports 0 through 65535. Inbound HTTP authority
|
||||
parsing is stricter: `Message::parseRequest()` rejects zero-valued ports but
|
||||
accepts nonzero leading-zero ports, normalizing the reconstructed URI while
|
||||
preserving the raw `Host` or request-target text; see "HTTP Start-line
|
||||
Parsing" and "URI Host and Scheme Validation".
|
||||
- Server globals reject a zero-valued `HTTP_HOST` and validate `SERVER_PORT`
|
||||
when fallback authority reconstruction needs it. A recognized absolute-form
|
||||
or CONNECT `REQUEST_URI` authority takes precedence and can still produce a
|
||||
URI with port zero; see "URI Host and Scheme Validation".
|
||||
- Synthesized `Host` headers now include any non-default URI port; see "Request
|
||||
Modification Changes" and "Request Host Synchronization".
|
||||
|
||||
`Uri` now knows the default ports of the `ws` and `wss` schemes, 80 and 443 per
|
||||
RFC 6455. An explicit default port on a `ws` or `wss` URI is removed when the
|
||||
URI is constructed or modified, `Uri::isDefaultPort()` returns `true` for such
|
||||
URIs, and `UriNormalizer::normalize()` with the `REMOVE_DEFAULT_PORT` flag
|
||||
removes the port from other `UriInterface` implementations as well. In 2.x,
|
||||
these ports were preserved.
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
|
||||
// 2.x: ws://example.com:80/chat
|
||||
// 3.0: ws://example.com/chat
|
||||
(string) new Uri('ws://example.com:80/chat');
|
||||
|
||||
// 2.x: 443
|
||||
// 3.0: null
|
||||
(new Uri('wss://example.com:443'))->getPort();
|
||||
```
|
||||
|
||||
Because a native `Uri` never carries a default `ws` or `wss` port, the `Host`
|
||||
header synchronized from such a request URI omits the port. `Request` and
|
||||
`Message` `Host` synthesis and `Utils::modifyRequest()` still append an explicit
|
||||
default port that a `ws` or `wss` URI from another `UriInterface` implementation
|
||||
reports.
|
||||
|
||||
`UriComparator::isCrossOrigin()` now applies these default ports when comparing
|
||||
effective ports, so two `ws` or `wss` URIs that differ only by an explicit
|
||||
default port, such as `ws://example.com/` and `ws://example.com:80/`, are
|
||||
same-origin no matter which `UriInterface` implementation supplies them. In 2.x,
|
||||
such pairs were considered cross-origin. Schemes other than `http`, `https`,
|
||||
`ws`, and `wss` still receive no implicit default port.
|
||||
|
||||
#### Sensitive Stack Trace Arguments
|
||||
|
||||
Credential-bearing URI, server-global, Authorization-header, and cookie
|
||||
arguments are marked with `#[\SensitiveParameter]`. PHP 8.2 and later replace
|
||||
those arguments in stack traces with `SensitiveParameterValue`. PHP 7.4 through
|
||||
8.1 do not redact trace arguments.
|
||||
|
||||
This does not redact logs, exception messages, object properties, wire traffic,
|
||||
captured variables, return values, user callbacks, or the separate executing
|
||||
object in an explicit backtrace.
|
||||
|
||||
1.x to 2.0
|
||||
----------
|
||||
|
||||
|
|
|
|||
90
vendor/guzzlehttp/psr7/src/AppendStream.php
vendored
90
vendor/guzzlehttp/psr7/src/AppendStream.php
vendored
|
|
@ -13,17 +13,16 @@ use Psr\Http\Message\StreamInterface;
|
|||
*/
|
||||
final class AppendStream implements StreamInterface
|
||||
{
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var StreamInterface[] Streams being decorated */
|
||||
private $streams = [];
|
||||
private array $streams = [];
|
||||
|
||||
/** @var bool */
|
||||
private $seekable = true;
|
||||
private bool $seekable = true;
|
||||
|
||||
/** @var int */
|
||||
private $current = 0;
|
||||
private int $current = 0;
|
||||
|
||||
/** @var int */
|
||||
private $pos = 0;
|
||||
private int $pos = 0;
|
||||
|
||||
/**
|
||||
* @param StreamInterface[] $streams Streams to decorate. Each stream must
|
||||
|
|
@ -38,18 +37,9 @@ final class AppendStream implements StreamInterface
|
|||
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
$this->rewind();
|
||||
$this->rewind();
|
||||
|
||||
return $this->getContents();
|
||||
} catch (\Throwable $e) {
|
||||
if (\PHP_VERSION_ID >= 70400) {
|
||||
throw $e;
|
||||
}
|
||||
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
|
||||
|
||||
return '';
|
||||
}
|
||||
return $this->getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,7 +122,7 @@ final class AppendStream implements StreamInterface
|
|||
if ($s === null) {
|
||||
return null;
|
||||
}
|
||||
$size += $s;
|
||||
$size = Integers::add($size, $s);
|
||||
}
|
||||
|
||||
return $size;
|
||||
|
|
@ -153,26 +143,8 @@ final class AppendStream implements StreamInterface
|
|||
/**
|
||||
* Attempts to seek to the given position. Only supports SEEK_SET.
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->seekable) {
|
||||
throw new \RuntimeException('This AppendStream is not seekable');
|
||||
} elseif ($whence !== SEEK_SET) {
|
||||
|
|
@ -203,15 +175,10 @@ final class AppendStream implements StreamInterface
|
|||
/**
|
||||
* Reads from all of the appended streams until the length is met or EOF.
|
||||
*/
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
if ($this->streams === []) {
|
||||
|
|
@ -233,7 +200,7 @@ final class AppendStream implements StreamInterface
|
|||
++$this->current;
|
||||
}
|
||||
|
||||
$result = $this->streams[$this->current]->read($remaining);
|
||||
$result = StreamTimeout::read($this->streams[$this->current], $remaining, 'Unable to read from stream: timed out');
|
||||
|
||||
if ($result === '') {
|
||||
$progressToNext = true;
|
||||
|
|
@ -244,7 +211,7 @@ final class AppendStream implements StreamInterface
|
|||
$remaining = $length - strlen($buffer);
|
||||
}
|
||||
|
||||
$this->pos += strlen($buffer);
|
||||
$this->pos = Integers::add($this->pos, strlen($buffer));
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
|
@ -264,34 +231,13 @@ final class AppendStream implements StreamInterface
|
|||
return $this->seekable;
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Cannot write to an AppendStream');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
public function getMetadata(?string $key = null): ?array
|
||||
{
|
||||
if ($key !== null && !\is_string($key)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
|
||||
\get_debug_type($key)
|
||||
);
|
||||
}
|
||||
|
||||
return $key ? null : [];
|
||||
return $key === null ? [] : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
vendor/guzzlehttp/psr7/src/BufferStream.php
vendored
71
vendor/guzzlehttp/psr7/src/BufferStream.php
vendored
|
|
@ -16,22 +16,22 @@ use Psr\Http\Message\StreamInterface;
|
|||
*/
|
||||
final class BufferStream implements StreamInterface
|
||||
{
|
||||
/** @var int */
|
||||
private $hwm;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var string */
|
||||
private $buffer = '';
|
||||
private int $hwm;
|
||||
|
||||
private string $buffer = '';
|
||||
|
||||
/**
|
||||
* @param int $hwm High water mark, representing the preferred maximum
|
||||
* buffer size. If the size of the buffer exceeds the high
|
||||
* water mark, then calls to write will continue to succeed
|
||||
* but will return 0 to inform writers to slow down
|
||||
* buffer size. If the size of the buffer reaches or exceeds
|
||||
* the high water mark, then calls to write will continue to
|
||||
* succeed but will return 0 to inform writers to slow down
|
||||
* until the buffer has been drained by reading from it.
|
||||
*/
|
||||
public function __construct(int $hwm = 16384)
|
||||
{
|
||||
$this->hwm = $hwm;
|
||||
$this->hwm = Integers::assertNonNegativeInteger($hwm, 'High water mark');
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
|
|
@ -84,26 +84,8 @@ final class BufferStream implements StreamInterface
|
|||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Cannot seek a BufferStream');
|
||||
}
|
||||
|
||||
|
|
@ -120,15 +102,10 @@ final class BufferStream implements StreamInterface
|
|||
/**
|
||||
* Reads data from the buffer.
|
||||
*/
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
$currentLength = strlen($this->buffer);
|
||||
|
|
@ -149,17 +126,8 @@ final class BufferStream implements StreamInterface
|
|||
/**
|
||||
* Writes data to the buffer.
|
||||
*/
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
$this->buffer .= $string;
|
||||
|
||||
if (strlen($this->buffer) >= $this->hwm) {
|
||||
|
|
@ -172,21 +140,12 @@ final class BufferStream implements StreamInterface
|
|||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
public function getMetadata(?string $key = null)
|
||||
{
|
||||
if ($key !== null && !\is_string($key)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
|
||||
\get_debug_type($key)
|
||||
);
|
||||
}
|
||||
|
||||
if ($key === 'hwm') {
|
||||
return $this->hwm;
|
||||
}
|
||||
|
||||
return $key ? null : [];
|
||||
return $key === null ? [] : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
123
vendor/guzzlehttp/psr7/src/CachingStream.php
vendored
123
vendor/guzzlehttp/psr7/src/CachingStream.php
vendored
|
|
@ -13,26 +13,30 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class CachingStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var StreamInterface Stream being wrapped */
|
||||
private $remoteStream;
|
||||
private StreamInterface $remoteStream;
|
||||
|
||||
/** @var int Number of bytes to skip reading due to a write on the buffer */
|
||||
private $skipReadBytes = 0;
|
||||
private int $skipReadBytes = 0;
|
||||
|
||||
/**
|
||||
* @var StreamInterface
|
||||
*/
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
/** @var bool */
|
||||
private $detached = false;
|
||||
private bool $detached = false;
|
||||
|
||||
private bool $closed = false;
|
||||
|
||||
/**
|
||||
* We will treat the buffer object as the body of the stream
|
||||
*
|
||||
* @param StreamInterface $stream Stream to cache. The cursor is assumed to be at the beginning of the stream.
|
||||
* @param StreamInterface $target Optionally specify where data is cached
|
||||
* @param StreamInterface $target Optionally specify where data is cached. Defaults to a "php://temp"
|
||||
* stream. A custom target is used as a random-access byte buffer to
|
||||
* replay the remote stream, so it must be readable, writable, and
|
||||
* seekable, report an accurate position and size, and store writes
|
||||
* losslessly. Lossy or non-seekable streams such as BufferStream and
|
||||
* DroppingStream are not valid targets.
|
||||
*/
|
||||
public function __construct(
|
||||
StreamInterface $stream,
|
||||
|
|
@ -62,36 +66,31 @@ final class CachingStream implements StreamInterface
|
|||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
if ($whence === SEEK_SET) {
|
||||
$byte = $offset;
|
||||
} elseif ($whence === SEEK_CUR) {
|
||||
$byte = $offset + $this->tell();
|
||||
$byte = Integers::addSigned($this->tell(), $offset);
|
||||
} elseif ($whence === SEEK_END) {
|
||||
$size = $this->remoteStream->getSize();
|
||||
if ($size === null) {
|
||||
// Discovering the size reads the remote stream to EOF and
|
||||
// moves the cursor, so restore the cursor if the computed
|
||||
// target is rejected to keep a failed seek side-effect free.
|
||||
$position = $this->tell();
|
||||
$size = $this->cacheEntireStream();
|
||||
|
||||
try {
|
||||
$byte = Integers::addSigned($size, $offset);
|
||||
} catch (\Throwable $e) {
|
||||
$this->stream->seek($position);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
$byte = Integers::addSigned($size, $offset);
|
||||
}
|
||||
$byte = $size + $offset;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid whence');
|
||||
}
|
||||
|
|
@ -119,15 +118,10 @@ final class CachingStream implements StreamInterface
|
|||
}
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
// Perform a regular read on any previously read data from the buffer
|
||||
|
|
@ -140,8 +134,10 @@ final class CachingStream implements StreamInterface
|
|||
// been filled from the remote stream, then we must skip bytes on
|
||||
// the remote stream to emulate overwriting bytes from that
|
||||
// position. This mimics the behavior of other PHP stream wrappers.
|
||||
$remoteData = $this->remoteStream->read(
|
||||
$remaining + $this->skipReadBytes
|
||||
$remoteData = StreamTimeout::read(
|
||||
$this->remoteStream,
|
||||
Integers::add($remaining, $this->skipReadBytes),
|
||||
'Unable to read from stream: timed out'
|
||||
);
|
||||
|
||||
if ($this->skipReadBytes) {
|
||||
|
|
@ -161,24 +157,15 @@ final class CachingStream implements StreamInterface
|
|||
return $data;
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
// When appending to the end of the currently read stream, you'll want
|
||||
// to skip bytes from being read from the remote stream to emulate
|
||||
// other stream wrappers. Basically replacing bytes of data of a fixed
|
||||
// length.
|
||||
$overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell();
|
||||
$overflow = Integers::add(strlen($string), $this->tell()) - $this->remoteStream->tell();
|
||||
if ($overflow > 0) {
|
||||
$this->skipReadBytes += $overflow;
|
||||
$this->skipReadBytes = Integers::add($this->skipReadBytes, $overflow);
|
||||
}
|
||||
|
||||
return $this->stream->write($string);
|
||||
|
|
@ -207,13 +194,39 @@ final class CachingStream implements StreamInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* Close both the remote stream and buffer stream
|
||||
* Close the remote stream and any attached cache stream.
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->remoteStream->close();
|
||||
$this->stream->close();
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$closeCache = !$this->detached;
|
||||
$this->closed = true;
|
||||
$this->detached = true;
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
$this->remoteStream->close();
|
||||
} catch (\Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
if ($closeCache) {
|
||||
try {
|
||||
$this->stream->close();
|
||||
} catch (\Throwable $e) {
|
||||
if ($exception === null) {
|
||||
$exception = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($exception !== null) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
private function cacheEntireStream(): int
|
||||
|
|
|
|||
63
vendor/guzzlehttp/psr7/src/DiagnosticValue.php
vendored
Normal file
63
vendor/guzzlehttp/psr7/src/DiagnosticValue.php
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* Escapes control characters and malformed UTF-8 for use in diagnostics.
|
||||
*/
|
||||
final class DiagnosticValue
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes C0, DEL, and C1 controls as uppercase `\xNN` sequences.
|
||||
*
|
||||
* ASCII bytes from 0x20 through 0x7E and valid UTF-8 characters outside
|
||||
* those control ranges remain unchanged. If the input is malformed UTF-8 or
|
||||
* PCRE cannot process it, every byte outside printable ASCII is escaped.
|
||||
* Valid C1 characters are rendered as `\xNN` using their Unicode code
|
||||
* points. During bytewise fallback, each original byte outside printable
|
||||
* ASCII is rendered in the same form. The result is diagnostic text, not a
|
||||
* reversible encoding.
|
||||
*
|
||||
* This does not encode values for HTML, JSON, shells, terminals, URLs, or
|
||||
* protocol fields.
|
||||
*/
|
||||
public static function escape(string $value): string
|
||||
{
|
||||
$escaped = \preg_replace_callback(
|
||||
'/[\x{0000}-\x{001F}\x{007F}-\x{009F}]/u',
|
||||
static function (array $matches): string {
|
||||
$character = $matches[0];
|
||||
$codePoint = \strlen($character) === 1 ? \ord($character) : \ord($character[1]);
|
||||
|
||||
return \sprintf('\\x%02X', $codePoint);
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
||||
return $escaped ?? self::escapeBytes($value);
|
||||
}
|
||||
|
||||
private static function escapeBytes(string $value): string
|
||||
{
|
||||
$escaped = '';
|
||||
|
||||
for ($offset = 0, $length = \strlen($value); $offset < $length; ++$offset) {
|
||||
$byte = \ord($value[$offset]);
|
||||
if ($byte >= 0x20 && $byte <= 0x7E) {
|
||||
$escaped .= $value[$offset];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$escaped .= \sprintf('\\x%02X', $byte);
|
||||
}
|
||||
|
||||
return $escaped;
|
||||
}
|
||||
}
|
||||
20
vendor/guzzlehttp/psr7/src/DroppingStream.php
vendored
20
vendor/guzzlehttp/psr7/src/DroppingStream.php
vendored
|
|
@ -13,12 +13,11 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class DroppingStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var int */
|
||||
private $maxLength;
|
||||
private int $maxLength;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
/**
|
||||
* @param StreamInterface $stream Underlying stream to decorate.
|
||||
|
|
@ -27,20 +26,11 @@ final class DroppingStream implements StreamInterface
|
|||
public function __construct(StreamInterface $stream, int $maxLength)
|
||||
{
|
||||
$this->stream = $stream;
|
||||
$this->maxLength = $maxLength;
|
||||
$this->maxLength = Integers::assertNonNegativeInteger($maxLength, 'Maximum length');
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
$diff = $this->maxLength - $this->stream->getSize();
|
||||
|
||||
// Begin returning 0 when the underlying stream is too large.
|
||||
|
|
|
|||
14
vendor/guzzlehttp/psr7/src/Exception/TimeoutException.php
vendored
Normal file
14
vendor/guzzlehttp/psr7/src/Exception/TimeoutException.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a stream operation times out.
|
||||
*/
|
||||
class TimeoutException extends RuntimeException
|
||||
{
|
||||
}
|
||||
153
vendor/guzzlehttp/psr7/src/FnStream.php
vendored
153
vendor/guzzlehttp/psr7/src/FnStream.php
vendored
|
|
@ -15,6 +15,8 @@ use Psr\Http\Message\StreamInterface;
|
|||
#[\AllowDynamicProperties]
|
||||
final class FnStream implements StreamInterface
|
||||
{
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
private const SLOTS = [
|
||||
'__toString', 'close', 'detach', 'rewind',
|
||||
'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write',
|
||||
|
|
@ -22,7 +24,9 @@ final class FnStream implements StreamInterface
|
|||
];
|
||||
|
||||
/** @var array<string, callable> */
|
||||
private $methods;
|
||||
private array $methods;
|
||||
|
||||
private bool $detached = false;
|
||||
|
||||
/**
|
||||
* @param array<string, callable> $methods Hash of method name to a callable.
|
||||
|
|
@ -44,8 +48,7 @@ final class FnStream implements StreamInterface
|
|||
*/
|
||||
public function __get(string $name): void
|
||||
{
|
||||
throw new \BadMethodCallException(str_replace('_fn_', '', $name)
|
||||
.'() is not implemented in the FnStream');
|
||||
throw new \BadMethodCallException(\sprintf('%s() is not implemented in the FnStream', DiagnosticValue::escape(str_replace('_fn_', '', $name))));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,8 +56,14 @@ final class FnStream implements StreamInterface
|
|||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($this->_fn_close)) {
|
||||
($this->_fn_close)();
|
||||
if ($this->detached || !isset($this->_fn_close)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->close();
|
||||
} catch (\Throwable $e) {
|
||||
// Destructors must not surface cleanup failures.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +74,18 @@ final class FnStream implements StreamInterface
|
|||
*/
|
||||
public function __wakeup(): void
|
||||
{
|
||||
throw new \LogicException('FnStream should never be unserialized');
|
||||
$this->methods = [];
|
||||
$this->detached = true;
|
||||
|
||||
throw new \LogicException(static::class.' should never be unserialized');
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->methods = [];
|
||||
$this->detached = true;
|
||||
|
||||
throw new \LogicException(static::class.' should never be unserialized');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,10 +94,8 @@ final class FnStream implements StreamInterface
|
|||
*
|
||||
* @param StreamInterface $stream Stream to decorate
|
||||
* @param array<string, callable> $methods Hash of method name to a callable
|
||||
*
|
||||
* @return FnStream
|
||||
*/
|
||||
public static function decorate(StreamInterface $stream, array $methods)
|
||||
public static function decorate(StreamInterface $stream, array $methods): self
|
||||
{
|
||||
// If any of the required methods were not provided, then simply
|
||||
// proxy to the decorated stream.
|
||||
|
|
@ -92,110 +110,113 @@ final class FnStream implements StreamInterface
|
|||
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
/** @var string */
|
||||
return ($this->_fn___toString)();
|
||||
} catch (\Throwable $e) {
|
||||
if (\PHP_VERSION_ID >= 70400) {
|
||||
throw $e;
|
||||
}
|
||||
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
|
||||
$this->assertAttached();
|
||||
|
||||
return '';
|
||||
}
|
||||
/** @var string */
|
||||
return ($this->_fn___toString)();
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
($this->_fn_close)();
|
||||
if ($this->detached) {
|
||||
return;
|
||||
}
|
||||
|
||||
$close = $this->_fn_close;
|
||||
$this->detached = true;
|
||||
$close();
|
||||
}
|
||||
|
||||
public function detach()
|
||||
{
|
||||
return ($this->_fn_detach)();
|
||||
if ($this->detached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$detach = $this->_fn_detach;
|
||||
$result = $detach();
|
||||
$this->detached = true;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getSize(): ?int
|
||||
{
|
||||
if ($this->detached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ($this->_fn_getSize)();
|
||||
}
|
||||
|
||||
public function tell(): int
|
||||
{
|
||||
$this->assertAttached();
|
||||
|
||||
return ($this->_fn_tell)();
|
||||
}
|
||||
|
||||
public function eof(): bool
|
||||
{
|
||||
$this->assertAttached();
|
||||
|
||||
return ($this->_fn_eof)();
|
||||
}
|
||||
|
||||
public function isSeekable(): bool
|
||||
{
|
||||
if ($this->detached) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->_fn_isSeekable)();
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->assertAttached();
|
||||
|
||||
($this->_fn_rewind)();
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
$this->assertAttached();
|
||||
|
||||
($this->_fn_seek)($offset, $whence);
|
||||
}
|
||||
|
||||
public function isWritable(): bool
|
||||
{
|
||||
if ($this->detached) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->_fn_isWritable)();
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
$this->assertAttached();
|
||||
|
||||
return ($this->_fn_write)($string);
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
{
|
||||
if ($this->detached) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->_fn_isReadable)();
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
$this->assertAttached();
|
||||
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
return ($this->_fn_read)($length);
|
||||
|
|
@ -203,23 +224,27 @@ final class FnStream implements StreamInterface
|
|||
|
||||
public function getContents(): string
|
||||
{
|
||||
$this->assertAttached();
|
||||
|
||||
return ($this->_fn_getContents)();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
public function getMetadata(?string $key = null)
|
||||
{
|
||||
if ($key !== null && !\is_string($key)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
|
||||
\get_debug_type($key)
|
||||
);
|
||||
if ($this->detached) {
|
||||
return $key === null ? [] : null;
|
||||
}
|
||||
|
||||
return ($this->_fn_getMetadata)($key);
|
||||
}
|
||||
|
||||
private function assertAttached(): void
|
||||
{
|
||||
if ($this->detached) {
|
||||
throw new \RuntimeException('Stream is detached');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
vendor/guzzlehttp/psr7/src/Header.php
vendored
56
vendor/guzzlehttp/psr7/src/Header.php
vendored
|
|
@ -6,11 +6,14 @@ namespace GuzzleHttp\Psr7;
|
|||
|
||||
final class Header
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an array of header values containing ";" separated data into an
|
||||
* array of associative arrays representing the header key value pair data
|
||||
* of the header. When a parameter does not contain a value, but just
|
||||
* contains a key, this function will inject a key with a '' string value.
|
||||
* Parses semicolon-separated header parameters into associative arrays, one
|
||||
* per comma-separated header value. Parameters without a value are appended
|
||||
* as values under integer keys.
|
||||
*
|
||||
* @param string|array $header Header to parse into components.
|
||||
*/
|
||||
|
|
@ -23,7 +26,13 @@ final class Header
|
|||
foreach (self::splitList($value) as $val) {
|
||||
$part = [];
|
||||
foreach (self::splitParameters($val) as $kvp) {
|
||||
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
|
||||
$count = preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches);
|
||||
|
||||
if ($count === false) {
|
||||
throw new \RuntimeException('Unable to parse header parameters: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($count !== 0) {
|
||||
$m = $matches[0];
|
||||
if (isset($m[1])) {
|
||||
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
|
||||
|
|
@ -86,35 +95,16 @@ final class Header
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts an array of header values that may contain comma separated
|
||||
* headers into an array of headers with no comma separated values.
|
||||
* Splits an HTTP header defined to contain a comma-separated list into each
|
||||
* individual value. Empty values are removed.
|
||||
*
|
||||
* @param string|array $header Header to normalize.
|
||||
* Example headers include `accept`, `cache-control`, and `if-none-match`.
|
||||
*
|
||||
* @deprecated Use self::splitList() instead.
|
||||
*/
|
||||
public static function normalize($header): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ((array) $header as $value) {
|
||||
foreach (self::splitList($value) as $parsed) {
|
||||
$result[] = $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a HTTP header defined to contain a comma-separated list into
|
||||
* each individual value. Empty values will be removed.
|
||||
* This method must not be used to parse headers that are not defined as a
|
||||
* list, such as `user-agent` or `set-cookie`.
|
||||
*
|
||||
* Example headers include 'accept', 'cache-control' and 'if-none-match'.
|
||||
*
|
||||
* This method must not be used to parse headers that are not defined as
|
||||
* a list, such as 'user-agent' or 'set-cookie'.
|
||||
*
|
||||
* @param string|string[] $values Header value as returned by MessageInterface::getHeader()
|
||||
* @param string|string[] $values Header value as returned by
|
||||
* MessageInterface::getHeader()
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
|
|
@ -142,7 +132,7 @@ final class Header
|
|||
}
|
||||
|
||||
if (!$isQuoted && $value[$i] === ',') {
|
||||
$v = \trim($v);
|
||||
$v = \trim($v, " \t\n\r");
|
||||
if ($v !== '') {
|
||||
$result[] = $v;
|
||||
}
|
||||
|
|
@ -167,7 +157,7 @@ final class Header
|
|||
$v .= $value[$i];
|
||||
}
|
||||
|
||||
$v = \trim($v);
|
||||
$v = \trim($v, " \t\n\r");
|
||||
if ($v !== '') {
|
||||
$result[] = $v;
|
||||
}
|
||||
|
|
|
|||
18
vendor/guzzlehttp/psr7/src/HttpFactory.php
vendored
18
vendor/guzzlehttp/psr7/src/HttpFactory.php
vendored
|
|
@ -36,7 +36,13 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter
|
|||
$size = $stream->getSize();
|
||||
}
|
||||
|
||||
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
|
||||
return new UploadedFile(
|
||||
$stream,
|
||||
Integers::assertOptionalNonNegativeSize($size, 'Uploaded file size'),
|
||||
$error,
|
||||
$clientFilename,
|
||||
$clientMediaType
|
||||
);
|
||||
}
|
||||
|
||||
public function createStream(string $content = ''): StreamInterface
|
||||
|
|
@ -50,7 +56,7 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter
|
|||
$resource = Utils::tryFopen($file, $mode);
|
||||
} catch (\RuntimeException $e) {
|
||||
if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid file opening mode "%s"', $mode), 0, $e);
|
||||
throw new \InvalidArgumentException(sprintf('Invalid file opening mode: %s', DiagnosticValue::escape($mode)), 0, $e);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
|
|
@ -64,8 +70,12 @@ final class HttpFactory implements RequestFactoryInterface, ResponseFactoryInter
|
|||
return Utils::streamFor($resource);
|
||||
}
|
||||
|
||||
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
|
||||
{
|
||||
public function createServerRequest(
|
||||
string $method,
|
||||
$uri,
|
||||
#[\SensitiveParameter]
|
||||
array $serverParams = []
|
||||
): ServerRequestInterface {
|
||||
if (empty($method)) {
|
||||
if (!empty($serverParams['REQUEST_METHOD'])) {
|
||||
$method = $serverParams['REQUEST_METHOD'];
|
||||
|
|
|
|||
68
vendor/guzzlehttp/psr7/src/InflateStream.php
vendored
68
vendor/guzzlehttp/psr7/src/InflateStream.php
vendored
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use GuzzleHttp\Psr7\Exception\TimeoutException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -20,12 +21,15 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class InflateStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
private ?StreamInterface $source;
|
||||
|
||||
public function __construct(StreamInterface $stream)
|
||||
{
|
||||
$this->source = $stream;
|
||||
$resource = StreamWrapper::getResource($stream);
|
||||
// Specify window=15+32, so zlib will use header detection to both gzip (with header) and zlib data
|
||||
// See https://www.zlib.net/manual.html#Advanced definition of inflateInit2
|
||||
|
|
@ -34,4 +38,64 @@ final class InflateStream implements StreamInterface
|
|||
stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 15 + 32]);
|
||||
$this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource));
|
||||
}
|
||||
|
||||
public function read(int $length): string
|
||||
{
|
||||
if ($length <= 0 || $this->source === null) {
|
||||
return $this->stream->read($length);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->stream->read($length);
|
||||
} catch (TimeoutException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
if (StreamTimeout::isReadTimedOut($this->source)) {
|
||||
throw new TimeoutException('Unable to read from stream: timed out', 0, $e);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($data === '' && StreamTimeout::isReadTimedOut($this->source)) {
|
||||
throw new TimeoutException('Unable to read from stream: timed out');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
$source = $this->source;
|
||||
$this->source = null;
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
$this->stream->close();
|
||||
} catch (\Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
if ($source !== null) {
|
||||
try {
|
||||
$source->close();
|
||||
} catch (\Throwable $e) {
|
||||
if ($exception === null) {
|
||||
$exception = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($exception !== null) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function detach()
|
||||
{
|
||||
$this->source = null;
|
||||
|
||||
return $this->stream->detach();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
vendor/guzzlehttp/psr7/src/Integers.php
vendored
Normal file
104
vendor/guzzlehttp/psr7/src/Integers.php
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Integers
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function add(int $a, int $b): int
|
||||
{
|
||||
if ($a < 0 || $b < 0) {
|
||||
throw new \InvalidArgumentException('Integer operands must be non-negative');
|
||||
}
|
||||
|
||||
if ($b > \PHP_INT_MAX - $a) {
|
||||
throw new \OverflowException('Stream byte count exceeds the maximum integer size supported on this platform');
|
||||
}
|
||||
|
||||
return $a + $b;
|
||||
}
|
||||
|
||||
public static function addSigned(int $base, int $delta): int
|
||||
{
|
||||
if ($base < 0) {
|
||||
throw new \InvalidArgumentException('Stream offset must be non-negative');
|
||||
}
|
||||
|
||||
if ($delta > 0 && $delta > \PHP_INT_MAX - $base) {
|
||||
throw new \OverflowException('Stream offset exceeds the maximum integer size supported on this platform');
|
||||
}
|
||||
|
||||
$value = $base + $delta;
|
||||
if ($value < 0) {
|
||||
// A negative computed offset is a seek failure at runtime, so throw
|
||||
// RuntimeException per PSR-7, unlike the precondition check above.
|
||||
throw new \RuntimeException('Stream offset must be non-negative');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function assertEngineInteger($value, string $what): ?int
|
||||
{
|
||||
if ($value === false || $value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_int($value) || $value < 0) {
|
||||
throw new \OverflowException($what.' exceeds the maximum integer size supported on this platform');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function assertOptionalNonNegativeSize($value, string $name): ?int
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_int($value) || $value < 0) {
|
||||
throw new \InvalidArgumentException($name.' must be a non-negative integer or null');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function assertNonNegativeInteger($value, string $name): int
|
||||
{
|
||||
if (!\is_int($value) || $value < 0) {
|
||||
throw new \InvalidArgumentException($name.' must be a non-negative integer');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function assertLimitInteger($value, string $name): int
|
||||
{
|
||||
if (!\is_int($value) || $value < -1) {
|
||||
throw new \InvalidArgumentException($name.' must be -1 or a non-negative integer');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
19
vendor/guzzlehttp/psr7/src/LazyOpenStream.php
vendored
19
vendor/guzzlehttp/psr7/src/LazyOpenStream.php
vendored
|
|
@ -13,17 +13,13 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class LazyOpenStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var string */
|
||||
private $filename;
|
||||
private string $filename;
|
||||
|
||||
/** @var string */
|
||||
private $mode;
|
||||
private string $mode;
|
||||
|
||||
/**
|
||||
* @var StreamInterface
|
||||
*/
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
/**
|
||||
* @param string $filename File to lazily open
|
||||
|
|
@ -39,6 +35,13 @@ final class LazyOpenStream implements StreamInterface
|
|||
unset($this->stream);
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->stream = new BufferStream();
|
||||
|
||||
throw new \LogicException(static::class.' should never be unserialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the underlying stream lazily when required.
|
||||
*/
|
||||
|
|
|
|||
99
vendor/guzzlehttp/psr7/src/LimitStream.php
vendored
99
vendor/guzzlehttp/psr7/src/LimitStream.php
vendored
|
|
@ -12,15 +12,15 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class LimitStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var int Offset to start reading from */
|
||||
private $offset;
|
||||
private int $offset;
|
||||
|
||||
/** @var int Limit the number of bytes that can be read */
|
||||
private $limit;
|
||||
private int $limit;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
/**
|
||||
* @param StreamInterface $stream Stream to wrap
|
||||
|
|
@ -51,7 +51,7 @@ final class LimitStream implements StreamInterface
|
|||
return false;
|
||||
}
|
||||
|
||||
return $this->stream->tell() >= $this->offset + $this->limit;
|
||||
return $this->stream->tell() >= Integers::add($this->offset, $this->limit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -75,26 +75,8 @@ final class LimitStream implements StreamInterface
|
|||
/**
|
||||
* Allow for a bounded seek on the read limited stream
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
if ($whence !== SEEK_SET || $offset < 0) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'Cannot seek to offset %s with whence %s',
|
||||
|
|
@ -103,11 +85,12 @@ final class LimitStream implements StreamInterface
|
|||
));
|
||||
}
|
||||
|
||||
$offset += $this->offset;
|
||||
$offset = Integers::add($this->offset, $offset);
|
||||
|
||||
if ($this->limit !== -1) {
|
||||
if ($offset > $this->offset + $this->limit) {
|
||||
$offset = $this->offset + $this->limit;
|
||||
$upperBound = Integers::add($this->offset, $this->limit);
|
||||
if ($offset > $upperBound) {
|
||||
$offset = $upperBound;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,17 +114,48 @@ final class LimitStream implements StreamInterface
|
|||
*/
|
||||
public function setOffset(int $offset): void
|
||||
{
|
||||
$offset = Integers::assertNonNegativeInteger($offset, 'Offset');
|
||||
|
||||
$current = $this->stream->tell();
|
||||
|
||||
if ($current !== $offset) {
|
||||
// If the stream cannot seek to the offset position, then read to it
|
||||
if ($this->stream->isSeekable()) {
|
||||
$this->stream->seek($offset);
|
||||
} elseif ($current > $offset) {
|
||||
throw new \RuntimeException("Could not seek to stream offset $offset");
|
||||
} else {
|
||||
$this->stream->read($offset - $current);
|
||||
if ($current === $offset) {
|
||||
$this->offset = $offset;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the stream cannot seek to the offset position, then read to it.
|
||||
if ($this->stream->isSeekable()) {
|
||||
$this->stream->seek($offset);
|
||||
$this->offset = $offset;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($current > $offset) {
|
||||
throw new \RuntimeException("Could not seek to stream offset $offset");
|
||||
}
|
||||
|
||||
while ($current < $offset) {
|
||||
if ($this->stream->eof()) {
|
||||
$this->offset = $current;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->stream->read($offset - $current);
|
||||
|
||||
if ($result === '') {
|
||||
if ($this->stream->eof()) {
|
||||
$this->offset = $current;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Could not seek to stream offset $offset");
|
||||
}
|
||||
|
||||
$current = Integers::add($current, strlen($result));
|
||||
}
|
||||
|
||||
$this->offset = $offset;
|
||||
|
|
@ -156,18 +170,13 @@ final class LimitStream implements StreamInterface
|
|||
*/
|
||||
public function setLimit(int $limit): void
|
||||
{
|
||||
$this->limit = $limit;
|
||||
$this->limit = Integers::assertLimitInteger($limit, 'Limit');
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
if ($this->limit === -1) {
|
||||
|
|
@ -176,7 +185,7 @@ final class LimitStream implements StreamInterface
|
|||
|
||||
// Check if the current position is less than the total allowed
|
||||
// bytes + original offset
|
||||
$remaining = ($this->offset + $this->limit) - $this->stream->tell();
|
||||
$remaining = Integers::add($this->offset, $this->limit) - $this->stream->tell();
|
||||
if ($remaining > 0) {
|
||||
// Only return the amount of requested data, ensuring that the byte
|
||||
// limit is not exceeded
|
||||
|
|
|
|||
203
vendor/guzzlehttp/psr7/src/Message.php
vendored
203
vendor/guzzlehttp/psr7/src/Message.php
vendored
|
|
@ -7,9 +7,16 @@ namespace GuzzleHttp\Psr7;
|
|||
use Psr\Http\Message\MessageInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
final class Message
|
||||
{
|
||||
private const DEFAULT_BODY_SUMMARY_TRUNCATE_AT = 120;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of an HTTP message.
|
||||
*
|
||||
|
|
@ -19,10 +26,10 @@ 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();
|
||||
$msg .= "\r\nHost: ".self::hostHeaderFromUri($message->getUri());
|
||||
}
|
||||
} elseif ($message instanceof ResponseInterface) {
|
||||
$msg = 'HTTP/'.$message->getProtocolVersion().' '
|
||||
|
|
@ -33,7 +40,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;
|
||||
}
|
||||
|
|
@ -45,16 +52,39 @@ final class Message
|
|||
return "{$msg}\r\n\r\n".$message->getBody();
|
||||
}
|
||||
|
||||
private static function hostHeaderFromUri(UriInterface $uri): string
|
||||
{
|
||||
$host = $uri->getHost();
|
||||
|
||||
if ($host === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
Uri::assertValidHost($host);
|
||||
|
||||
if (($port = $uri->getPort()) !== null) {
|
||||
$host .= ':'.$port;
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a short summary of the message body.
|
||||
*
|
||||
* Will return `null` if the response is not printable.
|
||||
*
|
||||
* Reads seekable bodies from the beginning and restores the original cursor
|
||||
* position before returning. Pass `null` for `$truncateAt` to use the
|
||||
* default summary length.
|
||||
*
|
||||
* @param MessageInterface $message The message to get the body summary
|
||||
* @param int $truncateAt The maximum allowed size of the summary
|
||||
* @param int|null $truncateAt Maximum allowed size of the summary
|
||||
*/
|
||||
public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string
|
||||
public static function bodySummary(MessageInterface $message, ?int $truncateAt = null): ?string
|
||||
{
|
||||
$truncateAt ??= self::DEFAULT_BODY_SUMMARY_TRUNCATE_AT;
|
||||
|
||||
$body = $message->getBody();
|
||||
|
||||
if (!$body->isSeekable() || !$body->isReadable()) {
|
||||
|
|
@ -67,19 +97,23 @@ final class Message
|
|||
return null;
|
||||
}
|
||||
|
||||
$body->rewind();
|
||||
$summary = $body->read($truncateAt);
|
||||
$position = $body->tell();
|
||||
|
||||
if ($size > $truncateAt) {
|
||||
if (preg_match('//u', $summary) !== 1) {
|
||||
$summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
|
||||
try {
|
||||
$body->rewind();
|
||||
$summary = $body->read($truncateAt);
|
||||
|
||||
if ($size > $truncateAt) {
|
||||
if (preg_match('//u', $summary) !== 1) {
|
||||
$summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
|
||||
}
|
||||
|
||||
$summary .= ' (truncated...)';
|
||||
}
|
||||
|
||||
$summary .= ' (truncated...)';
|
||||
} finally {
|
||||
$body->seek($position);
|
||||
}
|
||||
|
||||
$body->rewind();
|
||||
|
||||
// Matches any printable character, including unicode characters:
|
||||
// letters, marks, numbers, punctuation, spacing, and separators.
|
||||
if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) {
|
||||
|
|
@ -165,135 +199,49 @@ final class Message
|
|||
/**
|
||||
* Parses an HTTP message into an associative array.
|
||||
*
|
||||
* The array contains the "start-line" key containing the start line of
|
||||
* the message, "headers" key containing an associative array of header
|
||||
* array values, and a "body" key containing the body of the message.
|
||||
* The array contains the `start-line` key containing the start line of the
|
||||
* message, `headers` key containing an associative array of header array
|
||||
* values, and a `body` key containing the body of the message.
|
||||
*
|
||||
* @param string $message HTTP request or response to parse.
|
||||
*/
|
||||
public static function parseMessage(string $message): array
|
||||
{
|
||||
if (!$message) {
|
||||
throw new \InvalidArgumentException('Invalid message');
|
||||
}
|
||||
|
||||
$message = ltrim($message, "\r\n");
|
||||
|
||||
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
|
||||
|
||||
if ($messageParts === false || count($messageParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
|
||||
}
|
||||
|
||||
[$rawHeaders, $body] = $messageParts;
|
||||
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
|
||||
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
|
||||
|
||||
if ($headerParts === false || count($headerParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing status line');
|
||||
}
|
||||
|
||||
[$startLine, $rawHeaders] = $headerParts;
|
||||
|
||||
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
|
||||
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
|
||||
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
|
||||
}
|
||||
|
||||
/** @var array[] $headerLines */
|
||||
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
|
||||
|
||||
// If these aren't the same, then one line didn't match and there's an invalid header.
|
||||
if ($count !== substr_count($rawHeaders, "\n")) {
|
||||
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
|
||||
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
|
||||
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Invalid header syntax');
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($headerLines as $headerLine) {
|
||||
$headers[$headerLine[1]][] = $headerLine[2];
|
||||
}
|
||||
|
||||
return [
|
||||
'start-line' => $startLine,
|
||||
'headers' => $headers,
|
||||
'body' => $body,
|
||||
];
|
||||
return MessageParser::parseMessage($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a URI for an HTTP request message.
|
||||
*
|
||||
* The URI is composed from the start-line path and the `Host` header, using
|
||||
* `https` when the host's port is `443` and `http` otherwise. Without a
|
||||
* `Host` header, only the path is returned, with extra leading slashes
|
||||
* collapsed so an origin-form target cannot be parsed as a network-path
|
||||
* reference with its own authority. An `InvalidArgumentException` is thrown
|
||||
* when the `Host` header is invalid.
|
||||
*
|
||||
* @param string $path Path from the start-line
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
public static function parseRequestUri(string $path, array $headers): string
|
||||
{
|
||||
$host = self::getHostFromHeaders($headers);
|
||||
|
||||
// If no host is found, then a full URI cannot be constructed.
|
||||
if ($host === null) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
|
||||
|
||||
return $scheme.'://'.$host.'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
private static function getHostFromHeaders(array $headers): ?string
|
||||
{
|
||||
$hostKey = array_filter(array_keys($headers), function ($k) {
|
||||
// Numeric array keys are converted to int by PHP.
|
||||
$k = (string) $k;
|
||||
|
||||
return strtolower($k) === 'host';
|
||||
});
|
||||
|
||||
if (!$hostKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = $headers[reset($hostKey)][0];
|
||||
if (!is_string($host) || Rfc7230::parseHostHeader($host) === null) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
return $host;
|
||||
return MessageParser::parseRequestUri($path, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a request message string into a request object.
|
||||
*
|
||||
* The request-target must be in origin form, absolute form (without a
|
||||
* userinfo component), authority form (`CONNECT`), or asterisk form
|
||||
* (`OPTIONS`), and any `Host` header must be a single valid value;
|
||||
* otherwise an `InvalidArgumentException` is thrown. Non-origin-form
|
||||
* targets are preserved on the returned request via `withRequestTarget()`.
|
||||
*
|
||||
* @param string $message Request message string.
|
||||
*/
|
||||
public static function parseRequest(string $message): RequestInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
$matches = [];
|
||||
if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
$parts = explode(' ', $data['start-line'], 3);
|
||||
$version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1';
|
||||
|
||||
$request = new Request(
|
||||
$parts[0],
|
||||
$matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1],
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
$version
|
||||
);
|
||||
|
||||
return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]);
|
||||
return MessageParser::parseRequest($message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -303,21 +251,6 @@ final class Message
|
|||
*/
|
||||
public static function parseResponse(string $message): ResponseInterface
|
||||
{
|
||||
$data = self::parseMessage($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.
|
||||
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
|
||||
throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
|
||||
}
|
||||
$parts = explode(' ', $data['start-line'], 3);
|
||||
|
||||
return new Response(
|
||||
(int) $parts[1],
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
explode('/', $parts[0])[1],
|
||||
$parts[2] ?? null
|
||||
);
|
||||
return MessageParser::parseResponse($message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
363
vendor/guzzlehttp/psr7/src/MessageParser.php
vendored
Normal file
363
vendor/guzzlehttp/psr7/src/MessageParser.php
vendored
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MessageParser
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function parseMessage(string $message): array
|
||||
{
|
||||
if (!$message) {
|
||||
throw new \InvalidArgumentException('Invalid message');
|
||||
}
|
||||
|
||||
$message = ltrim($message, "\r\n");
|
||||
|
||||
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
|
||||
|
||||
if ($messageParts === false) {
|
||||
throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if (count($messageParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
|
||||
}
|
||||
|
||||
[$rawHeaders, $body] = $messageParts;
|
||||
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
|
||||
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
|
||||
|
||||
if ($headerParts === false) {
|
||||
throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if (count($headerParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing status line');
|
||||
}
|
||||
|
||||
[$startLine, $rawHeaders] = $headerParts;
|
||||
|
||||
$versionMatch = preg_match(
|
||||
'/(?:^HTTP\/|^'.Rfc9110::TOKEN_PATTERN.' '.Rfc9112::REQUEST_TARGET_PATTERN.' HTTP\/)('.Rfc9112::PROTOCOL_VERSION_PATTERN.')/i',
|
||||
$startLine,
|
||||
$matches
|
||||
);
|
||||
|
||||
if ($versionMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($versionMatch === 1 && $matches[1] === '1.0') {
|
||||
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
|
||||
$rawHeaders = preg_replace(Rfc9112::HEADER_FOLD_REGEX, ' ', $rawHeaders);
|
||||
|
||||
if ($rawHeaders === null) {
|
||||
throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
$count = preg_match_all(Rfc9112::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
|
||||
/** @var list<array<int, string>> $headerLines */
|
||||
if ($count === false) {
|
||||
throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
// If these aren't the same, then one line didn't match and there's an invalid header.
|
||||
if ($count !== substr_count($rawHeaders, "\n")) {
|
||||
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc9112#section-5.2
|
||||
$hasFoldedHeader = preg_match(Rfc9112::HEADER_FOLD_REGEX, $rawHeaders);
|
||||
|
||||
if ($hasFoldedHeader === false) {
|
||||
throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($hasFoldedHeader === 1) {
|
||||
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Invalid header syntax');
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($headerLines as $headerLine) {
|
||||
$headers[$headerLine[1]][] = $headerLine[2];
|
||||
}
|
||||
|
||||
return [
|
||||
'start-line' => $startLine,
|
||||
'headers' => $headers,
|
||||
'body' => $body,
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseRequestUri(string $path, array $headers): string
|
||||
{
|
||||
$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 self::normalizePathForOriginForm($path);
|
||||
}
|
||||
|
||||
[$authorityHost, $port] = self::parseHostHeaderAuthority($host);
|
||||
$scheme = $port === 443 ? 'https' : 'http';
|
||||
|
||||
return $scheme.'://'.self::composeAuthority($authorityHost, $port).'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
private static function normalizePathForOriginForm(string $path): string
|
||||
{
|
||||
if (str_starts_with($path, '//')) {
|
||||
return '/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: int|null}
|
||||
*/
|
||||
private static function parseHostHeaderAuthority(string $authority): array
|
||||
{
|
||||
$parsed = Rfc9112::parseHostHeader($authority);
|
||||
if ($parsed === null) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
private static function composeAuthority(string $host, ?int $port): string
|
||||
{
|
||||
return $host.($port !== null ? ':'.$port : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
private static function getHostFromHeaders(array $headers): ?string
|
||||
{
|
||||
$host = self::getSingleHostHeader($headers);
|
||||
if ($host === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
self::parseHostHeaderAuthority($host);
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
private static function getSingleHostHeader(array $headers): ?string
|
||||
{
|
||||
$host = null;
|
||||
$found = false;
|
||||
|
||||
foreach ($headers as $name => $values) {
|
||||
if (Utils::asciiToLower((string) $name) !== 'host') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($found || !is_array($values) || count($values) !== 1) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
$found = true;
|
||||
$host = reset($values);
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_string($host)) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
private static function parseRequestAuthorityUri(array $headers): string
|
||||
{
|
||||
$host = self::getHostFromHeaders($headers);
|
||||
if ($host === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
[$authorityHost, $port] = self::parseHostHeaderAuthority($host);
|
||||
$scheme = $port === 443 ? 'https' : 'http';
|
||||
|
||||
return $scheme.'://'.self::composeAuthority($authorityHost, $port);
|
||||
}
|
||||
|
||||
public static function parseRequest(string $message): RequestInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
$matches = [];
|
||||
$matched = preg_match(
|
||||
'/^(?P<method>'.Rfc9110::TOKEN_PATTERN.') (?P<target>'.Rfc9112::REQUEST_TARGET_PATTERN.') HTTP\/(?P<version>'.Rfc9112::PROTOCOL_VERSION_PATTERN.')$/D',
|
||||
$data['start-line'],
|
||||
$matches
|
||||
);
|
||||
|
||||
if ($matched === false) {
|
||||
throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($matched === 0) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
self::getHostFromHeaders($data['headers']);
|
||||
|
||||
if (str_starts_with($matches['target'], '/')) {
|
||||
return new Request(
|
||||
$matches['method'],
|
||||
self::parseRequestUri($matches['target'], $data['headers']),
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
$matches['version']
|
||||
);
|
||||
}
|
||||
|
||||
$absoluteFormUri = self::parseAbsoluteFormRequestTarget($matches['target']);
|
||||
if ($absoluteFormUri !== null) {
|
||||
return (new Request(
|
||||
$matches['method'],
|
||||
$absoluteFormUri,
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
$matches['version']
|
||||
))->withRequestTarget($matches['target']);
|
||||
}
|
||||
|
||||
if (Rfc9112::isAsteriskFormRequestTarget($matches['method'], $matches['target'])) {
|
||||
return (new Request(
|
||||
$matches['method'],
|
||||
self::parseRequestAuthorityUri($data['headers']),
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
$matches['version']
|
||||
))->withRequestTarget($matches['target']);
|
||||
}
|
||||
|
||||
$connectUri = self::parseConnectAuthorityFormRequestTarget($matches['method'], $matches['target']);
|
||||
if ($connectUri !== null) {
|
||||
return (new Request(
|
||||
$matches['method'],
|
||||
$connectUri,
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
$matches['version']
|
||||
))->withRequestTarget($matches['target']);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
private static function parseAbsoluteFormRequestTarget(string $target): ?Uri
|
||||
{
|
||||
if (!Rfc9112::isAbsoluteFormRequestTarget($target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$authority = substr($target, strpos($target, '//') + 2);
|
||||
$authority = substr($authority, 0, strcspn($authority, '/?#'));
|
||||
|
||||
// RFC 9110 deprecates userinfo in message target URIs and directs
|
||||
// recipients to treat its presence as an error, since it can obscure
|
||||
// the authority. Host headers and CONNECT targets already reject it.
|
||||
if (str_contains($authority, '@')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$uri = new Uri($target);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($uri->getHost() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
self::parseHostHeaderAuthority(self::composeAuthority($uri->getHost(), $uri->getPort()));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private static function parseConnectAuthorityFormRequestTarget(string $method, string $target): ?Uri
|
||||
{
|
||||
if (!Rfc9112::isConnectAuthorityFormRequestTarget($method, $target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parsed = Rfc9112::parseHostHeader($target);
|
||||
if ($parsed === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$host, $port] = $parsed;
|
||||
if ($port === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Uri('//'.self::composeAuthority($host, $port));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseResponse(string $message): ResponseInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
// According to https://datatracker.ietf.org/doc/html/rfc9112#section-4
|
||||
// the space between status-code and reason-phrase is required. But
|
||||
// browsers accept responses without space and reason as well.
|
||||
$matched = preg_match(
|
||||
'/^HTTP\/(?P<version>'.Rfc9112::PROTOCOL_VERSION_PATTERN.') (?P<status>[1-5][0-9]{2})(?: (?P<reason>'.Rfc9110::FIELD_VALUE_PATTERN.'))?$/D',
|
||||
$data['start-line'],
|
||||
$matches
|
||||
);
|
||||
|
||||
if ($matched === false) {
|
||||
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($matched === 0) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid response string: %s', DiagnosticValue::escape($data['start-line'])));
|
||||
}
|
||||
|
||||
return new Response(
|
||||
(int) $matches['status'],
|
||||
$data['headers'],
|
||||
$data['body'],
|
||||
$matches['version'],
|
||||
$matches['reason'] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
174
vendor/guzzlehttp/psr7/src/MessageTrait.php
vendored
174
vendor/guzzlehttp/psr7/src/MessageTrait.php
vendored
|
|
@ -13,16 +13,14 @@ use Psr\Http\Message\StreamInterface;
|
|||
trait MessageTrait
|
||||
{
|
||||
/** @var string[][] Map of all registered headers, as original name => array of values */
|
||||
private $headers = [];
|
||||
private array $headers = [];
|
||||
|
||||
/** @var string[] Map of lowercase header name => original name at registration */
|
||||
private $headerNames = [];
|
||||
private array $headerNames = [];
|
||||
|
||||
/** @var string */
|
||||
private $protocol = '1.1';
|
||||
private string $protocol = '1.1';
|
||||
|
||||
/** @var StreamInterface|null */
|
||||
private $stream;
|
||||
private ?StreamInterface $stream = null;
|
||||
|
||||
public function getProtocolVersion(): string
|
||||
{
|
||||
|
|
@ -32,16 +30,9 @@ trait MessageTrait
|
|||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function withProtocolVersion($version): MessageInterface
|
||||
public function withProtocolVersion(string $version): MessageInterface
|
||||
{
|
||||
if (!\is_string($version)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to MessageInterface::withProtocolVersion() is deprecated; guzzlehttp/psr7 3.0 requires string.',
|
||||
\get_debug_type($version)
|
||||
);
|
||||
}
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
if ($this->protocol === $version) {
|
||||
return $this;
|
||||
|
|
@ -58,14 +49,14 @@ trait MessageTrait
|
|||
return $this->headers;
|
||||
}
|
||||
|
||||
public function hasHeader($header): bool
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
return isset($this->headerNames[strtolower($header)]);
|
||||
return isset($this->headerNames[Utils::asciiToLower($name)]);
|
||||
}
|
||||
|
||||
public function getHeader($header): array
|
||||
public function getHeader(string $name): array
|
||||
{
|
||||
$header = strtolower($header);
|
||||
$header = Utils::asciiToLower($name);
|
||||
|
||||
if (!isset($this->headerNames[$header])) {
|
||||
return [];
|
||||
|
|
@ -76,39 +67,26 @@ trait MessageTrait
|
|||
return $this->headers[$header];
|
||||
}
|
||||
|
||||
public function getHeaderLine($header): string
|
||||
public function getHeaderLine(string $name): string
|
||||
{
|
||||
return implode(', ', $this->getHeader($header));
|
||||
return implode(', ', $this->getHeader($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function withHeader($header, $value): MessageInterface
|
||||
public function withHeader(string $name, $value): MessageInterface
|
||||
{
|
||||
$this->assertHeader($header);
|
||||
$values = \is_array($value) ? $value : [$value];
|
||||
foreach ($values as $item) {
|
||||
if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to MessageInterface::withHeader() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
|
||||
\get_debug_type($item)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertHeader($name);
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($name);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
unset($new->headers[$new->headerNames[$normalized]]);
|
||||
}
|
||||
$new->headerNames[$normalized] = $header;
|
||||
$new->headers[$header] = $value;
|
||||
$new->headerNames[$normalized] = $name;
|
||||
$new->headers[$name] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
|
@ -116,32 +94,19 @@ trait MessageTrait
|
|||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function withAddedHeader($header, $value): MessageInterface
|
||||
public function withAddedHeader(string $name, $value): MessageInterface
|
||||
{
|
||||
$this->assertHeader($header);
|
||||
$values = \is_array($value) ? $value : [$value];
|
||||
foreach ($values as $item) {
|
||||
if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to MessageInterface::withAddedHeader() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
|
||||
\get_debug_type($item)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertHeader($name);
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($name);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
$header = $this->headerNames[$normalized];
|
||||
$new->headers[$header] = array_merge($this->headers[$header], $value);
|
||||
$name = $this->headerNames[$normalized];
|
||||
$new->headers[$name] = array_merge($this->headers[$name], $value);
|
||||
} else {
|
||||
$new->headerNames[$normalized] = $header;
|
||||
$new->headers[$header] = $value;
|
||||
$new->headerNames[$normalized] = $name;
|
||||
$new->headers[$name] = $value;
|
||||
}
|
||||
|
||||
return $new;
|
||||
|
|
@ -150,18 +115,18 @@ trait MessageTrait
|
|||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function withoutHeader($header): MessageInterface
|
||||
public function withoutHeader(string $name): MessageInterface
|
||||
{
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($name);
|
||||
|
||||
if (!isset($this->headerNames[$normalized])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$header = $this->headerNames[$normalized];
|
||||
$name = $this->headerNames[$normalized];
|
||||
|
||||
$new = clone $this;
|
||||
unset($new->headers[$header], $new->headerNames[$normalized]);
|
||||
unset($new->headers[$name], $new->headerNames[$normalized]);
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
|
@ -201,22 +166,8 @@ trait MessageTrait
|
|||
$header = (string) $header;
|
||||
|
||||
$this->assertHeader($header);
|
||||
$values = \is_array($value) ? $value : [$value];
|
||||
foreach ($values as $item) {
|
||||
if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to %s::__construct() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
|
||||
\get_debug_type($item),
|
||||
static::class
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$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);
|
||||
|
|
@ -235,11 +186,7 @@ trait MessageTrait
|
|||
private function normalizeHeaderValue($value): array
|
||||
{
|
||||
if (is_array($value) && $value === []) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing an empty array as a header value is deprecated; guzzlehttp/psr7 3.0 rejects empty header value arrays.'
|
||||
);
|
||||
throw new \InvalidArgumentException('Header value must be a non-empty array or string.');
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
|
|
@ -261,19 +208,19 @@ trait MessageTrait
|
|||
*
|
||||
* @return string[] Trimmed header values
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
|
||||
*/
|
||||
private function trimAndValidateHeaderValues(array $values): array
|
||||
{
|
||||
return array_map(function ($value) {
|
||||
if (!is_scalar($value) && null !== $value) {
|
||||
return array_map(function ($value): string {
|
||||
if (!is_string($value)) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Header value must be scalar or null but %s provided.',
|
||||
is_object($value) ? get_class($value) : gettype($value)
|
||||
'Header value must be a string or array of strings but %s provided.',
|
||||
\get_debug_type($value)
|
||||
));
|
||||
}
|
||||
|
||||
$trimmed = trim((string) $value, " \t");
|
||||
$trimmed = trim($value, " \t");
|
||||
$this->assertValue($trimmed);
|
||||
|
||||
return $trimmed;
|
||||
|
|
@ -281,28 +228,24 @@ trait MessageTrait
|
|||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
|
||||
*
|
||||
* @param mixed $header
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.1
|
||||
*/
|
||||
private function assertHeader($header): void
|
||||
private function assertHeader(string $header): void
|
||||
{
|
||||
if (!is_string($header)) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Header name must be a string but %s provided.',
|
||||
is_object($header) ? get_class($header) : gettype($header)
|
||||
));
|
||||
if (!Rfc9110::isToken($header)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid header name: %s', DiagnosticValue::escape($header)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('"%s" is not valid header name.', $header)
|
||||
);
|
||||
private function assertProtocolVersion(string $version): void
|
||||
{
|
||||
if (!Rfc9112::isValidProtocolVersion($version)) {
|
||||
throw new \InvalidArgumentException('Protocol version must be a valid HTTP version number.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
|
||||
*
|
||||
* field-value = *( field-content / obs-fold )
|
||||
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||
|
|
@ -313,21 +256,20 @@ trait MessageTrait
|
|||
*/
|
||||
private function assertValue(string $value): void
|
||||
{
|
||||
// The regular expression intentionally does not support the obs-fold production, because as
|
||||
// per RFC 7230#3.2.4:
|
||||
// The regular expression intentionally does not support the obs-fold
|
||||
// production, because as per RFC 9112#5.2:
|
||||
//
|
||||
// A sender MUST NOT generate a message that includes
|
||||
// line folding (i.e., that has any field-value that contains a match to
|
||||
// the obs-fold rule) unless the message is intended for packaging
|
||||
// within the message/http media type.
|
||||
// A sender MUST NOT generate a message that includes line folding
|
||||
// (i.e., that has any field-value that contains a match to the obs-fold
|
||||
// rule) unless the message is intended for packaging within the
|
||||
// message/http media type.
|
||||
//
|
||||
// Clients must not send a request with line folding and a server sending folded headers is
|
||||
// likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting
|
||||
// folding is not likely to break any legitimate use case.
|
||||
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('"%s" is not valid header value.', $value)
|
||||
);
|
||||
// Clients must not send a request with line folding and a server
|
||||
// sending folded headers is likely very rare. Line folding is a fairly
|
||||
// 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
vendor/guzzlehttp/psr7/src/MimeType.php
vendored
10
vendor/guzzlehttp/psr7/src/MimeType.php
vendored
|
|
@ -6,6 +6,10 @@ namespace GuzzleHttp\Psr7;
|
|||
|
||||
final class MimeType
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
private const MIME_TYPES = [
|
||||
'123' => 'application/vnd.lotus-1-2-3',
|
||||
'1km' => 'application/vnd.1000minds.decision-model+xml',
|
||||
|
|
@ -1284,7 +1288,7 @@ final class MimeType
|
|||
];
|
||||
|
||||
/**
|
||||
* Determines the mimetype of a file by looking at its extension.
|
||||
* Determines the MIME type of a file by looking at its extension.
|
||||
*
|
||||
* @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json
|
||||
*/
|
||||
|
|
@ -1294,12 +1298,12 @@ final class MimeType
|
|||
}
|
||||
|
||||
/**
|
||||
* Maps a file extensions to a mimetype.
|
||||
* Maps a file extension to a MIME type.
|
||||
*
|
||||
* @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json
|
||||
*/
|
||||
public static function fromExtension(string $extension): ?string
|
||||
{
|
||||
return self::MIME_TYPES[strtolower($extension)] ?? null;
|
||||
return self::MIME_TYPES[Utils::asciiToLower($extension)] ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
119
vendor/guzzlehttp/psr7/src/MultipartStream.php
vendored
119
vendor/guzzlehttp/psr7/src/MultipartStream.php
vendored
|
|
@ -13,12 +13,11 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class MultipartStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var string */
|
||||
private $boundary;
|
||||
private string $boundary;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
private const BOUNDARY_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=? ";
|
||||
|
||||
|
|
@ -26,8 +25,9 @@ final class MultipartStream implements StreamInterface
|
|||
* @param array $elements Array of associative arrays, each containing a
|
||||
* required "name" key mapping to the form field,
|
||||
* name, a required "contents" key mapping to any
|
||||
* non-array value accepted by Utils::streamFor(),
|
||||
* or an array for nested expansion.
|
||||
* non-array value accepted by Utils::streamFor()
|
||||
* (non-string scalar field values are cast to
|
||||
* string), or an array for nested expansion.
|
||||
* Optional keys include "headers" (associative
|
||||
* array of custom headers) and "filename" (string
|
||||
* to send as the filename in the part).
|
||||
|
|
@ -42,15 +42,11 @@ final class MultipartStream implements StreamInterface
|
|||
*/
|
||||
public function __construct(array $elements = [], ?string $boundary = null)
|
||||
{
|
||||
if ($boundary !== null && !self::isValidBoundary($boundary)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing an invalid multipart boundary to MultipartStream::__construct() is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart boundaries.'
|
||||
);
|
||||
if ($boundary !== null) {
|
||||
self::validateBoundary($boundary);
|
||||
}
|
||||
|
||||
$this->boundary = $boundary ?: bin2hex(random_bytes(20));
|
||||
$this->boundary = $boundary ?? bin2hex(random_bytes(20));
|
||||
$this->stream = $this->createStream($elements);
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +70,14 @@ final class MultipartStream implements StreamInterface
|
|||
$str = '';
|
||||
foreach ($headers as $key => $value) {
|
||||
$key = (string) $key;
|
||||
|
||||
self::validatePartHeaderName($key);
|
||||
self::validatePartHeaderValue($value);
|
||||
|
||||
$str .= "{$key}: {$value}\r\n";
|
||||
}
|
||||
|
||||
return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n";
|
||||
return "--{$this->boundary}\r\n".rtrim($str, "\r\n")."\r\n\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,11 +124,23 @@ final class MultipartStream implements StreamInterface
|
|||
return;
|
||||
}
|
||||
|
||||
$element['contents'] = Utils::streamFor($element['contents']);
|
||||
$contents = $element['contents'];
|
||||
if (is_scalar($contents) && !is_string($contents)) {
|
||||
// Multipart field values are byte strings on the wire, so finite
|
||||
// numeric and boolean field values are cast to string here rather
|
||||
// than rejected by streamFor(). Non-finite floats cannot be
|
||||
// represented and are rejected.
|
||||
if (is_float($contents) && !is_finite($contents)) {
|
||||
throw new \InvalidArgumentException('Cannot create a stream from a non-finite float.');
|
||||
}
|
||||
|
||||
$contents = (string) $contents;
|
||||
}
|
||||
$element['contents'] = Utils::streamFor($contents);
|
||||
|
||||
if (empty($element['filename'])) {
|
||||
$uri = $element['contents']->getMetadata('uri');
|
||||
if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') {
|
||||
if ($uri && \is_string($uri) && !str_starts_with($uri, 'php://') && !str_starts_with($uri, 'data://')) {
|
||||
$element['filename'] = $uri;
|
||||
}
|
||||
}
|
||||
|
|
@ -175,21 +187,14 @@ final class MultipartStream implements StreamInterface
|
|||
// Set a default content-disposition header if one was no provided
|
||||
$disposition = self::getHeader($headers, 'content-disposition');
|
||||
if (!$disposition) {
|
||||
$escapedName = self::escapeContentDispositionParameter($name);
|
||||
$headers['Content-Disposition'] = ($filename === '0' || $filename)
|
||||
? sprintf(
|
||||
'form-data; name="%s"; filename="%s"',
|
||||
$name,
|
||||
basename($filename)
|
||||
$escapedName,
|
||||
self::escapeContentDispositionParameter(basename($filename))
|
||||
)
|
||||
: "form-data; name=\"{$name}\"";
|
||||
}
|
||||
|
||||
// Set a default content-length header if one was no provided
|
||||
$length = self::getHeader($headers, 'content-length');
|
||||
if (!$length) {
|
||||
if ($length = $stream->getSize()) {
|
||||
$headers['Content-Length'] = (string) $length;
|
||||
}
|
||||
: sprintf('form-data; name="%s"', $escapedName);
|
||||
}
|
||||
|
||||
// Set a default Content-Type if one was not supplied
|
||||
|
|
@ -206,9 +211,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -216,15 +221,17 @@ final class MultipartStream implements StreamInterface
|
|||
return null;
|
||||
}
|
||||
|
||||
private static function isValidBoundary(string $boundary): bool
|
||||
private static function validateBoundary(string $boundary): void
|
||||
{
|
||||
$length = strlen($boundary);
|
||||
|
||||
if ($length < 1 || $length > 70 || $boundary[$length - 1] === ' ') {
|
||||
return false;
|
||||
throw new \InvalidArgumentException('Invalid multipart boundary.');
|
||||
}
|
||||
|
||||
return strspn($boundary, self::BOUNDARY_CHARS) === $length;
|
||||
if (strspn($boundary, self::BOUNDARY_CHARS) !== $length) {
|
||||
throw new \InvalidArgumentException('Invalid multipart boundary.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -237,27 +244,15 @@ final class MultipartStream implements StreamInterface
|
|||
$normalized = [];
|
||||
|
||||
foreach ($headers as $key => $value) {
|
||||
self::deprecateInvalidPartHeaderName((string) $key);
|
||||
$key = (string) $key;
|
||||
|
||||
self::validatePartHeaderName($key);
|
||||
|
||||
if (!is_string($value)) {
|
||||
if (!is_scalar($value) && $value !== null && !(is_object($value) && method_exists($value, '__toString'))) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Multipart part header value must be a string or stringable value but %s provided.',
|
||||
\get_debug_type($value)
|
||||
));
|
||||
}
|
||||
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s as a multipart part header value is deprecated; guzzlehttp/psr7 3.0 requires string multipart part header values.',
|
||||
\get_debug_type($value)
|
||||
);
|
||||
throw new \InvalidArgumentException('Multipart part header value must be a string.');
|
||||
}
|
||||
|
||||
$value = (string) $value;
|
||||
|
||||
self::deprecateInvalidPartHeaderValue($value);
|
||||
self::validatePartHeaderValue($value);
|
||||
|
||||
$normalized[$key] = $value;
|
||||
}
|
||||
|
|
@ -265,25 +260,23 @@ final class MultipartStream implements StreamInterface
|
|||
return $normalized;
|
||||
}
|
||||
|
||||
private static function deprecateInvalidPartHeaderName(string $name): void
|
||||
private static function validatePartHeaderName(string $name): void
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing an invalid multipart part header name to MultipartStream is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart part header names.'
|
||||
);
|
||||
if (!Rfc9110::isToken($name)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid multipart part header name: %s', DiagnosticValue::escape($name)));
|
||||
}
|
||||
}
|
||||
|
||||
private static function deprecateInvalidPartHeaderValue(string $value): void
|
||||
private static function validatePartHeaderValue(string $value): void
|
||||
{
|
||||
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing an invalid multipart part header value to MultipartStream is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart part header values.'
|
||||
);
|
||||
if (!Rfc9110::isFieldValue($value)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid multipart part header value: %s', DiagnosticValue::escape($value)));
|
||||
}
|
||||
}
|
||||
|
||||
private static function escapeContentDispositionParameter(string $value): string
|
||||
{
|
||||
// Match WHATWG browser multipart/form-data behavior: escape CR, LF, and DQUOTE only.
|
||||
return str_replace(["\r", "\n", '"'], ['%0D', '%0A', '%22'], $value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
vendor/guzzlehttp/psr7/src/NoSeekStream.php
vendored
24
vendor/guzzlehttp/psr7/src/NoSeekStream.php
vendored
|
|
@ -12,30 +12,12 @@ use Psr\Http\Message\StreamInterface;
|
|||
final class NoSeekStream implements StreamInterface
|
||||
{
|
||||
use StreamDecoratorTrait;
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Cannot seek a NoSeekStream');
|
||||
}
|
||||
|
||||
|
|
|
|||
21
vendor/guzzlehttp/psr7/src/NonSerializableStreamTrait.php
vendored
Normal file
21
vendor/guzzlehttp/psr7/src/NonSerializableStreamTrait.php
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait NonSerializableStreamTrait
|
||||
{
|
||||
public function __serialize(): array
|
||||
{
|
||||
throw new \LogicException(static::class.' should never be serialized');
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
throw new \LogicException(static::class.' should never be unserialized');
|
||||
}
|
||||
}
|
||||
136
vendor/guzzlehttp/psr7/src/PumpStream.php
vendored
136
vendor/guzzlehttp/psr7/src/PumpStream.php
vendored
|
|
@ -13,38 +13,37 @@ use Psr\Http\Message\StreamInterface;
|
|||
* number of bytes to read to the callable. The callable can choose to ignore
|
||||
* this value and return fewer or more bytes than requested. Any extra data
|
||||
* returned by the callable is buffered internally until drained using the
|
||||
* read() function of the PumpStream. The callable MUST return false or null
|
||||
* when there is no more data to read.
|
||||
* read() function of the PumpStream. The callable MUST return a non-empty
|
||||
* string when data is available, or false or null when there is no more data
|
||||
* to read.
|
||||
*
|
||||
* Userland callables that declare no parameters are tolerated by PHP, but
|
||||
* length-aware callables remain the recommended formal shape.
|
||||
*/
|
||||
final class PumpStream implements StreamInterface
|
||||
{
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var callable|null */
|
||||
private $source;
|
||||
|
||||
/** @var int|null */
|
||||
private $size;
|
||||
private ?int $size;
|
||||
|
||||
/** @var int */
|
||||
private $tellPos = 0;
|
||||
private int $tellPos = 0;
|
||||
|
||||
/** @var array */
|
||||
private $metadata;
|
||||
private array $metadata;
|
||||
|
||||
/** @var BufferStream */
|
||||
private $buffer;
|
||||
private BufferStream $buffer;
|
||||
|
||||
/**
|
||||
* @param (callable(): (string|false|null))|(callable(int): (string|false|null)) $source Source of the stream data. The callable receives
|
||||
* the suggested number of bytes to read, may ignore
|
||||
* that value, and may return fewer or more bytes.
|
||||
* Extra bytes are buffered. The callable MUST return
|
||||
* a string when called, or false|null on error or EOF.
|
||||
* Userland callables that declare no parameters are
|
||||
* tolerated by PHP, but length-aware callables remain
|
||||
* the recommended formal shape.
|
||||
* a non-empty string when producing data, or false|null
|
||||
* on error or EOF. Userland callables that declare no
|
||||
* parameters are tolerated by PHP, but length-aware
|
||||
* callables remain the recommended formal shape.
|
||||
* @param array{size?: int, metadata?: array} $options Stream options:
|
||||
* - metadata: Hash of metadata to use with stream.
|
||||
* - size: Size of the stream, if known.
|
||||
|
|
@ -52,23 +51,25 @@ final class PumpStream implements StreamInterface
|
|||
public function __construct(callable $source, array $options = [])
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->size = $options['size'] ?? null;
|
||||
$this->size = Integers::assertOptionalNonNegativeSize($options['size'] ?? null, 'Stream size');
|
||||
$this->metadata = $options['metadata'] ?? [];
|
||||
$this->buffer = new BufferStream();
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->source = null;
|
||||
$this->size = null;
|
||||
$this->tellPos = 0;
|
||||
$this->metadata = [];
|
||||
$this->buffer = new BufferStream();
|
||||
|
||||
throw new \LogicException(static::class.' should never be unserialized');
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
return Utils::copyToString($this);
|
||||
} catch (\Throwable $e) {
|
||||
if (\PHP_VERSION_ID >= 70400) {
|
||||
throw $e;
|
||||
}
|
||||
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
|
||||
|
||||
return '';
|
||||
}
|
||||
return Utils::copyToString($this);
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
|
|
@ -80,6 +81,7 @@ final class PumpStream implements StreamInterface
|
|||
{
|
||||
$this->tellPos = 0;
|
||||
$this->source = null;
|
||||
$this->buffer->close();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -109,26 +111,8 @@ final class PumpStream implements StreamInterface
|
|||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Cannot seek a PumpStream');
|
||||
}
|
||||
|
||||
|
|
@ -137,17 +121,8 @@ final class PumpStream implements StreamInterface
|
|||
return false;
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Cannot write to a PumpStream');
|
||||
}
|
||||
|
||||
|
|
@ -156,56 +131,35 @@ final class PumpStream implements StreamInterface
|
|||
return true;
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
$bufferLength = $this->buffer->getSize() ?? 0;
|
||||
|
||||
if ($length > $bufferLength) {
|
||||
$this->pump($length - $bufferLength);
|
||||
}
|
||||
|
||||
$data = $this->buffer->read($length);
|
||||
$readLen = strlen($data);
|
||||
$this->tellPos += $readLen;
|
||||
$remaining = $length - $readLen;
|
||||
|
||||
if ($remaining) {
|
||||
$this->pump($remaining);
|
||||
$data .= $this->buffer->read($remaining);
|
||||
$this->tellPos += strlen($data) - $readLen;
|
||||
}
|
||||
$this->tellPos = Integers::add($this->tellPos, strlen($data));
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getContents(): string
|
||||
{
|
||||
$result = '';
|
||||
while (!$this->eof()) {
|
||||
$result .= $this->read(1000000);
|
||||
}
|
||||
|
||||
return $result;
|
||||
return Utils::copyToString($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
public function getMetadata(?string $key = null)
|
||||
{
|
||||
if ($key !== null && !\is_string($key)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
|
||||
\get_debug_type($key)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$key) {
|
||||
if ($key === null) {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
|
|
@ -216,12 +170,18 @@ final class PumpStream implements StreamInterface
|
|||
{
|
||||
if ($this->source !== null) {
|
||||
do {
|
||||
/** @var string|false|null $data */
|
||||
$data = ($this->source)($length);
|
||||
if ($data === false || $data === null) {
|
||||
$this->source = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data === '') {
|
||||
throw new \RuntimeException('PumpStream source returned an empty string');
|
||||
}
|
||||
|
||||
$this->buffer->write($data);
|
||||
$length -= strlen($data);
|
||||
} while ($length > 0);
|
||||
|
|
|
|||
76
vendor/guzzlehttp/psr7/src/Query.php
vendored
76
vendor/guzzlehttp/psr7/src/Query.php
vendored
|
|
@ -6,13 +6,17 @@ namespace GuzzleHttp\Psr7;
|
|||
|
||||
final class Query
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a query string into an associative array.
|
||||
*
|
||||
* If multiple values are found for the same key, the value of that key
|
||||
* value pair will become an array. This function does not parse nested
|
||||
* PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2`
|
||||
* will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`.
|
||||
* If multiple values are found for the same key, the value of that
|
||||
* key-value pair becomes an array. This function does not parse nested PHP
|
||||
* style arrays into an associative array. For example, `foo[a]=1&foo[b]=2`
|
||||
* will be parsed into `['foo[a]' => '1', 'foo[b]' => '2']`.
|
||||
*
|
||||
* @param string $str Query string to parse
|
||||
* @param int|bool $urlEncoding How the query string is encoded
|
||||
|
|
@ -26,15 +30,19 @@ final class Query
|
|||
}
|
||||
|
||||
if ($urlEncoding === true) {
|
||||
$decoder = function ($value) {
|
||||
return rawurldecode(str_replace('+', ' ', (string) $value));
|
||||
$decoder = function (string $value): string {
|
||||
return \rawurldecode(str_replace('+', ' ', $value));
|
||||
};
|
||||
} elseif ($urlEncoding === PHP_QUERY_RFC3986) {
|
||||
$decoder = 'rawurldecode';
|
||||
$decoder = static function (string $value): string {
|
||||
return \rawurldecode($value);
|
||||
};
|
||||
} elseif ($urlEncoding === PHP_QUERY_RFC1738) {
|
||||
$decoder = 'urldecode';
|
||||
$decoder = static function (string $value): string {
|
||||
return \urldecode($value);
|
||||
};
|
||||
} else {
|
||||
$decoder = function ($str) {
|
||||
$decoder = function (string $str): string {
|
||||
return $str;
|
||||
};
|
||||
}
|
||||
|
|
@ -57,11 +65,11 @@ final class Query
|
|||
}
|
||||
|
||||
/**
|
||||
* Build a query string from an array of key value pairs.
|
||||
* Build a query string from an array of key-value pairs.
|
||||
*
|
||||
* This function can use the return value of `parse()` to build a query
|
||||
* string. This function does not modify the provided keys when an array is
|
||||
* encountered (like `http_build_query()` would).
|
||||
* encountered, unlike `http_build_query()`.
|
||||
*
|
||||
* @param array $params Query string parameters.
|
||||
* @param int|false $encoding Set to false to not encode,
|
||||
|
|
@ -82,31 +90,33 @@ final class Query
|
|||
return $str;
|
||||
};
|
||||
} elseif ($encoding === PHP_QUERY_RFC3986) {
|
||||
$encoder = 'rawurlencode';
|
||||
$encoder = static function (string $value): string {
|
||||
return \rawurlencode($value);
|
||||
};
|
||||
} elseif ($encoding === PHP_QUERY_RFC1738) {
|
||||
$encoder = 'urlencode';
|
||||
$encoder = static function (string $value): string {
|
||||
return \urlencode($value);
|
||||
};
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid type');
|
||||
}
|
||||
|
||||
$castBool = $treatBoolsAsInts ? static function ($v) { return (int) $v; } : static function ($v) { return $v ? 'true' : 'false'; };
|
||||
|
||||
$qs = '';
|
||||
foreach ($params as $k => $v) {
|
||||
$k = $encoder((string) $k);
|
||||
if (!is_array($v)) {
|
||||
$qs .= $k;
|
||||
$v = is_bool($v) ? $castBool($v) : $v;
|
||||
$v = self::normalizeValue($v, $treatBoolsAsInts);
|
||||
if ($v !== null) {
|
||||
$qs .= '='.$encoder((string) $v);
|
||||
$qs .= '='.$encoder($v);
|
||||
}
|
||||
$qs .= '&';
|
||||
} else {
|
||||
foreach ($v as $vv) {
|
||||
$qs .= $k;
|
||||
$vv = is_bool($vv) ? $castBool($vv) : $vv;
|
||||
$vv = self::normalizeValue($vv, $treatBoolsAsInts);
|
||||
if ($vv !== null) {
|
||||
$qs .= '='.$encoder((string) $vv);
|
||||
$qs .= '='.$encoder($vv);
|
||||
}
|
||||
$qs .= '&';
|
||||
}
|
||||
|
|
@ -115,4 +125,32 @@ final class Query
|
|||
|
||||
return $qs ? (string) substr($qs, 0, -1) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function normalizeValue($value, bool $treatBoolsAsInts): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $treatBoolsAsInts ? (string) (int) $value : ($value ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
throw new \InvalidArgumentException('Query string values must be finite; non-finite floats are not supported.');
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_object($value) && method_exists($value, '__toString')) {
|
||||
return $value->__toString();
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Query string values must be scalar, null, or stringable objects');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
131
vendor/guzzlehttp/psr7/src/Request.php
vendored
131
vendor/guzzlehttp/psr7/src/Request.php
vendored
|
|
@ -16,14 +16,11 @@ class Request implements RequestInterface
|
|||
{
|
||||
use MessageTrait;
|
||||
|
||||
/** @var string */
|
||||
private $method;
|
||||
private string $method;
|
||||
|
||||
/** @var string|null */
|
||||
private $requestTarget;
|
||||
private ?string $requestTarget = null;
|
||||
|
||||
/** @var UriInterface */
|
||||
private $uri;
|
||||
private UriInterface $uri;
|
||||
|
||||
/**
|
||||
* @param string $method HTTP method
|
||||
|
|
@ -40,12 +37,14 @@ class Request implements RequestInterface
|
|||
string $version = '1.1'
|
||||
) {
|
||||
$this->assertMethod($method);
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
if (!$uri instanceof UriInterface) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
self::getRequestTargetFromUri($uri);
|
||||
|
||||
self::warnOnMethodCasingChange($method);
|
||||
$this->method = strtoupper($method);
|
||||
$this->method = $method;
|
||||
$this->uri = $uri;
|
||||
$this->setHeaders($headers);
|
||||
$this->protocol = $version;
|
||||
|
|
@ -65,24 +64,12 @@ class Request implements RequestInterface
|
|||
return $this->requestTarget;
|
||||
}
|
||||
|
||||
$target = $this->uri->getPath();
|
||||
if ($target === '') {
|
||||
$target = '/';
|
||||
}
|
||||
if ($this->uri->getQuery() != '') {
|
||||
$target .= '?'.$this->uri->getQuery();
|
||||
}
|
||||
|
||||
return $target;
|
||||
return self::getRequestTargetFromUri($this->uri);
|
||||
}
|
||||
|
||||
public function withRequestTarget($requestTarget): RequestInterface
|
||||
public function withRequestTarget(string $requestTarget): RequestInterface
|
||||
{
|
||||
if (preg_match('#\s#', $requestTarget)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid request target provided; cannot contain whitespace'
|
||||
);
|
||||
}
|
||||
self::assertRequestTarget($requestTarget);
|
||||
|
||||
$new = clone $this;
|
||||
$new->requestTarget = $requestTarget;
|
||||
|
|
@ -95,12 +82,11 @@ class Request implements RequestInterface
|
|||
return $this->method;
|
||||
}
|
||||
|
||||
public function withMethod($method): RequestInterface
|
||||
public function withMethod(string $method): RequestInterface
|
||||
{
|
||||
$this->assertMethod($method);
|
||||
self::warnOnMethodCasingChange($method);
|
||||
$new = clone $this;
|
||||
$new->method = strtoupper($method);
|
||||
$new->method = $method;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
|
@ -110,26 +96,30 @@ class Request implements RequestInterface
|
|||
return $this->uri;
|
||||
}
|
||||
|
||||
public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
|
||||
public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface
|
||||
{
|
||||
if (!\is_bool($preserveHost)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to RequestInterface::withUri() is deprecated; guzzlehttp/psr7 3.0 requires bool for $preserveHost.',
|
||||
\get_debug_type($preserveHost)
|
||||
);
|
||||
$sameUri = $uri === $this->uri;
|
||||
|
||||
if (!$sameUri && $this->requestTarget === null) {
|
||||
self::getRequestTargetFromUri($uri);
|
||||
}
|
||||
|
||||
if ($uri === $this->uri) {
|
||||
$currentHost = $this->getHeaderLine('Host');
|
||||
$host = null;
|
||||
|
||||
if (!$preserveHost || $currentHost === '') {
|
||||
$host = $this->getHostFromUri($uri);
|
||||
}
|
||||
|
||||
if ($sameUri && ($host === null || $currentHost === $host)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->uri = $uri;
|
||||
|
||||
if (!$preserveHost || !isset($this->headerNames['host'])) {
|
||||
$new->updateHostFromUri();
|
||||
if ($host !== null) {
|
||||
$new->setHostHeader($host);
|
||||
}
|
||||
|
||||
return $new;
|
||||
|
|
@ -137,20 +127,36 @@ class Request implements RequestInterface
|
|||
|
||||
private function updateHostFromUri(): void
|
||||
{
|
||||
$host = $this->uri->getHost();
|
||||
$host = $this->getHostFromUri($this->uri);
|
||||
|
||||
if ($host == '') {
|
||||
if ($host === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setHostHeader($host);
|
||||
}
|
||||
|
||||
private function getHostFromUri(UriInterface $uri): ?string
|
||||
{
|
||||
$host = $uri->getHost();
|
||||
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri::assertValidHost($host);
|
||||
|
||||
if (($port = $this->uri->getPort()) !== null) {
|
||||
if (($port = $uri->getPort()) !== null) {
|
||||
$host .= ':'.$port;
|
||||
}
|
||||
|
||||
$this->assertValue($host);
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
private function setHostHeader(string $host): void
|
||||
{
|
||||
if (isset($this->headerNames['host'])) {
|
||||
$header = $this->headerNames['host'];
|
||||
} else {
|
||||
|
|
@ -158,28 +164,47 @@ class Request implements RequestInterface
|
|||
$this->headerNames['host'] = 'Host';
|
||||
}
|
||||
// Ensure Host is the first header.
|
||||
// See: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
|
||||
// See: https://datatracker.ietf.org/doc/html/rfc9110#section-7.2
|
||||
$this->headers = [$header => [$host]] + $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $method
|
||||
*/
|
||||
private function assertMethod($method): void
|
||||
private function assertMethod(string $method): void
|
||||
{
|
||||
if (!is_string($method) || $method === '') {
|
||||
throw new InvalidArgumentException('Method must be a non-empty string.');
|
||||
if (!Rfc9110::isToken($method)) {
|
||||
throw new InvalidArgumentException('Method must be a valid HTTP token.');
|
||||
}
|
||||
}
|
||||
|
||||
private static function warnOnMethodCasingChange(string $method): void
|
||||
private static function getRequestTargetFromUri(UriInterface $uri): string
|
||||
{
|
||||
if ($method !== strtoupper($method)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing a non-uppercase HTTP method is deprecated; guzzlehttp/psr7 3.0 preserves method casing and will no longer uppercase it. Normalize the method before constructing or modifying requests if uppercase is required.'
|
||||
$target = self::normalizePathForOriginForm($uri->getPath());
|
||||
if ($target === '') {
|
||||
$target = '/';
|
||||
}
|
||||
if ($uri->getQuery() != '') {
|
||||
$target .= '?'.$uri->getQuery();
|
||||
}
|
||||
|
||||
self::assertRequestTarget($target);
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
private static function assertRequestTarget(string $requestTarget): void
|
||||
{
|
||||
if (!Rfc9112::isValidRequestTarget($requestTarget)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid request target provided; cannot be empty or contain whitespace or control characters'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function normalizePathForOriginForm(string $path): string
|
||||
{
|
||||
if (str_starts_with($path, '//')) {
|
||||
return '/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
58
vendor/guzzlehttp/psr7/src/Response.php
vendored
58
vendor/guzzlehttp/psr7/src/Response.php
vendored
|
|
@ -78,11 +78,9 @@ class Response implements ResponseInterface
|
|||
511 => 'Network Authentication Required',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $reasonPhrase;
|
||||
private string $reasonPhrase;
|
||||
|
||||
/** @var int */
|
||||
private $statusCode;
|
||||
private int $statusCode;
|
||||
|
||||
/**
|
||||
* @param int $status Status code
|
||||
|
|
@ -99,6 +97,7 @@ class Response implements ResponseInterface
|
|||
?string $reason = null
|
||||
) {
|
||||
$this->assertStatusCodeRange($status);
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
$this->statusCode = $status;
|
||||
|
||||
|
|
@ -108,11 +107,14 @@ class Response implements ResponseInterface
|
|||
|
||||
$this->setHeaders($headers);
|
||||
if ($reason == '' && isset(self::PHRASES[$this->statusCode])) {
|
||||
$this->reasonPhrase = self::PHRASES[$this->statusCode];
|
||||
$reasonPhrase = self::PHRASES[$this->statusCode];
|
||||
} else {
|
||||
$this->reasonPhrase = (string) $reason;
|
||||
$reasonPhrase = (string) $reason;
|
||||
}
|
||||
|
||||
$this->assertReasonPhrase($reasonPhrase);
|
||||
$this->reasonPhrase = $reasonPhrase;
|
||||
|
||||
$this->protocol = $version;
|
||||
}
|
||||
|
||||
|
|
@ -126,54 +128,32 @@ class Response implements ResponseInterface
|
|||
return $this->reasonPhrase;
|
||||
}
|
||||
|
||||
public function withStatus($code, $reasonPhrase = ''): ResponseInterface
|
||||
public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
|
||||
{
|
||||
if (!\is_int($code) && \filter_var($code, \FILTER_VALIDATE_INT) !== false) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to ResponseInterface::withStatus() is deprecated; guzzlehttp/psr7 3.0 requires int for $code.',
|
||||
\get_debug_type($code)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_string($reasonPhrase)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to ResponseInterface::withStatus() is deprecated; guzzlehttp/psr7 3.0 requires string for $reasonPhrase.',
|
||||
\get_debug_type($reasonPhrase)
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertStatusCodeIsInteger($code);
|
||||
$code = (int) $code;
|
||||
$this->assertStatusCodeRange($code);
|
||||
|
||||
$new = clone $this;
|
||||
$new->statusCode = $code;
|
||||
if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
|
||||
if ($reasonPhrase === '' && isset(self::PHRASES[$new->statusCode])) {
|
||||
$reasonPhrase = self::PHRASES[$new->statusCode];
|
||||
}
|
||||
$new->reasonPhrase = (string) $reasonPhrase;
|
||||
$this->assertReasonPhrase($reasonPhrase);
|
||||
$new->reasonPhrase = $reasonPhrase;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $statusCode
|
||||
*/
|
||||
private function assertStatusCodeIsInteger($statusCode): void
|
||||
{
|
||||
if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) {
|
||||
throw new \InvalidArgumentException('Status code must be an integer value.');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertStatusCodeRange(int $statusCode): void
|
||||
{
|
||||
if ($statusCode < 100 || $statusCode >= 600) {
|
||||
throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertReasonPhrase(string $reasonPhrase): void
|
||||
{
|
||||
if (!Rfc9112::isValidReasonPhrase($reasonPhrase)) {
|
||||
throw new \InvalidArgumentException('Reason phrase must not contain invalid control characters.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
338
vendor/guzzlehttp/psr7/src/Rfc3986.php
vendored
338
vendor/guzzlehttp/psr7/src/Rfc3986.php
vendored
|
|
@ -5,14 +5,21 @@ declare(strict_types=1);
|
|||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Syntax predicates and canonicalization helpers for the URI grammar defined
|
||||
* by RFC 3986.
|
||||
*/
|
||||
final class Rfc3986
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-delims for use in a regex.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
|
||||
|
||||
|
|
@ -20,6 +27,335 @@ final class Rfc3986
|
|||
* Unreserved characters for use in a regex.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
|
||||
|
||||
/**
|
||||
* The two hex digits of a percent-encoded octet (the "3A" in "%3A"), for use in a regex.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const HEX_OCTET = '[A-Fa-f0-9]{2}';
|
||||
|
||||
/**
|
||||
* Whether the string is a valid URI scheme.
|
||||
*
|
||||
* Per RFC 3986 a scheme must start with a letter, followed by letters,
|
||||
* digits, `+`, `-`, or `.`. The empty string is also accepted, since a URI
|
||||
* reference may omit the scheme.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
|
||||
*/
|
||||
public static function isValidScheme(string $scheme): bool
|
||||
{
|
||||
return $scheme === '' || preg_match('/^[A-Za-z][A-Za-z0-9.+-]*$/D', $scheme) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the string is a valid URI host.
|
||||
*
|
||||
* Per RFC 3986 the host is `IP-literal / IPv4address / reg-name`. An empty
|
||||
* host is accepted, since the authority (and thus the host) may be empty.
|
||||
* Bracketed values are validated as IPv6 / IPvFuture literals; any other
|
||||
* value is rejected if it contains control characters, whitespace, an
|
||||
* authority or path delimiter (`/ ? # @ \`), an embedded colon denoting
|
||||
* a port, a malformed percent-sequence, or a percent-encoded octet that
|
||||
* decodes to one of those rejected bytes, to a bracket, or to `%` itself.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
|
||||
*/
|
||||
public static function isValidHost(string $host): bool
|
||||
{
|
||||
if ($host === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
|
||||
|
||||
if ($invalidHost === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($invalidHost === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($host, '[') || str_contains($host, ']')) {
|
||||
return self::isValidIpLiteralHost($host);
|
||||
}
|
||||
|
||||
if (str_contains($host, ':')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !str_contains($host, '%') || self::hasValidHostPercentEncoding($host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the string is a valid port number (0-65535).
|
||||
*
|
||||
* RFC 3986 defines the port as `*DIGIT`, which also permits an empty port
|
||||
* and has no upper bound. This applies the stricter policy used throughout
|
||||
* the library instead: the value must be a non-empty run of digits (leading
|
||||
* zeros are accepted and normalized) that resolves to 0-65535.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
|
||||
*/
|
||||
public static function isValidPort(string $port): bool
|
||||
{
|
||||
if ($port === '' || !ctype_digit($port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalized = ltrim($port, '0');
|
||||
if ($normalized === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return strlen($normalized) <= 5 && (int) $normalized <= 0xFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RFC 5952 canonical form of a valid IPv6 address.
|
||||
*
|
||||
* The address must be a valid textual IPv6 address without brackets and
|
||||
* without a zone identifier, such as the inside of an IP-literal accepted
|
||||
* by `isValidHost()`. Canonicalization lowercases the hexadecimal fields,
|
||||
* suppresses leading zeros, and collapses the longest run of two or more
|
||||
* zero fields (the leftmost on a tie) with `::`. Embedded dotted-decimal
|
||||
* notation follows the rendering policy of BIND-derived `inet_ntop()`
|
||||
* implementations and curl: exactly the IPv4-mapped (`::ffff:0:0/96`) and
|
||||
* deprecated IPv4-compatible (`::/96`) layouts use it, while other
|
||||
* embedded-IPv4 forms, including translated (NAT64) well-known prefixes
|
||||
* such as `64:ff9b::/96` (RFC 6052), serialize in pure hexadecimal fields.
|
||||
*
|
||||
* Validation is strict and platform-independent: the address is checked
|
||||
* against the RFC 3986 `IPv6address` grammar with PHP's
|
||||
* `FILTER_VALIDATE_IP` filter and parsed in pure PHP, so spellings that
|
||||
* only some platform parsers accept, such as the zero-padded dotted octets
|
||||
* in `::ffff:192.168.001.001`, are rejected everywhere.
|
||||
*
|
||||
* @throws \InvalidArgumentException If the address cannot be parsed.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc5952#section-4
|
||||
*/
|
||||
public static function canonicalizeIpv6(string $address): string
|
||||
{
|
||||
$canonical = self::tryCanonicalizeIpv6($address);
|
||||
if ($canonical === null) {
|
||||
throw new \InvalidArgumentException('Invalid IPv6 address');
|
||||
}
|
||||
|
||||
return $canonical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RFC 5952 canonical form of a valid IPv6 address, or null
|
||||
* when the address cannot be parsed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function tryCanonicalizeIpv6(string $address): ?string
|
||||
{
|
||||
// Platform parsers disagree on which spellings are valid: Apple libc
|
||||
// and OpenBSD inet_pton() accept zero-padded dotted octets such as
|
||||
// "::ffff:192.168.001.001", and macOS additionally accepts and silently
|
||||
// strips zone IDs ("fe80::1%eth0"), while glibc, musl, and PHP's own
|
||||
// filter reject both. Origin classification built on this helper must
|
||||
// fail closed and must not vary by operating system, so the address is
|
||||
// validated with the platform-independent FILTER_VALIDATE_IP filter and
|
||||
// parsed in pure PHP; no OS parser is consulted.
|
||||
if (\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$words = self::parseIpv6Words($address);
|
||||
if ($words === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the longest run of two or more zero fields; ties keep the
|
||||
// leftmost run per RFC 5952 section 4.2.3.
|
||||
$bestStart = 0;
|
||||
$bestLen = 0;
|
||||
$start = -1;
|
||||
foreach ($words as $i => $word) {
|
||||
if ($word !== 0) {
|
||||
$start = -1;
|
||||
continue;
|
||||
}
|
||||
if ($start === -1) {
|
||||
$start = $i;
|
||||
}
|
||||
if ($i - $start + 1 > $bestLen) {
|
||||
$bestStart = $start;
|
||||
$bestLen = $i - $start + 1;
|
||||
}
|
||||
}
|
||||
if ($bestLen < 2) {
|
||||
$bestLen = 0;
|
||||
}
|
||||
|
||||
// RFC 5952 section 5: embedded IPv4 notation for IPv4-mapped
|
||||
// (::ffff:0:0/96) and IPv4-compatible (::/96) addresses, the same
|
||||
// condition BIND-derived inet_ntop() and curl use. bestStart must be
|
||||
// zero: a five or six field zero run elsewhere is not an IPv4 prefix.
|
||||
$mixed = $bestStart === 0
|
||||
&& ($bestLen === 6 || ($bestLen === 5 && $words[5] === 0xFFFF));
|
||||
|
||||
$groups = [];
|
||||
for ($i = 0, $n = $mixed ? 6 : 8; $i < $n; ++$i) {
|
||||
$groups[] = dechex($words[$i]);
|
||||
}
|
||||
if ($mixed) {
|
||||
$groups[] = sprintf(
|
||||
'%d.%d.%d.%d',
|
||||
$words[6] >> 8,
|
||||
$words[6] & 0xFF,
|
||||
$words[7] >> 8,
|
||||
$words[7] & 0xFF
|
||||
);
|
||||
}
|
||||
|
||||
if ($bestLen === 0) {
|
||||
return implode(':', $groups);
|
||||
}
|
||||
|
||||
return implode(':', array_slice($groups, 0, $bestStart)).'::'.implode(':', array_slice($groups, $bestStart + $bestLen));
|
||||
}
|
||||
|
||||
private static function hasValidHostPercentEncoding(string $host): bool
|
||||
{
|
||||
// Mirror of the raw reg-name policy above for percent-encoded octets:
|
||||
// reject malformed sequences (RFC 3986 requires "%" HEXDIG HEXDIG) and
|
||||
// octets that decode to bytes the raw grammar rejects - C0 controls,
|
||||
// SP, DEL, the delimiters / ? # @ \ [ ], the port colon, and % itself.
|
||||
// Octets decoding to any other byte (unreserved, sub-delims, and
|
||||
// non-ASCII UTF-8 data) remain accepted.
|
||||
$invalidEncoding = preg_match(
|
||||
'/%(?!'.self::HEX_OCTET.')|%(?:[01][0-9A-Fa-f]|2[035F]|3[AF]|40|5[BCD]|7F)/i',
|
||||
$host
|
||||
);
|
||||
|
||||
return $invalidEncoding === 0;
|
||||
}
|
||||
|
||||
private static function isValidIpLiteralHost(string $host): bool
|
||||
{
|
||||
if (!str_starts_with($host, '[') || !str_ends_with($host, ']')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$address = substr($host, 1, -1);
|
||||
if (\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// RFC 6874 IPv6 zone identifiers are intentionally not supported here.
|
||||
// Bracketed hosts are validated as IPv6 or IPvFuture only.
|
||||
return preg_match('/^v[0-9a-f]+\.['.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.':]+$/iD', $address) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a textual IPv6 address into its eight 16-bit words, or null
|
||||
* when the text is not a structurally valid address.
|
||||
*
|
||||
* The grammar enforced here is the RFC 3986 `IPv6address` rule: one to four
|
||||
* hexadecimal digits per field, at most one `::` eliding one or more zero
|
||||
* fields, and an optional dotted-decimal tail of four octets (0-255, no
|
||||
* leading zeros) as the final 32 bits. FILTER_VALIDATE_IP accepts exactly
|
||||
* this grammar, so the filter guard in tryCanonicalizeIpv6() and this
|
||||
* parser always agree and the null paths here can only fail closed.
|
||||
*
|
||||
* @return list<int>|null
|
||||
*/
|
||||
private static function parseIpv6Words(string $address): ?array
|
||||
{
|
||||
// A dotted-decimal tail is only valid as the final 32 bits, after the
|
||||
// final colon. Rewrite it into its two hexadecimal fields so the
|
||||
// remainder of the parse handles hexadecimal fields only.
|
||||
$dot = strpos($address, '.');
|
||||
if ($dot !== false) {
|
||||
$colon = strrpos($address, ':');
|
||||
if ($colon === false || $colon > $dot) {
|
||||
return null;
|
||||
}
|
||||
$octets = explode('.', substr($address, $colon + 1));
|
||||
if (count($octets) !== 4) {
|
||||
return null;
|
||||
}
|
||||
$bytes = [];
|
||||
foreach ($octets as $octet) {
|
||||
if ($octet === '' || strlen($octet) > 3 || !ctype_digit($octet)) {
|
||||
return null;
|
||||
}
|
||||
if ($octet[0] === '0' && $octet !== '0') {
|
||||
return null;
|
||||
}
|
||||
$byte = (int) $octet;
|
||||
if ($byte > 255) {
|
||||
return null;
|
||||
}
|
||||
$bytes[] = $byte;
|
||||
}
|
||||
$address = substr($address, 0, $colon + 1)
|
||||
.dechex(($bytes[0] << 8) | $bytes[1])
|
||||
.':'
|
||||
.dechex(($bytes[2] << 8) | $bytes[3]);
|
||||
}
|
||||
|
||||
$halves = explode('::', $address);
|
||||
if (count($halves) > 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$head = self::parseHexFields($halves[0]);
|
||||
if ($head === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count($halves) === 1) {
|
||||
return count($head) === 8 ? $head : null;
|
||||
}
|
||||
|
||||
$tail = self::parseHexFields($halves[1]);
|
||||
if ($tail === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The "::" must elide at least one zero field.
|
||||
$elided = 8 - count($head) - count($tail);
|
||||
if ($elided < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_merge($head, array_fill(0, $elided, 0), $tail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a colon-separated run of 16-bit hexadecimal fields, or null
|
||||
* when a field is empty, longer than four digits, or not hexadecimal.
|
||||
*
|
||||
* @return list<int>|null
|
||||
*/
|
||||
private static function parseHexFields(string $fields): ?array
|
||||
{
|
||||
if ($fields === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$words = [];
|
||||
foreach (explode(':', $fields) as $field) {
|
||||
if ($field === '' || strlen($field) > 4 || !ctype_xdigit($field)) {
|
||||
return null;
|
||||
}
|
||||
$words[] = intval($field, 16);
|
||||
}
|
||||
|
||||
return $words;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
vendor/guzzlehttp/psr7/src/Rfc7230.php
vendored
106
vendor/guzzlehttp/psr7/src/Rfc7230.php
vendored
|
|
@ -1,106 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Rfc7230
|
||||
{
|
||||
/**
|
||||
* Header related regular expressions (based on amphp/http package)
|
||||
*
|
||||
* Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons.
|
||||
*
|
||||
* @see https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15
|
||||
*
|
||||
* @license https://github.com/amphp/http/blob/v1.0.1/LICENSE
|
||||
*/
|
||||
public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
|
||||
public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: int|null}|null
|
||||
*/
|
||||
public static function parseHostHeader(string $authority): ?array
|
||||
{
|
||||
if ($authority === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = $authority;
|
||||
$port = null;
|
||||
|
||||
if ($authority[0] === '[') {
|
||||
$closingBracket = strpos($authority, ']');
|
||||
if ($closingBracket === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = substr($authority, 0, $closingBracket + 1);
|
||||
$remainder = substr($authority, $closingBracket + 1);
|
||||
if ($remainder !== '') {
|
||||
if ($remainder[0] !== ':') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = self::parseAuthorityPort(substr($remainder, 1));
|
||||
if ($port === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} elseif (false !== ($colon = strpos($authority, ':'))) {
|
||||
$host = substr($authority, 0, $colon);
|
||||
$port = self::parseAuthorityPort(substr($authority, $colon + 1));
|
||||
if ($port === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($host === '' || !self::isValidHostHeaderHost($host)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$host, $port];
|
||||
}
|
||||
|
||||
private static function isValidHostHeaderHost(string $host): bool
|
||||
{
|
||||
if (preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
|
||||
if ($host[0] !== '[' || substr($host, -1) !== ']') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$address = substr($host, 1, -1);
|
||||
|
||||
return filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false
|
||||
|| preg_match('/^v[0-9a-f]+\.['.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.':]+$/iD', $address) === 1;
|
||||
}
|
||||
|
||||
return strpos($host, ':') === false;
|
||||
}
|
||||
|
||||
private static function parseAuthorityPort(string $port): ?int
|
||||
{
|
||||
if ($port === '' || !ctype_digit($port)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = ltrim($port, '0');
|
||||
if ($normalized === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strlen($normalized) > 5 || (int) $normalized > 0xFFFF) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $normalized;
|
||||
}
|
||||
}
|
||||
41
vendor/guzzlehttp/psr7/src/Rfc9110.php
vendored
Normal file
41
vendor/guzzlehttp/psr7/src/Rfc9110.php
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Rfc9110
|
||||
{
|
||||
/**
|
||||
* A token for use in a regular expression.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
|
||||
*/
|
||||
public const TOKEN_PATTERN = '[!#$%&\'*+.^_`|~0-9A-Za-z-]+';
|
||||
|
||||
/**
|
||||
* A field value for use in a regular expression.
|
||||
*
|
||||
* Obsolete line folding is intentionally excluded.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
|
||||
*/
|
||||
public const FIELD_VALUE_PATTERN = '[\x09\x20-\x7E\x80-\xFF]*';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function isToken(string $value): bool
|
||||
{
|
||||
return preg_match('/^'.self::TOKEN_PATTERN.'$/D', $value) === 1;
|
||||
}
|
||||
|
||||
public static function isFieldValue(string $value): bool
|
||||
{
|
||||
return preg_match('/^'.self::FIELD_VALUE_PATTERN.'$/D', $value) === 1;
|
||||
}
|
||||
}
|
||||
126
vendor/guzzlehttp/psr7/src/Rfc9112.php
vendored
Normal file
126
vendor/guzzlehttp/psr7/src/Rfc9112.php
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Rfc9112
|
||||
{
|
||||
/**
|
||||
* An HTTP protocol version for use in a regular expression.
|
||||
*/
|
||||
public const PROTOCOL_VERSION_PATTERN = '\d+(?:\.\d+)?';
|
||||
|
||||
/**
|
||||
* The request-target bytes accepted by the HTTP/1 start-line grammar for
|
||||
* use in a regular expression.
|
||||
*/
|
||||
public const REQUEST_TARGET_PATTERN = '[^\x00-\x20\x7F]+';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Header related regular expressions (based on amphp/http package)
|
||||
*
|
||||
* Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons.
|
||||
*
|
||||
* @see https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15
|
||||
*
|
||||
* @license https://github.com/amphp/http/blob/v1.0.1/LICENSE
|
||||
*/
|
||||
public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
|
||||
public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
|
||||
|
||||
public static function isValidProtocolVersion(string $version): bool
|
||||
{
|
||||
return preg_match('/^'.self::PROTOCOL_VERSION_PATTERN.'$/D', $version) === 1;
|
||||
}
|
||||
|
||||
public static function isValidRequestTarget(string $target): bool
|
||||
{
|
||||
return preg_match('/^'.self::REQUEST_TARGET_PATTERN.'$/D', $target) === 1;
|
||||
}
|
||||
|
||||
public static function isValidReasonPhrase(string $reasonPhrase): bool
|
||||
{
|
||||
return Rfc9110::isFieldValue($reasonPhrase);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: int|null}|null
|
||||
*/
|
||||
public static function parseHostHeader(string $authority): ?array
|
||||
{
|
||||
if ($authority === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = $authority;
|
||||
$port = null;
|
||||
|
||||
if (str_starts_with($authority, '[')) {
|
||||
$closingBracket = strpos($authority, ']');
|
||||
if ($closingBracket === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = substr($authority, 0, $closingBracket + 1);
|
||||
$remainder = substr($authority, $closingBracket + 1);
|
||||
if ($remainder !== '') {
|
||||
if (!str_starts_with($remainder, ':')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = self::parsePort(substr($remainder, 1));
|
||||
if ($port === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} elseif (false !== ($colon = strpos($authority, ':'))) {
|
||||
$host = substr($authority, 0, $colon);
|
||||
$port = self::parsePort(substr($authority, $colon + 1));
|
||||
if ($port === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($host === '' || !Rfc3986::isValidHost($host)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$host, $port];
|
||||
}
|
||||
|
||||
public static function isAbsoluteFormRequestTarget(string $target): bool
|
||||
{
|
||||
return preg_match('/^[A-Za-z][A-Za-z0-9+.-]*:\/\//D', $target) === 1;
|
||||
}
|
||||
|
||||
public static function isAsteriskFormRequestTarget(string $method, string $target): bool
|
||||
{
|
||||
return $method === 'OPTIONS' && $target === '*';
|
||||
}
|
||||
|
||||
public static function isConnectAuthorityFormRequestTarget(string $method, string $target): bool
|
||||
{
|
||||
return $method === 'CONNECT' && strpbrk($target, '/?#') === false;
|
||||
}
|
||||
|
||||
public static function parsePort(string $port): ?int
|
||||
{
|
||||
if (!Rfc3986::isValidPort($port)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A zero port is valid per RFC 3986 but meaningless for an HTTP
|
||||
// authority, so reject it on top of the generic syntax check.
|
||||
$parsed = (int) ltrim($port, '0');
|
||||
|
||||
return $parsed === 0 ? null : $parsed;
|
||||
}
|
||||
}
|
||||
302
vendor/guzzlehttp/psr7/src/ServerRequest.php
vendored
302
vendor/guzzlehttp/psr7/src/ServerRequest.php
vendored
|
|
@ -26,35 +26,20 @@ use Psr\Http\Message\UriInterface;
|
|||
*/
|
||||
class ServerRequest extends Request implements ServerRequestInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $attributes = [];
|
||||
private array $attributes = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $cookieParams = [];
|
||||
private array $cookieParams = [];
|
||||
|
||||
/**
|
||||
* @var array|object|null
|
||||
*/
|
||||
private $parsedBody;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $queryParams = [];
|
||||
private array $queryParams = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $serverParams;
|
||||
private array $serverParams;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $uploadedFiles = [];
|
||||
private array $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* @param string $method HTTP method
|
||||
|
|
@ -70,6 +55,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
array $headers = [],
|
||||
$body = null,
|
||||
string $version = '1.1',
|
||||
#[\SensitiveParameter]
|
||||
array $serverParams = []
|
||||
) {
|
||||
$this->serverParams = $serverParams;
|
||||
|
|
@ -86,73 +72,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
*/
|
||||
public static function normalizeFiles(array $files): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($files as $key => $value) {
|
||||
if ($value instanceof UploadedFileInterface) {
|
||||
$normalized[$key] = $value;
|
||||
} elseif (is_array($value) && isset($value['tmp_name'])) {
|
||||
$normalized[$key] = self::createUploadedFileFromSpec($value);
|
||||
} elseif (is_array($value)) {
|
||||
$normalized[$key] = self::normalizeFiles($value);
|
||||
continue;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid value in files specification');
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an UploadedFile instance from a $_FILES specification.
|
||||
*
|
||||
* If the specification represents an array of values, this method will
|
||||
* delegate to normalizeNestedFileSpec() and return that return value.
|
||||
*
|
||||
* @param array $value $_FILES struct
|
||||
*
|
||||
* @return UploadedFileInterface|UploadedFileInterface[]
|
||||
*/
|
||||
private static function createUploadedFileFromSpec(array $value)
|
||||
{
|
||||
if (is_array($value['tmp_name'])) {
|
||||
return self::normalizeNestedFileSpec($value);
|
||||
}
|
||||
|
||||
return new UploadedFile(
|
||||
$value['tmp_name'],
|
||||
(int) $value['size'],
|
||||
(int) $value['error'],
|
||||
$value['name'],
|
||||
$value['type']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an array of file specifications.
|
||||
*
|
||||
* Loops through all nested files and returns a normalized array of
|
||||
* UploadedFileInterface instances.
|
||||
*
|
||||
* @return UploadedFileInterface[]
|
||||
*/
|
||||
private static function normalizeNestedFileSpec(array $files = []): array
|
||||
{
|
||||
$normalizedFiles = [];
|
||||
|
||||
foreach (array_keys($files['tmp_name']) as $key) {
|
||||
$spec = [
|
||||
'tmp_name' => $files['tmp_name'][$key],
|
||||
'size' => $files['size'][$key] ?? null,
|
||||
'error' => $files['error'][$key] ?? null,
|
||||
'name' => $files['name'][$key] ?? null,
|
||||
'type' => $files['type'][$key] ?? null,
|
||||
];
|
||||
$normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
|
||||
}
|
||||
|
||||
return $normalizedFiles;
|
||||
return UploadedFileNormalizer::normalize($files);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -165,79 +85,20 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
*/
|
||||
public static function fromGlobals(): ServerRequestInterface
|
||||
{
|
||||
$method = strtoupper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
|
||||
$headers = self::removeInvalidHostHeader(self::getAllHeaders());
|
||||
$uri = self::getUriFromGlobals();
|
||||
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
|
||||
$serverProtocol = self::getServerParam('SERVER_PROTOCOL');
|
||||
$protocol = $serverProtocol !== null ? str_replace('HTTP/', '', $serverProtocol) : '1.1';
|
||||
return ServerRequestGlobalsFactory::fromArrays(
|
||||
$_SERVER,
|
||||
$_GET,
|
||||
$_POST,
|
||||
$_COOKIE,
|
||||
$_FILES,
|
||||
static function () {
|
||||
if (!\function_exists('apache_request_headers')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
|
||||
|
||||
return $serverRequest
|
||||
->withCookieParams($_COOKIE)
|
||||
->withQueryParams($_GET)
|
||||
->withParsedBody($_POST)
|
||||
->withUploadedFiles(self::normalizeFiles($_FILES));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function getAllHeaders(): array
|
||||
{
|
||||
return self::normalizeHeaderValues(getallheaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $headers
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function normalizeHeaderValues(array $headers): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||
$normalized[$name] = (string) $value;
|
||||
return \apache_request_headers();
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private static function getServerParam(string $key): ?string
|
||||
{
|
||||
return isset($_SERVER[$key]) && is_string($_SERVER[$key]) ? $_SERVER[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string> $headers
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function removeInvalidHostHeader(array $headers): array
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
if (strtolower((string) $name) !== 'host') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Rfc7230::parseHostHeader($value) === null) {
|
||||
unset($headers[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string|null, 1: int|null}
|
||||
*/
|
||||
private static function extractHostAndPortFromAuthority(string $authority): array
|
||||
{
|
||||
return Rfc7230::parseHostHeader($authority) ?? [null, null];
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -245,51 +106,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
*/
|
||||
public static function getUriFromGlobals(): UriInterface
|
||||
{
|
||||
$uri = new Uri('');
|
||||
|
||||
$https = self::getServerParam('HTTPS');
|
||||
$uri = $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
|
||||
|
||||
$hasPort = false;
|
||||
$authority = self::getServerParam('HTTP_HOST');
|
||||
if ($authority !== null) {
|
||||
[$host, $port] = self::extractHostAndPortFromAuthority($authority);
|
||||
if ($host !== null) {
|
||||
$uri = $uri->withHost($host);
|
||||
}
|
||||
|
||||
if ($port !== null) {
|
||||
$hasPort = true;
|
||||
$uri = $uri->withPort($port);
|
||||
}
|
||||
} elseif (($serverName = self::getServerParam('SERVER_NAME')) !== null) {
|
||||
$uri = $uri->withHost($serverName);
|
||||
} elseif (($serverAddr = self::getServerParam('SERVER_ADDR')) !== null) {
|
||||
$uri = $uri->withHost($serverAddr);
|
||||
}
|
||||
|
||||
$serverPort = self::getServerParam('SERVER_PORT');
|
||||
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/', $serverPort) === 1) {
|
||||
$uri = $uri->withPort((int) $serverPort);
|
||||
}
|
||||
|
||||
$hasQuery = false;
|
||||
$requestUri = self::getServerParam('REQUEST_URI');
|
||||
if ($requestUri !== null) {
|
||||
$requestUriParts = explode('?', $requestUri, 2);
|
||||
$uri = $uri->withPath($requestUriParts[0]);
|
||||
if (isset($requestUriParts[1])) {
|
||||
$hasQuery = true;
|
||||
$uri = $uri->withQuery($requestUriParts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$queryString = self::getServerParam('QUERY_STRING');
|
||||
if (!$hasQuery && $queryString !== null) {
|
||||
$uri = $uri->withQuery($queryString);
|
||||
}
|
||||
|
||||
return $uri;
|
||||
return ServerRequestGlobalsFactory::getUriFromServerParams($_SERVER);
|
||||
}
|
||||
|
||||
public function getServerParams(): array
|
||||
|
|
@ -304,12 +121,10 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
|
||||
public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
|
||||
{
|
||||
$invalidUploadedFileFound = false;
|
||||
$invalidUploadedFile = null;
|
||||
$stack = [$uploadedFiles];
|
||||
|
||||
while ($stack !== []) {
|
||||
foreach (\array_pop($stack) as $uploadedFile) {
|
||||
for ($i = 0; $i < \count($stack); ++$i) {
|
||||
foreach ($stack[$i] as $uploadedFile) {
|
||||
if ($uploadedFile instanceof UploadedFileInterface) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -319,22 +134,13 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
continue;
|
||||
}
|
||||
|
||||
$invalidUploadedFileFound = true;
|
||||
$invalidUploadedFile = $uploadedFile;
|
||||
|
||||
break 2;
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid uploaded file tree; expected UploadedFileInterface instances but %s provided.',
|
||||
\get_debug_type($uploadedFile)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ($invalidUploadedFileFound) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s inside ServerRequestInterface::withUploadedFiles() is deprecated; guzzlehttp/psr7 3.0 requires an UploadedFileInterface[] tree.',
|
||||
\get_debug_type($invalidUploadedFile)
|
||||
);
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->uploadedFiles = $uploadedFiles;
|
||||
|
||||
|
|
@ -346,8 +152,10 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
return $this->cookieParams;
|
||||
}
|
||||
|
||||
public function withCookieParams(array $cookies): ServerRequestInterface
|
||||
{
|
||||
public function withCookieParams(
|
||||
#[\SensitiveParameter]
|
||||
array $cookies
|
||||
): ServerRequestInterface {
|
||||
$new = clone $this;
|
||||
$new->cookieParams = $cookies;
|
||||
|
||||
|
|
@ -378,12 +186,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
public function withParsedBody($data): ServerRequestInterface
|
||||
{
|
||||
if ($data !== null && !\is_array($data) && !\is_object($data)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to ServerRequestInterface::withParsedBody() is deprecated; guzzlehttp/psr7 3.0 requires array|object|null.',
|
||||
\get_debug_type($data)
|
||||
);
|
||||
throw new InvalidArgumentException('Parsed body must be an array, object, or null.');
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
|
|
@ -400,58 +203,31 @@ class ServerRequest extends Request implements ServerRequestInterface
|
|||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAttribute($attribute, $default = null)
|
||||
public function getAttribute(string $name, $default = null)
|
||||
{
|
||||
if (!\is_string($attribute)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to ServerRequestInterface::getAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
|
||||
\get_debug_type($attribute)
|
||||
);
|
||||
}
|
||||
|
||||
if (false === array_key_exists($attribute, $this->attributes)) {
|
||||
if (false === array_key_exists($name, $this->attributes)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->attributes[$attribute];
|
||||
return $this->attributes[$name];
|
||||
}
|
||||
|
||||
public function withAttribute($attribute, $value): ServerRequestInterface
|
||||
public function withAttribute(string $name, $value): ServerRequestInterface
|
||||
{
|
||||
if (!\is_string($attribute)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to ServerRequestInterface::withAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
|
||||
\get_debug_type($attribute)
|
||||
);
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
$new->attributes[$attribute] = $value;
|
||||
$new->attributes[$name] = $value;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withoutAttribute($attribute): ServerRequestInterface
|
||||
public function withoutAttribute(string $name): ServerRequestInterface
|
||||
{
|
||||
if (!\is_string($attribute)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to ServerRequestInterface::withoutAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
|
||||
\get_debug_type($attribute)
|
||||
);
|
||||
}
|
||||
|
||||
if (false === array_key_exists($attribute, $this->attributes)) {
|
||||
if (false === array_key_exists($name, $this->attributes)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$new = clone $this;
|
||||
unset($new->attributes[$attribute]);
|
||||
unset($new->attributes[$name]);
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
|
|
|||
504
vendor/guzzlehttp/psr7/src/ServerRequestGlobalsFactory.php
vendored
Normal file
504
vendor/guzzlehttp/psr7/src/ServerRequestGlobalsFactory.php
vendored
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ServerRequestGlobalsFactory
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server Typically the $_SERVER superglobal
|
||||
* @param array<array-key, mixed> $query Typically the $_GET superglobal
|
||||
* @param array<array-key, mixed> $post Typically the $_POST superglobal
|
||||
* @param array<array-key, mixed> $cookies Typically the $_COOKIE superglobal
|
||||
* @param array<array-key, mixed> $files Typically the $_FILES superglobal
|
||||
* @param (callable(): mixed)|null $headerProvider
|
||||
*/
|
||||
public static function fromArrays(
|
||||
#[\SensitiveParameter]
|
||||
array $server,
|
||||
array $query,
|
||||
array $post,
|
||||
#[\SensitiveParameter]
|
||||
array $cookies,
|
||||
array $files,
|
||||
?callable $headerProvider = null
|
||||
): ServerRequestInterface {
|
||||
$method = self::getRequestMethodFromServer($server);
|
||||
$headers = self::removeInvalidHostHeader(self::getAllHeaders($server, $headerProvider));
|
||||
[$uri, $requestTarget] = self::getUriAndRequestTargetFromServer($server, $method);
|
||||
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
|
||||
|
||||
$serverRequest = new ServerRequest($method, $uri, $headers, $body, self::getProtocolFromServer($server), $server);
|
||||
if ($requestTarget !== null) {
|
||||
/** @var ServerRequestInterface $serverRequest */
|
||||
$serverRequest = $serverRequest->withRequestTarget($requestTarget);
|
||||
}
|
||||
|
||||
return $serverRequest
|
||||
->withCookieParams($cookies)
|
||||
->withQueryParams($query)
|
||||
->withParsedBody($post)
|
||||
->withUploadedFiles(UploadedFileNormalizer::normalize($files));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server Typically the $_SERVER superglobal
|
||||
*/
|
||||
public static function getUriFromServerParams(
|
||||
#[\SensitiveParameter]
|
||||
array $server
|
||||
): UriInterface {
|
||||
$method = self::getRequestMethodFromServer($server);
|
||||
|
||||
return self::getUriAndRequestTargetFromServer($server, $method)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
* @param (callable(): mixed)|null $headerProvider
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function getAllHeaders(
|
||||
#[\SensitiveParameter]
|
||||
array $server,
|
||||
?callable $headerProvider
|
||||
): array {
|
||||
$headers = $headerProvider !== null ? $headerProvider() : false;
|
||||
|
||||
if (!is_array($headers)) {
|
||||
$headers = self::getHeadersFromServer($server);
|
||||
}
|
||||
|
||||
return self::normalizeHeaderValues($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $headers
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function normalizeHeaderValues(
|
||||
#[\SensitiveParameter]
|
||||
array $headers
|
||||
): array {
|
||||
$normalized = [];
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||
$normalized[$name] = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server Typically the $_SERVER superglobal
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function getHeadersFromServer(array $server): array
|
||||
{
|
||||
$headers = [];
|
||||
|
||||
$copyServer = [
|
||||
'CONTENT_TYPE' => 'Content-Type',
|
||||
'CONTENT_LENGTH' => 'Content-Length',
|
||||
'CONTENT_MD5' => 'Content-Md5',
|
||||
];
|
||||
|
||||
foreach ($server as $key => $value) {
|
||||
if (!is_string($key) || !is_string($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$header = substr($key, 5);
|
||||
|
||||
if (isset($copyServer[$header], $server[$header]) && is_string($server[$header])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode(' ', Utils::asciiToLower(str_replace('_', ' ', $header)));
|
||||
foreach ($parts as $i => $part) {
|
||||
$parts[$i] = Utils::asciiUcFirst($part);
|
||||
}
|
||||
$header = implode('-', $parts);
|
||||
$headers[$header] = $value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($copyServer[$key])) {
|
||||
$headers[$copyServer[$key]] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($headers['Authorization'])) {
|
||||
if (isset($server['REDIRECT_HTTP_AUTHORIZATION']) && is_string($server['REDIRECT_HTTP_AUTHORIZATION'])) {
|
||||
$headers['Authorization'] = $server['REDIRECT_HTTP_AUTHORIZATION'];
|
||||
} elseif (isset($server['PHP_AUTH_USER']) && is_string($server['PHP_AUTH_USER'])) {
|
||||
$password = isset($server['PHP_AUTH_PW']) && is_string($server['PHP_AUTH_PW'])
|
||||
? $server['PHP_AUTH_PW']
|
||||
: '';
|
||||
|
||||
$headers['Authorization'] = 'Basic '.base64_encode($server['PHP_AUTH_USER'].':'.$password);
|
||||
} elseif (isset($server['PHP_AUTH_DIGEST']) && is_string($server['PHP_AUTH_DIGEST'])) {
|
||||
$headers['Authorization'] = $server['PHP_AUTH_DIGEST'];
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string> $headers
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private static function removeInvalidHostHeader(array $headers): array
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
if (Utils::asciiToLower((string) $name) !== 'host') {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$host] = self::extractHostAndPortFromAuthority($value);
|
||||
if ($host === null) {
|
||||
unset($headers[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
*/
|
||||
private static function getServerParam(array $server, string $key): ?string
|
||||
{
|
||||
return isset($server[$key]) && is_string($server[$key]) ? $server[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
*/
|
||||
private static function getRequestMethodFromServer(array $server): string
|
||||
{
|
||||
return Utils::asciiToUpper(self::getServerParam($server, 'REQUEST_METHOD') ?? 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
*/
|
||||
private static function getProtocolFromServer(array $server): string
|
||||
{
|
||||
$serverProtocol = self::getServerParam($server, 'SERVER_PROTOCOL');
|
||||
if ($serverProtocol === null) {
|
||||
return '1.1';
|
||||
}
|
||||
|
||||
return str_starts_with($serverProtocol, 'HTTP/') ? substr($serverProtocol, 5) : $serverProtocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string|null, 1: int|null}
|
||||
*/
|
||||
private static function extractHostAndPortFromAuthority(string $authority): array
|
||||
{
|
||||
return Rfc9112::parseHostHeader($authority) ?? [null, null];
|
||||
}
|
||||
|
||||
private static function parseServerPort(string $port): int
|
||||
{
|
||||
$parsed = Rfc9112::parsePort($port);
|
||||
if ($parsed === null) {
|
||||
throw new InvalidArgumentException('Invalid SERVER_PORT; expected an integer between 1 and 65535.');
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
private static function withHostFromServer(UriInterface $uri, ?string $host): ?UriInterface
|
||||
{
|
||||
if ($host === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $uri->withHost($host);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
*/
|
||||
private static function getUriWithSchemeFromServer(array $server): UriInterface
|
||||
{
|
||||
$uri = new Uri('');
|
||||
|
||||
$https = self::getServerParam($server, 'HTTPS');
|
||||
|
||||
return $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
*/
|
||||
private static function getAuthorityUriFromServer(
|
||||
#[\SensitiveParameter]
|
||||
array $server
|
||||
): UriInterface {
|
||||
$uri = self::getUriWithSchemeFromServer($server);
|
||||
|
||||
$hasPort = false;
|
||||
$hasHost = false;
|
||||
$authority = self::getServerParam($server, 'HTTP_HOST');
|
||||
if ($authority !== null) {
|
||||
[$host, $port] = self::extractHostAndPortFromAuthority($authority);
|
||||
if ($host !== null) {
|
||||
$hostUri = self::withHostFromServer($uri, $host);
|
||||
if ($hostUri !== null) {
|
||||
$uri = $hostUri;
|
||||
$hasHost = true;
|
||||
|
||||
if ($port !== null) {
|
||||
$hasPort = true;
|
||||
$uri = $uri->withPort($port);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['SERVER_NAME', 'SERVER_ADDR'] as $serverParam) {
|
||||
if ($hasHost) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hostUri = self::withHostFromServer($uri, self::getServerParam($server, $serverParam));
|
||||
if ($hostUri !== null) {
|
||||
$uri = $hostUri;
|
||||
$hasHost = true;
|
||||
}
|
||||
}
|
||||
|
||||
$serverPort = self::getServerParam($server, 'SERVER_PORT');
|
||||
if (!$hasPort && $serverPort !== null) {
|
||||
$uri = $uri->withPort(self::parseServerPort($serverPort));
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $server
|
||||
*
|
||||
* @return array{0: UriInterface, 1: string|null}
|
||||
*/
|
||||
private static function getUriAndRequestTargetFromServer(
|
||||
#[\SensitiveParameter]
|
||||
array $server,
|
||||
string $method
|
||||
): array {
|
||||
$requestUri = self::getServerParam($server, 'REQUEST_URI');
|
||||
$queryString = self::getServerParam($server, 'QUERY_STRING');
|
||||
|
||||
if ($requestUri !== null) {
|
||||
$connectAuthority = self::parseConnectAuthorityFormRequestTarget($method, $requestUri);
|
||||
if ($connectAuthority !== null) {
|
||||
[$host, $port] = $connectAuthority;
|
||||
$uri = self::getUriWithSchemeFromServer($server);
|
||||
|
||||
return [
|
||||
$uri->withHost($host)->withPort($port)->withPath('')->withQuery(''),
|
||||
$requestUri,
|
||||
];
|
||||
}
|
||||
|
||||
$absoluteForm = self::getAbsoluteFormUriAndRequestTarget($requestUri, $queryString);
|
||||
if ($absoluteForm !== null) {
|
||||
return $absoluteForm;
|
||||
}
|
||||
}
|
||||
|
||||
$uri = self::getAuthorityUriFromServer($server);
|
||||
|
||||
if ($requestUri === null) {
|
||||
if ($queryString !== null) {
|
||||
$uri = $uri->withQuery($queryString);
|
||||
}
|
||||
|
||||
return [$uri, null];
|
||||
}
|
||||
|
||||
if (Rfc9112::isAsteriskFormRequestTarget($method, $requestUri)) {
|
||||
return [$uri->withPath('')->withQuery(''), '*'];
|
||||
}
|
||||
|
||||
[$path, $query, $hasQuery] = self::splitRequestTargetQuery($requestUri);
|
||||
$uri = $uri->withPath(self::normalizeOriginFormPathFromServer($path));
|
||||
|
||||
if ($hasQuery) {
|
||||
$uri = $uri->withQuery($query);
|
||||
} elseif ($queryString !== null) {
|
||||
$uri = $uri->withQuery($queryString);
|
||||
}
|
||||
|
||||
return [$uri, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: UriInterface, 1: string}|null
|
||||
*/
|
||||
private static function getAbsoluteFormUriAndRequestTarget(
|
||||
#[\SensitiveParameter]
|
||||
string $requestUri,
|
||||
?string $queryString
|
||||
): ?array {
|
||||
if (!Rfc9112::isAbsoluteFormRequestTarget($requestUri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$targetUri = (new Uri($requestUri))->withFragment('');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($targetUri->getHost() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestTarget = self::removeRequestTargetFragment($requestUri);
|
||||
$requestTargetWithoutUserInfo = self::removeUserInfoFromAbsoluteFormRequestTarget($requestTarget);
|
||||
if ($requestTargetWithoutUserInfo !== $requestTarget) {
|
||||
$targetUri = $targetUri->withUserInfo('');
|
||||
$requestTarget = $requestTargetWithoutUserInfo;
|
||||
}
|
||||
|
||||
if (!str_contains($requestTarget, '?') && $queryString !== null && $queryString !== '') {
|
||||
$targetUri = $targetUri->withQuery($queryString);
|
||||
$requestTarget .= '?'.$queryString;
|
||||
}
|
||||
|
||||
// Preserve the received absolute-form target unless it cannot be used as
|
||||
// a PSR-7 request target without normalization.
|
||||
$normalizeRequestTarget = !Rfc9112::isValidRequestTarget($requestTarget)
|
||||
|| self::hasEmptyPortInAbsoluteFormRequestTarget($requestTarget);
|
||||
|
||||
return [$targetUri, $normalizeRequestTarget ? (string) $targetUri : $requestTarget];
|
||||
}
|
||||
|
||||
private static function removeUserInfoFromAbsoluteFormRequestTarget(string $target): string
|
||||
{
|
||||
$authorityStart = strpos($target, '://');
|
||||
if ($authorityStart === false) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$authorityStart += 3;
|
||||
$authorityLength = strcspn($target, '/?#', $authorityStart);
|
||||
$authority = substr($target, $authorityStart, $authorityLength);
|
||||
if ($authority === '') {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$lastAt = strrpos($authority, '@');
|
||||
if ($lastAt === false) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
$authorityEnd = $authorityStart + $authorityLength;
|
||||
|
||||
return substr($target, 0, $authorityStart)
|
||||
.substr($authority, $lastAt + 1)
|
||||
.substr($target, $authorityEnd);
|
||||
}
|
||||
|
||||
private static function hasEmptyPortInAbsoluteFormRequestTarget(string $target): bool
|
||||
{
|
||||
$authorityStart = strpos($target, '://');
|
||||
if ($authorityStart === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorityStart += 3;
|
||||
$authority = substr($target, $authorityStart, strcspn($target, '/?#', $authorityStart));
|
||||
if ($authority === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lastAt = strrpos($authority, '@');
|
||||
if ($lastAt !== false) {
|
||||
$authority = substr($authority, $lastAt + 1);
|
||||
}
|
||||
|
||||
if ($authority === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_starts_with($authority, '[')) {
|
||||
$closingBracket = strpos($authority, ']');
|
||||
|
||||
return $closingBracket !== false && substr($authority, $closingBracket + 1) === ':';
|
||||
}
|
||||
|
||||
return str_ends_with($authority, ':');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: int}|null
|
||||
*/
|
||||
private static function parseConnectAuthorityFormRequestTarget(string $method, string $target): ?array
|
||||
{
|
||||
if (!Rfc9112::isConnectAuthorityFormRequestTarget($method, $target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$host, $port] = self::extractHostAndPortFromAuthority($target);
|
||||
if ($host === null || $port === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$host, $port];
|
||||
}
|
||||
|
||||
private static function removeRequestTargetFragment(string $target): string
|
||||
{
|
||||
return explode('#', $target, 2)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: string, 2: bool}
|
||||
*/
|
||||
private static function splitRequestTargetQuery(string $target): array
|
||||
{
|
||||
$parts = explode('?', $target, 2);
|
||||
|
||||
return [$parts[0], $parts[1] ?? '', isset($parts[1])];
|
||||
}
|
||||
|
||||
private static function normalizeOriginFormPathFromServer(string $path): string
|
||||
{
|
||||
if ($path === '' || str_starts_with($path, '/')) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return '/'.$path;
|
||||
}
|
||||
}
|
||||
198
vendor/guzzlehttp/psr7/src/Stream.php
vendored
198
vendor/guzzlehttp/psr7/src/Stream.php
vendored
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use GuzzleHttp\Psr7\Exception\TimeoutException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -11,27 +12,17 @@ use Psr\Http\Message\StreamInterface;
|
|||
*/
|
||||
class Stream implements StreamInterface
|
||||
{
|
||||
/**
|
||||
* @see https://www.php.net/manual/en/function.fopen.php
|
||||
* @see https://www.php.net/manual/en/function.gzopen.php
|
||||
*/
|
||||
private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
|
||||
private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
|
||||
use NonSerializableStreamTrait;
|
||||
|
||||
/** @var resource */
|
||||
private $stream;
|
||||
/** @var int|null */
|
||||
private $size;
|
||||
/** @var bool */
|
||||
private $seekable;
|
||||
/** @var bool */
|
||||
private $readable;
|
||||
/** @var bool */
|
||||
private $writable;
|
||||
/** @var string|null */
|
||||
private $uri;
|
||||
private ?int $size = null;
|
||||
private bool $seekable;
|
||||
private bool $readable;
|
||||
private bool $writable;
|
||||
private ?string $uri = null;
|
||||
/** @var mixed[] */
|
||||
private $customMetadata;
|
||||
private array $customMetadata;
|
||||
|
||||
/**
|
||||
* This constructor accepts an associative array of options.
|
||||
|
|
@ -53,16 +44,14 @@ class Stream implements StreamInterface
|
|||
throw new \InvalidArgumentException('Stream must be a resource');
|
||||
}
|
||||
|
||||
if (isset($options['size'])) {
|
||||
$this->size = $options['size'];
|
||||
}
|
||||
$this->size = Integers::assertOptionalNonNegativeSize($options['size'] ?? null, 'Stream size');
|
||||
|
||||
$this->customMetadata = $options['metadata'] ?? [];
|
||||
$this->stream = $stream;
|
||||
$meta = stream_get_meta_data($this->stream);
|
||||
$this->seekable = $meta['seekable'];
|
||||
$this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']);
|
||||
$this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']);
|
||||
$this->readable = self::isReadableMode($meta['mode']);
|
||||
$this->writable = self::isWritableMode($meta['mode']);
|
||||
$this->uri = $meta['uri'] ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -76,20 +65,11 @@ class Stream implements StreamInterface
|
|||
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
if ($this->isSeekable()) {
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
return $this->getContents();
|
||||
} catch (\Throwable $e) {
|
||||
if (\PHP_VERSION_ID >= 70400) {
|
||||
throw $e;
|
||||
}
|
||||
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
|
||||
|
||||
return '';
|
||||
if ($this->isSeekable()) {
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
return $this->getContents();
|
||||
}
|
||||
|
||||
public function getContents(): string
|
||||
|
|
@ -145,13 +125,13 @@ class Stream implements StreamInterface
|
|||
}
|
||||
|
||||
$stats = fstat($this->stream);
|
||||
if (is_array($stats) && isset($stats['size'])) {
|
||||
$this->size = $stats['size'];
|
||||
|
||||
return $this->size;
|
||||
if ($stats === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
$this->size = Integers::assertEngineInteger($stats['size'], 'Stream size');
|
||||
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
|
|
@ -185,12 +165,16 @@ class Stream implements StreamInterface
|
|||
}
|
||||
|
||||
$result = ftell($this->stream);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('Unable to determine stream position');
|
||||
}
|
||||
|
||||
return $result;
|
||||
$position = Integers::assertEngineInteger($result, 'Stream position');
|
||||
if ($position === null) {
|
||||
throw new \RuntimeException('Unable to determine stream position');
|
||||
}
|
||||
|
||||
return $position;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
|
|
@ -198,28 +182,8 @@ class Stream implements StreamInterface
|
|||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
$whence = (int) $whence;
|
||||
|
||||
if (!isset($this->stream)) {
|
||||
throw new \RuntimeException('Stream is detached');
|
||||
}
|
||||
|
|
@ -232,17 +196,8 @@ class Stream implements StreamInterface
|
|||
}
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($this->stream)) {
|
||||
throw new \RuntimeException('Stream is detached');
|
||||
}
|
||||
|
|
@ -259,28 +214,33 @@ class Stream implements StreamInterface
|
|||
|
||||
try {
|
||||
$string = fread($this->stream, $length);
|
||||
} catch (TimeoutException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
if ($this->timedOut()) {
|
||||
throw new TimeoutException('Unable to read from stream: timed out', 0, $e);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Unable to read from stream', 0, $e);
|
||||
}
|
||||
|
||||
if (false === $string) {
|
||||
if ($this->timedOut()) {
|
||||
throw new TimeoutException('Unable to read from stream: timed out');
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Unable to read from stream');
|
||||
}
|
||||
|
||||
if ($string === '' && $this->timedOut()) {
|
||||
throw new TimeoutException('Unable to read from stream: timed out');
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($this->stream)) {
|
||||
throw new \RuntimeException('Stream is detached');
|
||||
}
|
||||
|
|
@ -288,34 +248,48 @@ class Stream implements StreamInterface
|
|||
throw new \RuntimeException('Cannot write to a non-writable stream');
|
||||
}
|
||||
|
||||
if ($string === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We can't know the size after writing anything
|
||||
$this->size = null;
|
||||
$result = fwrite($this->stream, $string);
|
||||
|
||||
try {
|
||||
$result = fwrite($this->stream, $string);
|
||||
} catch (TimeoutException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
if ($this->writeTimedOut()) {
|
||||
throw new TimeoutException('Unable to write to stream: timed out', 0, $e);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Unable to write to stream', 0, $e);
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
if ($this->writeTimedOut()) {
|
||||
throw new TimeoutException('Unable to write to stream: timed out');
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Unable to write to stream');
|
||||
}
|
||||
|
||||
if ($result === 0 && $this->writeTimedOut()) {
|
||||
throw new TimeoutException('Unable to write to stream: timed out');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
public function getMetadata(?string $key = null)
|
||||
{
|
||||
if ($key !== null && !\is_string($key)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
|
||||
\get_debug_type($key)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($this->stream)) {
|
||||
return $key ? null : [];
|
||||
} elseif (!$key) {
|
||||
return $key === null ? [] : null;
|
||||
} elseif ($key === null) {
|
||||
return $this->customMetadata + stream_get_meta_data($this->stream);
|
||||
} elseif (isset($this->customMetadata[$key])) {
|
||||
return $this->customMetadata[$key];
|
||||
|
|
@ -325,4 +299,36 @@ class Stream implements StreamInterface
|
|||
|
||||
return $meta[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.php.net/manual/en/function.fopen.php
|
||||
* @see https://www.php.net/manual/en/function.gzopen.php
|
||||
*/
|
||||
private static function isReadableMode(string $mode): bool
|
||||
{
|
||||
return str_starts_with($mode, 'r') || str_contains($mode, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.php.net/manual/en/function.fopen.php
|
||||
* @see https://www.php.net/manual/en/function.gzopen.php
|
||||
*/
|
||||
private static function isWritableMode(string $mode): bool
|
||||
{
|
||||
return str_starts_with($mode, 'a')
|
||||
|| str_starts_with($mode, 'w')
|
||||
|| str_starts_with($mode, 'x')
|
||||
|| str_starts_with($mode, 'c')
|
||||
|| str_contains($mode, '+');
|
||||
}
|
||||
|
||||
private function timedOut(): bool
|
||||
{
|
||||
return StreamTimeout::isResourceReadTimedOut($this->stream);
|
||||
}
|
||||
|
||||
private function writeTimedOut(): bool
|
||||
{
|
||||
return StreamTimeout::isResourceWriteTimedOut($this->stream);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ trait StreamDecoratorTrait
|
|||
/**
|
||||
* Magic method used to create a new stream if streams are not added in
|
||||
* the constructor of a decorator (e.g., LazyOpenStream).
|
||||
*
|
||||
* @return StreamInterface
|
||||
*/
|
||||
public function __get(string $name)
|
||||
public function __get(string $name): StreamInterface
|
||||
{
|
||||
if ($name === 'stream') {
|
||||
$this->stream = $this->createStream();
|
||||
|
|
@ -35,25 +33,16 @@ trait StreamDecoratorTrait
|
|||
return $this->stream;
|
||||
}
|
||||
|
||||
throw new \UnexpectedValueException("$name not found on class");
|
||||
throw new \UnexpectedValueException(\sprintf('%s not found on class', DiagnosticValue::escape($name)));
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
if ($this->isSeekable()) {
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
return $this->getContents();
|
||||
} catch (\Throwable $e) {
|
||||
if (\PHP_VERSION_ID >= 70400) {
|
||||
throw $e;
|
||||
}
|
||||
trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
|
||||
|
||||
return '';
|
||||
if ($this->isSeekable()) {
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
return $this->getContents();
|
||||
}
|
||||
|
||||
public function getContents(): string
|
||||
|
|
@ -84,17 +73,8 @@ trait StreamDecoratorTrait
|
|||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
public function getMetadata(?string $key = null)
|
||||
{
|
||||
if ($key !== null && !\is_string($key)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
|
||||
\get_debug_type($key)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->stream->getMetadata($key);
|
||||
}
|
||||
|
||||
|
|
@ -138,54 +118,22 @@ trait StreamDecoratorTrait
|
|||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!\is_int($offset)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
|
||||
\get_debug_type($offset)
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_int($whence)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
|
||||
\get_debug_type($whence)
|
||||
);
|
||||
}
|
||||
|
||||
$this->stream->seek($offset, $whence);
|
||||
}
|
||||
|
||||
public function read($length): string
|
||||
public function read(int $length): string
|
||||
{
|
||||
if (!\is_int($length)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
|
||||
\get_debug_type($length)
|
||||
);
|
||||
if ($length < 0) {
|
||||
throw new \RuntimeException('Length parameter cannot be negative');
|
||||
}
|
||||
|
||||
return $this->stream->read($length);
|
||||
}
|
||||
|
||||
public function write($string): int
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!\is_string($string)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
|
||||
\get_debug_type($string)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->stream->write($string);
|
||||
}
|
||||
|
||||
|
|
|
|||
106
vendor/guzzlehttp/psr7/src/StreamTimeout.php
vendored
Normal file
106
vendor/guzzlehttp/psr7/src/StreamTimeout.php
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use GuzzleHttp\Psr7\Exception\TimeoutException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StreamTimeout
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function read(StreamInterface $stream, int $length, string $timeoutMessage): string
|
||||
{
|
||||
try {
|
||||
$buffer = $stream->read($length);
|
||||
} catch (TimeoutException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
self::throwIfReadTimedOut($stream, $timeoutMessage, $e);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($buffer === '') {
|
||||
self::throwIfReadTimedOut($stream, $timeoutMessage);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
public static function throwIfReadTimedOut(
|
||||
StreamInterface $stream,
|
||||
string $message,
|
||||
?\Throwable $previous = null
|
||||
): void {
|
||||
if (self::isReadTimedOut($stream)) {
|
||||
throw new TimeoutException($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public static function throwIfWriteTimedOut(StreamInterface $stream, ?\Throwable $previous = null): void
|
||||
{
|
||||
if (self::isWriteTimedOut($stream)) {
|
||||
throw new TimeoutException('Unable to write to stream: timed out', 0, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public static function isReadTimedOut(StreamInterface $stream): bool
|
||||
{
|
||||
try {
|
||||
if ($stream->getMetadata('timed_out') !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$stream->eof();
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function isWriteTimedOut(StreamInterface $stream): bool
|
||||
{
|
||||
try {
|
||||
return $stream->getMetadata('timed_out') === true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $resource
|
||||
*/
|
||||
public static function isResourceReadTimedOut($resource): bool
|
||||
{
|
||||
try {
|
||||
/** @var array<string, mixed> $metadata */
|
||||
$metadata = stream_get_meta_data($resource);
|
||||
|
||||
return ($metadata['timed_out'] ?? false) === true && !feof($resource);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $resource
|
||||
*/
|
||||
public static function isResourceWriteTimedOut($resource): bool
|
||||
{
|
||||
try {
|
||||
/** @var array<string, mixed> $metadata */
|
||||
$metadata = stream_get_meta_data($resource);
|
||||
|
||||
return ($metadata['timed_out'] ?? false) === true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
vendor/guzzlehttp/psr7/src/StreamWrapper.php
vendored
70
vendor/guzzlehttp/psr7/src/StreamWrapper.php
vendored
|
|
@ -16,11 +16,10 @@ final class StreamWrapper
|
|||
/** @var resource */
|
||||
public $context;
|
||||
|
||||
/** @var StreamInterface */
|
||||
private $stream;
|
||||
private StreamInterface $stream;
|
||||
|
||||
/** @var string r, r+, or w */
|
||||
private $mode;
|
||||
private string $mode;
|
||||
|
||||
/**
|
||||
* Returns a resource representing the stream.
|
||||
|
|
@ -78,40 +77,67 @@ final class StreamWrapper
|
|||
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool
|
||||
{
|
||||
$options = stream_context_get_options($this->context);
|
||||
$stream = $options['guzzle']['stream'] ?? null;
|
||||
|
||||
if (!isset($options['guzzle']['stream'])) {
|
||||
if (!$stream instanceof StreamInterface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->mode = $mode;
|
||||
$this->stream = $options['guzzle']['stream'];
|
||||
$this->stream = $stream;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function stream_read(int $count): string
|
||||
/**
|
||||
* @return string|false
|
||||
*/
|
||||
public function stream_read(int $count)
|
||||
{
|
||||
return $this->stream->read($count);
|
||||
try {
|
||||
return $this->stream->read($count);
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function stream_write(string $data): int
|
||||
{
|
||||
return $this->stream->write($data);
|
||||
try {
|
||||
return $this->stream->write($data);
|
||||
} catch (\RuntimeException $e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public function stream_tell(): int
|
||||
/**
|
||||
* @return int|false
|
||||
*/
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->stream->tell();
|
||||
try {
|
||||
return $this->stream->tell();
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function stream_eof(): bool
|
||||
{
|
||||
return $this->stream->eof();
|
||||
try {
|
||||
return $this->stream->eof();
|
||||
} catch (\RuntimeException $e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public function stream_seek(int $offset, int $whence): bool
|
||||
{
|
||||
$this->stream->seek($offset, $whence);
|
||||
try {
|
||||
$this->stream->seek($offset, $whence);
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -121,8 +147,12 @@ final class StreamWrapper
|
|||
*/
|
||||
public function stream_cast(int $cast_as)
|
||||
{
|
||||
$stream = clone $this->stream;
|
||||
$resource = $stream->detach();
|
||||
try {
|
||||
$stream = clone $this->stream;
|
||||
$resource = $stream->detach();
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $resource ?? false;
|
||||
}
|
||||
|
|
@ -146,7 +176,13 @@ final class StreamWrapper
|
|||
*/
|
||||
public function stream_stat()
|
||||
{
|
||||
if ($this->stream->getSize() === null) {
|
||||
try {
|
||||
$size = $this->stream->getSize();
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($size === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -161,12 +197,12 @@ final class StreamWrapper
|
|||
return [
|
||||
'dev' => 0,
|
||||
'ino' => 0,
|
||||
'mode' => $modeMap[$this->mode],
|
||||
'mode' => $modeMap[$this->mode] ?? 0,
|
||||
'nlink' => 0,
|
||||
'uid' => 0,
|
||||
'gid' => 0,
|
||||
'rdev' => 0,
|
||||
'size' => $this->stream->getSize() ?: 0,
|
||||
'size' => $size,
|
||||
'atime' => 0,
|
||||
'mtime' => 0,
|
||||
'ctime' => 0,
|
||||
|
|
|
|||
57
vendor/guzzlehttp/psr7/src/UploadedFile.php
vendored
57
vendor/guzzlehttp/psr7/src/UploadedFile.php
vendored
|
|
@ -22,40 +22,19 @@ class UploadedFile implements UploadedFileInterface
|
|||
UPLOAD_ERR_EXTENSION => 'UPLOAD_ERR_EXTENSION',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $clientFilename;
|
||||
private ?string $clientFilename;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $clientMediaType;
|
||||
private ?string $clientMediaType;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $error;
|
||||
private int $error;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $file;
|
||||
private ?string $file = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $moved = false;
|
||||
private bool $moved = false;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $size;
|
||||
private ?int $size;
|
||||
|
||||
/**
|
||||
* @var StreamInterface|null
|
||||
*/
|
||||
private $stream;
|
||||
private ?StreamInterface $stream = null;
|
||||
|
||||
/**
|
||||
* @param StreamInterface|string|resource $streamOrFile
|
||||
|
|
@ -68,7 +47,7 @@ class UploadedFile implements UploadedFileInterface
|
|||
?string $clientMediaType = null
|
||||
) {
|
||||
$this->setError($errorStatus);
|
||||
$this->size = $size;
|
||||
$this->size = Integers::assertOptionalNonNegativeSize($size, 'Uploaded file size');
|
||||
$this->clientFilename = $clientFilename;
|
||||
$this->clientMediaType = $clientMediaType;
|
||||
|
||||
|
|
@ -113,11 +92,6 @@ class UploadedFile implements UploadedFileInterface
|
|||
$this->error = $error;
|
||||
}
|
||||
|
||||
private static function isStringNotEmpty($param): bool
|
||||
{
|
||||
return is_string($param) && false === empty($param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if there is no upload error
|
||||
*/
|
||||
|
|
@ -159,11 +133,11 @@ class UploadedFile implements UploadedFileInterface
|
|||
return new LazyOpenStream($file, 'r+');
|
||||
}
|
||||
|
||||
public function moveTo($targetPath): void
|
||||
public function moveTo(string $targetPath): void
|
||||
{
|
||||
$this->validateActive();
|
||||
|
||||
if (false === self::isStringNotEmpty($targetPath)) {
|
||||
if ($targetPath === '') {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid path provided for move operation; must be a non-empty string'
|
||||
);
|
||||
|
|
@ -174,8 +148,13 @@ class UploadedFile implements UploadedFileInterface
|
|||
? rename($this->file, $targetPath)
|
||||
: move_uploaded_file($this->file, $targetPath);
|
||||
} else {
|
||||
$stream = $this->getStream();
|
||||
if ($stream->isSeekable()) {
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
Utils::copyToStream(
|
||||
$this->getStream(),
|
||||
$stream,
|
||||
new LazyOpenStream($targetPath, 'w')
|
||||
);
|
||||
|
||||
|
|
@ -183,9 +162,7 @@ class UploadedFile implements UploadedFileInterface
|
|||
}
|
||||
|
||||
if (false === $this->moved) {
|
||||
throw new RuntimeException(
|
||||
sprintf('Uploaded file could not be moved to %s', $targetPath)
|
||||
);
|
||||
throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', DiagnosticValue::escape($targetPath)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
130
vendor/guzzlehttp/psr7/src/UploadedFileNormalizer.php
vendored
Normal file
130
vendor/guzzlehttp/psr7/src/UploadedFileNormalizer.php
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @phpstan-type UploadedFileTree array<array-key, UploadedFileInterface|array>
|
||||
*/
|
||||
final class UploadedFileNormalizer
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an UploadedFile instance array.
|
||||
*
|
||||
* @param array $files An array which respect $_FILES structure
|
||||
*
|
||||
* @return UploadedFileTree
|
||||
*
|
||||
* @throws InvalidArgumentException for unrecognized values
|
||||
*/
|
||||
public static function normalize(array $files): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($files as $key => $value) {
|
||||
if ($value instanceof UploadedFileInterface) {
|
||||
$normalized[$key] = $value;
|
||||
} elseif (is_array($value) && array_key_exists('tmp_name', $value)) {
|
||||
$normalized[$key] = self::createUploadedFileFromSpec($value);
|
||||
} elseif (is_array($value)) {
|
||||
$normalized[$key] = self::normalize($value);
|
||||
continue;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid value in files specification');
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an UploadedFile instance from a $_FILES specification.
|
||||
*
|
||||
* If the specification represents an array of values, this method will
|
||||
* delegate to normalizeNestedFileSpec() and return that return value.
|
||||
*
|
||||
* @param array $value $_FILES struct
|
||||
*
|
||||
* @return UploadedFileInterface|UploadedFileTree
|
||||
*/
|
||||
private static function createUploadedFileFromSpec(array $value)
|
||||
{
|
||||
self::assertFileSpec($value);
|
||||
|
||||
if (is_array($value['tmp_name'])) {
|
||||
return self::normalizeNestedFileSpec($value);
|
||||
}
|
||||
|
||||
return new UploadedFile(
|
||||
$value['tmp_name'],
|
||||
Integers::assertNonNegativeInteger($value['size'], 'Uploaded file size'),
|
||||
Integers::assertNonNegativeInteger($value['error'], 'Uploaded file error'),
|
||||
$value['name'] ?? null,
|
||||
$value['type'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
private static function assertFileSpec(array $value): void
|
||||
{
|
||||
if (!isset($value['tmp_name'], $value['size'], $value['error'])) {
|
||||
throw new InvalidArgumentException('Invalid file specification; expected keys "tmp_name", "size", and "error".');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an array of file specifications.
|
||||
*
|
||||
* Loops through all nested files and returns a normalized array of
|
||||
* UploadedFileInterface instances.
|
||||
*
|
||||
* @return UploadedFileTree
|
||||
*/
|
||||
private static function normalizeNestedFileSpec(array $files = []): array
|
||||
{
|
||||
self::assertNestedFileSpec($files);
|
||||
|
||||
$normalizedFiles = [];
|
||||
|
||||
foreach (array_keys($files['tmp_name']) as $key) {
|
||||
if (!array_key_exists($key, $files['size']) || !array_key_exists($key, $files['error'])) {
|
||||
throw new InvalidArgumentException('Invalid nested file specification; expected "tmp_name", "size", and "error" arrays to have matching keys.');
|
||||
}
|
||||
|
||||
$spec = [
|
||||
'tmp_name' => $files['tmp_name'][$key],
|
||||
'size' => $files['size'][$key],
|
||||
'error' => $files['error'][$key],
|
||||
'name' => $files['name'][$key] ?? null,
|
||||
'type' => $files['type'][$key] ?? null,
|
||||
];
|
||||
$normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
|
||||
}
|
||||
|
||||
return $normalizedFiles;
|
||||
}
|
||||
|
||||
private static function assertNestedFileSpec(array $files): void
|
||||
{
|
||||
foreach (['tmp_name', 'size', 'error'] as $key) {
|
||||
if (!isset($files[$key]) || !is_array($files[$key])) {
|
||||
throw new InvalidArgumentException('Invalid nested file specification; expected keys "tmp_name", "size", and "error" to be arrays.');
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['name', 'type'] as $key) {
|
||||
if (isset($files[$key]) && !is_array($files[$key])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid nested file specification; expected key "%s" to be an array when present.', $key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
496
vendor/guzzlehttp/psr7/src/Uri.php
vendored
496
vendor/guzzlehttp/psr7/src/Uri.php
vendored
|
|
@ -17,10 +17,9 @@ use Psr\Http\Message\UriInterface;
|
|||
class Uri implements UriInterface, \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Absolute http and https URIs require a host per RFC 7230 Section 2.7
|
||||
* but in generic URIs the host can be empty. So for http(s) URIs
|
||||
* we apply this default host when no host is given yet to form a
|
||||
* valid URI.
|
||||
* Absolute http and https URIs require a host per RFC 9110 Section 4.2.1
|
||||
* but in generic URIs the host can be empty. So for http(s) URIs we apply
|
||||
* this default host when no host is given yet to form a valid URI.
|
||||
*/
|
||||
private const HTTP_DEFAULT_HOST = 'localhost';
|
||||
|
||||
|
|
@ -36,37 +35,41 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
'imap' => 143,
|
||||
'pop' => 110,
|
||||
'ldap' => 389,
|
||||
'ws' => 80,
|
||||
'wss' => 443,
|
||||
];
|
||||
|
||||
private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
|
||||
|
||||
/** @var string Uri scheme. */
|
||||
private $scheme = '';
|
||||
private string $scheme = '';
|
||||
|
||||
/** @var string Uri user info. */
|
||||
private $userInfo = '';
|
||||
private string $userInfo = '';
|
||||
|
||||
/** @var string Uri host. */
|
||||
private $host = '';
|
||||
private string $host = '';
|
||||
|
||||
/** @var int|null Uri port. */
|
||||
private $port;
|
||||
private ?int $port = null;
|
||||
|
||||
/** @var string Uri path. */
|
||||
private $path = '';
|
||||
private string $path = '';
|
||||
|
||||
/** @var string Uri query string. */
|
||||
private $query = '';
|
||||
private string $query = '';
|
||||
|
||||
/** @var string Uri fragment. */
|
||||
private $fragment = '';
|
||||
private string $fragment = '';
|
||||
|
||||
public function __construct(string $uri = '')
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
string $uri = ''
|
||||
) {
|
||||
if ($uri !== '') {
|
||||
$parts = self::parse($uri);
|
||||
$parts = UriParser::parse($uri);
|
||||
if ($parts === false) {
|
||||
throw new MalformedUriException("Unable to parse URI: $uri");
|
||||
throw new MalformedUriException(\sprintf('Unable to parse URI: %s', DiagnosticValue::escape($uri)));
|
||||
}
|
||||
try {
|
||||
$this->applyParts($parts);
|
||||
|
|
@ -78,90 +81,6 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 aware \parse_url() replacement.
|
||||
*
|
||||
* The internal function produces broken output for non ASCII domain names
|
||||
* (IDN) when used with locales other than "C".
|
||||
*
|
||||
* On the other hand, cURL understands IDN correctly only when UTF-8 locale
|
||||
* is configured ("C.UTF-8", "en_US.UTF-8", etc.).
|
||||
*
|
||||
* @see https://bugs.php.net/bug.php?id=52923
|
||||
* @see https://www.php.net/manual/en/function.parse-url.php#114817
|
||||
* @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
private static function parse(string $url)
|
||||
{
|
||||
if (self::isPathNoSchemeReference($url)) {
|
||||
return self::parsePathNoSchemeReference($url);
|
||||
}
|
||||
|
||||
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4 tails.
|
||||
$prefix = '';
|
||||
if (preg_match('%^([0-9A-Za-z+.-]+://\[[0-9:.a-fA-F]+\])(.*?)$%', $url, $matches)) {
|
||||
/** @var array{0:string, 1:string, 2:string} $matches */
|
||||
$prefix = $matches[1];
|
||||
$url = $matches[2];
|
||||
}
|
||||
|
||||
/** @var string|null */
|
||||
$encodedUrl = preg_replace_callback(
|
||||
'%[^:/@?&=#]+%usD',
|
||||
static function ($matches) {
|
||||
return urlencode($matches[0]);
|
||||
},
|
||||
$url
|
||||
);
|
||||
|
||||
if ($encodedUrl === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = parse_url($prefix.$encodedUrl);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_map('urldecode', $result);
|
||||
}
|
||||
|
||||
private static function isPathNoSchemeReference(string $url): bool
|
||||
{
|
||||
if ($url === '' || $url[0] === '/' || $url[0] === '?' || $url[0] === '#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstSegment = substr($url, 0, strcspn($url, '/?#'));
|
||||
|
||||
return strpos($firstSegment, ':') === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{path: string, query?: string, fragment?: string}
|
||||
*/
|
||||
private static function parsePathNoSchemeReference(string $url): array
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (false !== ($fragmentPosition = strpos($url, '#'))) {
|
||||
$parts['fragment'] = substr($url, $fragmentPosition + 1);
|
||||
$url = substr($url, 0, $fragmentPosition);
|
||||
}
|
||||
|
||||
if (false !== ($queryPosition = strpos($url, '?'))) {
|
||||
$parts['query'] = substr($url, $queryPosition + 1);
|
||||
$url = substr($url, 0, $queryPosition);
|
||||
}
|
||||
|
||||
$parts['path'] = $url;
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return self::composeComponents(
|
||||
|
|
@ -174,20 +93,25 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
}
|
||||
|
||||
/**
|
||||
* Composes a URI reference string from its various components.
|
||||
* Composes a URI reference string from its various components according to
|
||||
* RFC 3986 Section 5.3.
|
||||
*
|
||||
* Usually this method does not need to be called manually but instead is used indirectly via
|
||||
* `Psr\Http\Message\UriInterface::__toString`.
|
||||
* Usually this method does not need to be called manually but instead is
|
||||
* used indirectly via `Psr\Http\Message\UriInterface::__toString`.
|
||||
*
|
||||
* PSR-7 UriInterface treats an empty component the same as a missing component as
|
||||
* getQuery(), getFragment() etc. always return a string. This explains the slight
|
||||
* difference to RFC 3986 Section 5.3.
|
||||
* PSR-7 UriInterface treats an empty component the same as a missing
|
||||
* component as `getQuery()`, `getFragment()` etc. always return a string.
|
||||
* This explains the slight difference to RFC 3986 Section 5.3.
|
||||
*
|
||||
* Another adjustment is that the authority separator is added even when the authority is missing/empty
|
||||
* for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with
|
||||
* `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But
|
||||
* `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to
|
||||
* that format).
|
||||
* Another adjustment is that the authority separator is added even when the
|
||||
* authority is missing/empty for the "file" scheme. This is because PHP
|
||||
* stream functions like `file_get_contents` only work with `file:///myfile`
|
||||
* but not with `file:/myfile` although they are equivalent according to RFC
|
||||
* 3986. But `file:///` is the more common syntax for the file scheme anyway
|
||||
* (Chrome for example redirects to that format). The separator is omitted
|
||||
* when such a URI has a rootless or empty path: adding it would turn the
|
||||
* first path segment into the authority of the composed URI, or compose the
|
||||
* string `file://`, which cannot be parsed back into a URI.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3
|
||||
*/
|
||||
|
|
@ -200,11 +124,11 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
$uri .= $scheme.':';
|
||||
}
|
||||
|
||||
if ($authority != '' || $scheme === 'file') {
|
||||
if ($authority != '' || ($scheme === 'file' && str_starts_with($path, '/'))) {
|
||||
$uri .= '//'.$authority;
|
||||
}
|
||||
|
||||
if ($authority != '' && $path != '' && $path[0] != '/') {
|
||||
if ($authority != '' && $path != '' && !str_starts_with($path, '/')) {
|
||||
$path = '/'.$path;
|
||||
}
|
||||
|
||||
|
|
@ -224,8 +148,8 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
/**
|
||||
* Whether the URI has the default port of the current scheme.
|
||||
*
|
||||
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
|
||||
* independently of the implementation.
|
||||
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard
|
||||
* port. This method can be used independently of the implementation.
|
||||
*/
|
||||
public static function isDefaultPort(UriInterface $uri): bool
|
||||
{
|
||||
|
|
@ -236,17 +160,18 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
/**
|
||||
* Whether the URI is absolute, i.e. it has a scheme.
|
||||
*
|
||||
* An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
|
||||
* if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
|
||||
* to another URI, the base URI. Relative references can be divided into several forms:
|
||||
* - network-path references, e.g. '//example.com/path'
|
||||
* - absolute-path references, e.g. '/path'
|
||||
* - relative-path references, e.g. 'subpath'
|
||||
* An instance of UriInterface can either be an absolute URI or a relative
|
||||
* reference. An absolute URI has a scheme. A relative reference is used to
|
||||
* express a URI relative to another URI, the base URI. Relative references
|
||||
* can be divided into several forms according to RFC 3986 Section 4.2:
|
||||
* - network-path references, e.g. `//example.com/path`
|
||||
* - absolute-path references, e.g. `/path`
|
||||
* - relative-path references, e.g. `subpath`
|
||||
*
|
||||
* @see Uri::isNetworkPathReference
|
||||
* @see Uri::isAbsolutePathReference
|
||||
* @see Uri::isRelativePathReference
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
|
||||
*/
|
||||
public static function isAbsolute(UriInterface $uri): bool
|
||||
{
|
||||
|
|
@ -256,7 +181,8 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
/**
|
||||
* Whether the URI is a network-path reference.
|
||||
*
|
||||
* A relative reference that begins with two slash characters is termed an network-path reference.
|
||||
* A relative reference that begins with two slash characters is termed a
|
||||
* network-path reference.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
|
||||
*/
|
||||
|
|
@ -266,9 +192,10 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether the URI is a absolute-path reference.
|
||||
* Whether the URI is an absolute-path reference.
|
||||
*
|
||||
* A relative reference that begins with a single slash character is termed an absolute-path reference.
|
||||
* A relative reference that begins with a single slash character is termed
|
||||
* an absolute-path reference.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
|
||||
*/
|
||||
|
|
@ -283,7 +210,8 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
/**
|
||||
* Whether the URI is a relative-path reference.
|
||||
*
|
||||
* A relative reference that does not begin with a slash character is termed a relative-path reference.
|
||||
* A relative reference that does not begin with a slash character is termed
|
||||
* a relative-path reference.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
|
||||
*/
|
||||
|
|
@ -297,9 +225,10 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
/**
|
||||
* Whether the URI is a same-document reference.
|
||||
*
|
||||
* A same-document reference refers to a URI that is, aside from its fragment
|
||||
* component, identical to the base URI. When no base URI is given, only an empty
|
||||
* URI reference (apart from its fragment) is considered a same-document reference.
|
||||
* A same-document reference refers to a URI that is, aside from its
|
||||
* fragment component, identical to the base URI. When no base URI is given,
|
||||
* only an empty URI reference (apart from its fragment) is considered a
|
||||
* same-document reference.
|
||||
*
|
||||
* @param UriInterface $uri The URI to check
|
||||
* @param UriInterface|null $base An optional base URI to compare against
|
||||
|
|
@ -313,7 +242,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
|
||||
return ($uri->getScheme() === $base->getScheme())
|
||||
&& ($uri->getAuthority() === $base->getAuthority())
|
||||
&& ($uri->getPath() === $base->getPath())
|
||||
&& (self::rawPath($uri) === self::rawPath($base))
|
||||
&& ($uri->getQuery() === $base->getQuery());
|
||||
}
|
||||
|
||||
|
|
@ -340,10 +269,9 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
* Creates a new URI with a specific query string value.
|
||||
*
|
||||
* Any existing query string values that exactly match the provided key are
|
||||
* removed and replaced with the given key value pair.
|
||||
*
|
||||
* A value of null will set the query string key without a value, e.g. "key"
|
||||
* instead of "key=value".
|
||||
* removed and replaced with the given key value pair. A value of null will
|
||||
* set the query string key without a value, e.g. "key" instead of
|
||||
* "key=value".
|
||||
*
|
||||
* @param UriInterface $uri URI to use as a base.
|
||||
* @param string $key Key to set.
|
||||
|
|
@ -359,9 +287,10 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a new URI with multiple specific query string values.
|
||||
* Creates a new URI with multiple query string values.
|
||||
*
|
||||
* It has the same behavior as withQueryValue() but for an associative array of key => value.
|
||||
* It has the same behavior as `withQueryValue()` but for an associative
|
||||
* array of key => value.
|
||||
*
|
||||
* @param UriInterface $uri URI to use as a base.
|
||||
* @param (string|null)[] $keyValueArray Associative array of key and values
|
||||
|
|
@ -371,12 +300,26 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
$result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
|
||||
|
||||
foreach ($keyValueArray as $key => $value) {
|
||||
self::assertStringOrNullQueryValue($value);
|
||||
$result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
|
||||
}
|
||||
|
||||
return $uri->withQuery(implode('&', $result));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function assertStringOrNullQueryValue($value): void
|
||||
{
|
||||
if ($value !== null && !is_string($value)) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'Query string values must be a string or null, %s given.',
|
||||
\get_debug_type($value)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URI from a hash of `parse_url` components.
|
||||
*
|
||||
|
|
@ -384,8 +327,10 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
*
|
||||
* @throws MalformedUriException If the components do not form a valid URI.
|
||||
*/
|
||||
public static function fromParts(array $parts): UriInterface
|
||||
{
|
||||
public static function fromParts(
|
||||
#[\SensitiveParameter]
|
||||
array $parts
|
||||
): UriInterface {
|
||||
$uri = new self();
|
||||
try {
|
||||
$uri->applyParts($parts);
|
||||
|
|
@ -406,12 +351,8 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
*/
|
||||
public static function assertValidHost(string $host): void
|
||||
{
|
||||
if ($host === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preg_match('/[\x00-\x20\x7F]/', $host)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
if (!Rfc3986::isValidHost($host)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: %s', DiagnosticValue::escape($host)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,9 +392,52 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
|
||||
public function getPath(): string
|
||||
{
|
||||
if (str_starts_with($this->path, '//')) {
|
||||
return '/'.ltrim($this->path, '/');
|
||||
}
|
||||
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path as it appears within a URI's string form.
|
||||
*
|
||||
* getPath() collapses multiple leading slashes so that a path used in
|
||||
* isolation cannot be mistaken for a protocol-relative URL. Whole-URI
|
||||
* operations like reference resolution and normalization (RFC 3986
|
||||
* Sections 5 and 6) are defined on the URI string form, where the path
|
||||
* stays verbatim, so they must read the path through this method instead.
|
||||
* For direct instances of this class the path is derived from the stored
|
||||
* 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.
|
||||
*
|
||||
* @throws \RuntimeException If the path cannot be split from the string form.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function rawPath(UriInterface $uri): string
|
||||
{
|
||||
if (get_class($uri) === self::class) {
|
||||
if ($uri->path !== '' && !str_starts_with($uri->path, '/') && $uri->getAuthority() !== '') {
|
||||
// composeComponents() prepends a slash to a rootless path when
|
||||
// an authority is present, so the string form uses this path.
|
||||
return '/'.$uri->path;
|
||||
}
|
||||
|
||||
return $uri->path;
|
||||
}
|
||||
|
||||
$count = preg_match('%^(?:[^:/?#]+:)?(?://[^/?#]*)?([^?#]*)%', (string) $uri, $matches);
|
||||
|
||||
if ($count === false) {
|
||||
throw new \RuntimeException('Unable to read the URI path: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $matches[1] ?? '';
|
||||
}
|
||||
|
||||
public function getQuery(): string
|
||||
{
|
||||
return $this->query;
|
||||
|
|
@ -464,7 +448,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $this->fragment;
|
||||
}
|
||||
|
||||
public function withScheme($scheme): UriInterface
|
||||
public function withScheme(string $scheme): UriInterface
|
||||
{
|
||||
$scheme = $this->filterScheme($scheme);
|
||||
|
||||
|
|
@ -480,8 +464,11 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $new;
|
||||
}
|
||||
|
||||
public function withUserInfo($user, $password = null): UriInterface
|
||||
{
|
||||
public function withUserInfo(
|
||||
string $user,
|
||||
#[\SensitiveParameter]
|
||||
?string $password = null
|
||||
): UriInterface {
|
||||
$info = $this->filterUserInfoComponent($user);
|
||||
if ($password !== null) {
|
||||
$info .= ':'.$this->filterUserInfoComponent($password);
|
||||
|
|
@ -498,7 +485,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $new;
|
||||
}
|
||||
|
||||
public function withHost($host): UriInterface
|
||||
public function withHost(string $host): UriInterface
|
||||
{
|
||||
$host = $this->filterHost($host);
|
||||
|
||||
|
|
@ -513,17 +500,8 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $new;
|
||||
}
|
||||
|
||||
public function withPort($port): UriInterface
|
||||
public function withPort(?int $port): UriInterface
|
||||
{
|
||||
if ($port !== null && !\is_int($port)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to UriInterface::withPort() is deprecated; guzzlehttp/psr7 3.0 requires int|null.',
|
||||
\get_debug_type($port)
|
||||
);
|
||||
}
|
||||
|
||||
$port = $this->filterPort($port);
|
||||
|
||||
if ($this->port === $port) {
|
||||
|
|
@ -538,7 +516,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $new;
|
||||
}
|
||||
|
||||
public function withPath($path): UriInterface
|
||||
public function withPath(string $path): UriInterface
|
||||
{
|
||||
$path = $this->filterPath($path);
|
||||
|
||||
|
|
@ -553,7 +531,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $new;
|
||||
}
|
||||
|
||||
public function withQuery($query): UriInterface
|
||||
public function withQuery(string $query): UriInterface
|
||||
{
|
||||
$query = $this->filterQueryAndFragment($query);
|
||||
|
||||
|
|
@ -567,7 +545,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $new;
|
||||
}
|
||||
|
||||
public function withFragment($fragment): UriInterface
|
||||
public function withFragment(string $fragment): UriInterface
|
||||
{
|
||||
$fragment = $this->filterQueryAndFragment($fragment);
|
||||
|
||||
|
|
@ -591,8 +569,10 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
*
|
||||
* @param array $parts Array of parse_url parts to apply.
|
||||
*/
|
||||
private function applyParts(array $parts): void
|
||||
{
|
||||
private function applyParts(
|
||||
#[\SensitiveParameter]
|
||||
array $parts
|
||||
): void {
|
||||
$this->scheme = isset($parts['scheme'])
|
||||
? $this->filterScheme($parts['scheme'])
|
||||
: '';
|
||||
|
|
@ -603,7 +583,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
? $this->filterHost($parts['host'])
|
||||
: '';
|
||||
$this->port = isset($parts['port'])
|
||||
? $this->filterPort($parts['port'])
|
||||
? $this->filterPortPart($parts['port'])
|
||||
: null;
|
||||
$this->path = isset($parts['path'])
|
||||
? $this->filterPath($parts['path'])
|
||||
|
|
@ -622,77 +602,70 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
}
|
||||
|
||||
/**
|
||||
* @param mixed $scheme
|
||||
*
|
||||
* @throws \InvalidArgumentException If the scheme is invalid.
|
||||
*/
|
||||
private function filterScheme($scheme): string
|
||||
private function filterScheme(string $scheme): string
|
||||
{
|
||||
if (!is_string($scheme)) {
|
||||
throw new \InvalidArgumentException('Scheme must be a string');
|
||||
}
|
||||
$scheme = Utils::asciiToLower($scheme);
|
||||
|
||||
$scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
|
||||
if ($scheme !== '' && !preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing "%s" as a URI scheme is deprecated; guzzlehttp/psr7 3.0 requires URI schemes to match RFC 3986 syntax and begin with a letter.',
|
||||
$scheme
|
||||
);
|
||||
if (!Rfc3986::isValidScheme($scheme)) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid scheme: %s', DiagnosticValue::escape($scheme)));
|
||||
}
|
||||
|
||||
return $scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $component
|
||||
*
|
||||
* @throws \InvalidArgumentException If the user info is invalid.
|
||||
*/
|
||||
private function filterUserInfoComponent($component): string
|
||||
{
|
||||
if (!is_string($component)) {
|
||||
throw new \InvalidArgumentException('User info must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$component
|
||||
private function filterUserInfoComponent(
|
||||
#[\SensitiveParameter]
|
||||
string $component
|
||||
): string {
|
||||
return $this->filterComponent(
|
||||
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']++|%(?!'.Rfc3986::HEX_OCTET.'))/',
|
||||
$component,
|
||||
'Unable to filter URI user info'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $host
|
||||
*
|
||||
* @throws \InvalidArgumentException If the host is invalid.
|
||||
*/
|
||||
private function filterHost($host): string
|
||||
private function filterHost(string $host): string
|
||||
{
|
||||
if (!is_string($host)) {
|
||||
throw new \InvalidArgumentException('Host must be a string');
|
||||
$host = Utils::asciiToLower($host);
|
||||
$filtered = \preg_replace_callback('/%'.Rfc3986::HEX_OCTET.'/', static function (array $m): string {
|
||||
return Utils::asciiToUpper($m[0]);
|
||||
}, $host);
|
||||
if ($filtered === null) {
|
||||
throw new \RuntimeException('Unable to normalize URI host percent-encoding: '.\preg_last_error_msg());
|
||||
}
|
||||
self::assertValidHost($filtered);
|
||||
|
||||
if (str_starts_with($filtered, '[') && !str_starts_with($filtered, '[v')) {
|
||||
// assertValidHost() accepted this bracketed value with the same
|
||||
// filter_var() predicate tryCanonicalizeIpv6() validates with, and
|
||||
// its pure-PHP parse cannot fail on filter-accepted text, so the
|
||||
// null guard is defense in depth only.
|
||||
$canonical = Rfc3986::tryCanonicalizeIpv6(substr($filtered, 1, -1));
|
||||
if ($canonical !== null) {
|
||||
$filtered = '['.$canonical.']';
|
||||
}
|
||||
}
|
||||
|
||||
$host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
self::assertValidHost($host);
|
||||
|
||||
return $host;
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $port
|
||||
*
|
||||
* @throws \InvalidArgumentException If the port is invalid.
|
||||
*/
|
||||
private function filterPort($port): ?int
|
||||
private function filterPort(?int $port): ?int
|
||||
{
|
||||
if ($port === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = (int) $port;
|
||||
if (0 > $port || 0xFFFF < $port) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf('Invalid port: %d. Must be between 0 and 65535', $port)
|
||||
|
|
@ -702,6 +675,72 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $port
|
||||
*
|
||||
* @throws \InvalidArgumentException If the port is invalid.
|
||||
*/
|
||||
private function filterPortPart($port): ?int
|
||||
{
|
||||
if (\is_int($port)) {
|
||||
return $this->filterPort($port);
|
||||
}
|
||||
|
||||
if (\is_string($port) && \ctype_digit($port)) {
|
||||
// A zero port is accepted here; only Rfc9112::parsePort() rejects
|
||||
// it for HTTP Host/authority parsing.
|
||||
if (Rfc3986::isValidPort($port)) {
|
||||
return (int) \ltrim($port, '0');
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Invalid port: %s. Must be between 0 and 65535',
|
||||
\ltrim($port, '0')
|
||||
));
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Invalid port: %s. Must be between 0 and 65535',
|
||||
self::describeInvalidPort($port)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $port
|
||||
*/
|
||||
private static function describeInvalidPort($port): string
|
||||
{
|
||||
if (\is_string($port)) {
|
||||
return DiagnosticValue::escape($port);
|
||||
}
|
||||
|
||||
if (\is_int($port)) {
|
||||
return (string) $port;
|
||||
}
|
||||
|
||||
if (\is_bool($port)) {
|
||||
return $port ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if ($port === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (\is_float($port)) {
|
||||
if (\is_nan($port)) {
|
||||
return 'NAN';
|
||||
}
|
||||
|
||||
if (\is_infinite($port)) {
|
||||
return $port > 0 ? 'INF' : '-INF';
|
||||
}
|
||||
|
||||
return \sprintf('%.14G', $port);
|
||||
}
|
||||
|
||||
return \get_debug_type($port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (string|int)[] $keys
|
||||
*
|
||||
|
|
@ -719,7 +758,7 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
return rawurldecode((string) $k);
|
||||
}, $keys);
|
||||
|
||||
return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
|
||||
return array_filter(explode('&', $current), static function (string $part) use ($decodedKeys): bool {
|
||||
return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
|
||||
});
|
||||
}
|
||||
|
|
@ -749,41 +788,44 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
/**
|
||||
* Filters the path of a URI
|
||||
*
|
||||
* @param mixed $path
|
||||
*
|
||||
* @throws \InvalidArgumentException If the path is invalid.
|
||||
*/
|
||||
private function filterPath($path): string
|
||||
private function filterPath(string $path): string
|
||||
{
|
||||
if (!is_string($path)) {
|
||||
throw new \InvalidArgumentException('Path must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$path
|
||||
return $this->filterComponent(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?!'.Rfc3986::HEX_OCTET.'))/',
|
||||
$path,
|
||||
'Unable to filter URI path'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the query string or fragment of a URI.
|
||||
*
|
||||
* @param mixed $str
|
||||
*
|
||||
* @throws \InvalidArgumentException If the query or fragment is invalid.
|
||||
*/
|
||||
private function filterQueryAndFragment($str): string
|
||||
private function filterQueryAndFragment(string $str): string
|
||||
{
|
||||
if (!is_string($str)) {
|
||||
throw new \InvalidArgumentException('Query and fragment must be a string');
|
||||
return $this->filterComponent(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?!'.Rfc3986::HEX_OCTET.'))/',
|
||||
$str,
|
||||
'Unable to filter URI query or fragment'
|
||||
);
|
||||
}
|
||||
|
||||
private function filterComponent(
|
||||
string $pattern,
|
||||
#[\SensitiveParameter]
|
||||
string $component,
|
||||
string $context
|
||||
): string {
|
||||
$filtered = preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
|
||||
|
||||
if ($filtered === null) {
|
||||
throw new \RuntimeException($context.': '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$str
|
||||
);
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
private function rawurlencodeMatchZero(array $match): string
|
||||
|
|
@ -798,10 +840,10 @@ class Uri implements UriInterface, \JsonSerializable
|
|||
}
|
||||
|
||||
if ($this->getAuthority() === '') {
|
||||
if (0 === strpos($this->path, '//')) {
|
||||
if (str_starts_with($this->path, '//')) {
|
||||
throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"');
|
||||
}
|
||||
if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
|
||||
if ($this->scheme === '' && str_contains(explode('/', $this->path, 2)[0], ':')) {
|
||||
throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
vendor/guzzlehttp/psr7/src/UriComparator.php
vendored
48
vendor/guzzlehttp/psr7/src/UriComparator.php
vendored
|
|
@ -7,19 +7,33 @@ namespace GuzzleHttp\Psr7;
|
|||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Provides methods to determine if a modified URL should be considered cross-origin.
|
||||
* Provides methods to determine if a modified URI should be considered
|
||||
* cross-origin.
|
||||
*
|
||||
* @author Graham Campbell
|
||||
*/
|
||||
final class UriComparator
|
||||
{
|
||||
/**
|
||||
* Determines if a modified URL should be considered cross-origin with
|
||||
* respect to an original URL.
|
||||
* Determines if a modified URI should be considered cross-origin with
|
||||
* respect to an original URI.
|
||||
*
|
||||
* Two URIs are cross-origin when their scheme, host, or effective port
|
||||
* differ. Host comparison is case-insensitive, and bracketed IPv6 literals
|
||||
* are canonicalized to their RFC 5952 form from any PSR-7 implementation
|
||||
* before comparison, so equivalent spellings of the same address are
|
||||
* same-origin. IPvFuture literals and bracketed values that cannot be
|
||||
* parsed as an IPv6 address, such as those carrying zone identifiers,
|
||||
* still compare as case-insensitive text. Missing ports use the default
|
||||
* port for `http`, `https`, `ws`, or `wss`. Other schemes do not receive
|
||||
* implicit default ports.
|
||||
*
|
||||
* This helper only compares URI origins. It does not implement redirect
|
||||
* handling or credential policy.
|
||||
*/
|
||||
public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
|
||||
{
|
||||
if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
|
||||
if (!Utils::caselessEquals(self::normalizeHost($original), self::normalizeHost($modified))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +48,28 @@ final class UriComparator
|
|||
return false;
|
||||
}
|
||||
|
||||
private static function normalizeHost(UriInterface $uri): string
|
||||
{
|
||||
$host = $uri->getHost();
|
||||
if (!str_starts_with($host, '[') || !str_ends_with($host, ']')) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
// Foreign UriInterface implementations may carry non-canonical IPv6
|
||||
// spellings; canonicalize what is unambiguously an IPv6 address so
|
||||
// equivalent literals compare as same-origin, and leave IPvFuture,
|
||||
// zone-identifier, and invalid text to the caseless textual
|
||||
// comparison. Validation is platform-independent, so a spelling only
|
||||
// some OS parsers accept, such as zero-padded dotted octets, is
|
||||
// cross-origin everywhere instead of same-origin on some systems.
|
||||
$canonical = Rfc3986::tryCanonicalizeIpv6(substr($host, 1, -1));
|
||||
if ($canonical === null) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
return '['.$canonical.']';
|
||||
}
|
||||
|
||||
private static function computePort(UriInterface $uri): ?int
|
||||
{
|
||||
$port = $uri->getPort();
|
||||
|
|
@ -42,11 +78,11 @@ final class UriComparator
|
|||
return $port;
|
||||
}
|
||||
|
||||
if ('http' === $uri->getScheme()) {
|
||||
if (\in_array($uri->getScheme(), ['http', 'ws'], true)) {
|
||||
return 80;
|
||||
}
|
||||
|
||||
if ('https' === $uri->getScheme()) {
|
||||
if (\in_array($uri->getScheme(), ['https', 'wss'], true)) {
|
||||
return 443;
|
||||
}
|
||||
|
||||
|
|
|
|||
272
vendor/guzzlehttp/psr7/src/UriNormalizer.php
vendored
272
vendor/guzzlehttp/psr7/src/UriNormalizer.php
vendored
|
|
@ -16,7 +16,8 @@ use Psr\Http\Message\UriInterface;
|
|||
final class UriNormalizer
|
||||
{
|
||||
/**
|
||||
* Default normalizations which only include the ones that preserve semantics.
|
||||
* Default normalizations which only include the ones that preserve
|
||||
* semantics.
|
||||
*/
|
||||
public const PRESERVING_NORMALIZATIONS =
|
||||
self::CAPITALIZE_PERCENT_ENCODING |
|
||||
|
|
@ -24,10 +25,21 @@ final class UriNormalizer
|
|||
self::CONVERT_EMPTY_PATH |
|
||||
self::REMOVE_DEFAULT_HOST |
|
||||
self::REMOVE_DEFAULT_PORT |
|
||||
self::REMOVE_DOT_SEGMENTS;
|
||||
self::REMOVE_DOT_SEGMENTS |
|
||||
self::CANONICALIZE_IPV6_HOST;
|
||||
|
||||
/**
|
||||
* All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized.
|
||||
* All letters within a percent-encoding triplet (e.g., "%3A") are
|
||||
* case-insensitive, and should be capitalized. This applies to the
|
||||
* userinfo, host, path, query, and fragment components. Bracketed
|
||||
* IP-literal hosts are skipped as a legacy tolerance for nonstandard values
|
||||
* other implementations may carry; zone-identifier text was briefly valid
|
||||
* URI syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The
|
||||
* userinfo and host are only rewritten when the value returned by the
|
||||
* implementation matches the normalized form, and a userinfo with an empty
|
||||
* user segment is never rewritten. No percent-encoding normalization is
|
||||
* applied to a component that contains malformed percent syntax, such as a
|
||||
* `%` not followed by two hexadecimal digits.
|
||||
*
|
||||
* Example: http://example.org/a%c2%b1b → http://example.org/a%C2%B1b
|
||||
*/
|
||||
|
|
@ -36,9 +48,22 @@ final class UriNormalizer
|
|||
/**
|
||||
* Decodes percent-encoded octets of unreserved characters.
|
||||
*
|
||||
* For consistency, percent-encoded octets in the ranges of ALPHA (%41–%5A and %61–%7A), DIGIT (%30–%39),
|
||||
* hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should not be created by URI producers and,
|
||||
* when found in a URI, should be decoded to their corresponding unreserved characters by URI normalizers.
|
||||
* For consistency, percent-encoded octets in the ranges of ALPHA (%41–%5A
|
||||
* and %61–%7A), DIGIT (%30–%39), hyphen (%2D), period (%2E), underscore
|
||||
* (%5F), or tilde (%7E) should not be created by URI producers and, when
|
||||
* found in a URI, should be decoded to their corresponding unreserved
|
||||
* characters by URI normalizers. This applies to the userinfo, host, path,
|
||||
* query, and fragment components. Since the host is case-insensitive and
|
||||
* PSR-7 requires it to be lowercase, octets decoded in the host are
|
||||
* lowercased (e.g., "%41" becomes "a"). Bracketed IP-literal hosts are
|
||||
* skipped as a legacy tolerance for nonstandard values other
|
||||
* implementations may carry; zone-identifier text was briefly valid URI
|
||||
* syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The
|
||||
* userinfo and host are only rewritten when the value returned by the
|
||||
* implementation matches the normalized form, and a userinfo with an empty
|
||||
* user segment is never rewritten. No percent-encoding normalization is
|
||||
* applied to a component that contains malformed percent syntax, such as a
|
||||
* `%` not followed by two hexadecimal digits.
|
||||
*
|
||||
* Example: http://example.org/%7Eusern%61me/ → http://example.org/~username/
|
||||
*/
|
||||
|
|
@ -54,11 +79,12 @@ final class UriNormalizer
|
|||
/**
|
||||
* Removes the default host of the given URI scheme from the URI.
|
||||
*
|
||||
* Only the "file" scheme defines the default host "localhost".
|
||||
* All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile`
|
||||
* are equivalent according to RFC 3986. The first format is not accepted
|
||||
* by PHPs stream functions and thus already normalized implicitly to the
|
||||
* second format in the Uri class. See `GuzzleHttp\Psr7\Uri::composeComponents`.
|
||||
* Only the "file" scheme defines the default host "localhost". All of
|
||||
* `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are
|
||||
* equivalent according to RFC 3986. The first format is not accepted by
|
||||
* PHPs stream functions and thus already normalized implicitly to the
|
||||
* second format in the Uri class. See
|
||||
* `GuzzleHttp\Psr7\Uri::composeComponents`.
|
||||
*
|
||||
* Example: file://localhost/myfile → file:///myfile
|
||||
*/
|
||||
|
|
@ -84,9 +110,10 @@ final class UriNormalizer
|
|||
/**
|
||||
* Paths which include two or more adjacent slashes are converted to one.
|
||||
*
|
||||
* Webservers usually ignore duplicate slashes and treat those URIs equivalent.
|
||||
* But in theory those URIs do not need to be equivalent. So this normalization
|
||||
* may change the semantics. Encoded slashes (%2F) are not removed.
|
||||
* Webservers usually ignore duplicate slashes and treat those URIs
|
||||
* equivalent. But in theory those URIs do not need to be equivalent. So
|
||||
* this normalization may change the semantics. Encoded slashes (%2F) are
|
||||
* not removed.
|
||||
*
|
||||
* Example: http://example.org//foo///bar.html → http://example.org/foo/bar.html
|
||||
*/
|
||||
|
|
@ -95,26 +122,49 @@ final class UriNormalizer
|
|||
/**
|
||||
* Sort query parameters with their values in alphabetical order.
|
||||
*
|
||||
* However, the order of parameters in a URI may be significant (this is not defined by the standard).
|
||||
* So this normalization is not safe and may change the semantics of the URI.
|
||||
* However, the order of parameters in a URI may be significant (this is not
|
||||
* defined by the standard). So this normalization is not safe and may
|
||||
* change the semantics of the URI.
|
||||
*
|
||||
* Example: ?lang=en&article=fred → ?article=fred&lang=en
|
||||
*
|
||||
* Note: The sorting is neither locale nor Unicode aware (the URI query does not get decoded at all) as the
|
||||
* purpose is to be able to compare URIs in a reproducible way, not to have the params sorted perfectly.
|
||||
* Note: The sorting is neither locale nor Unicode aware (the URI query does
|
||||
* not get decoded at all) as the purpose is to be able to compare URIs in a
|
||||
* reproducible way, not to have the params sorted perfectly.
|
||||
*/
|
||||
public const SORT_QUERY_PARAMETERS = 128;
|
||||
|
||||
/**
|
||||
* Canonicalizes IPv6 hosts to their RFC 5952 form.
|
||||
*
|
||||
* IPv6 addresses allow leading zeros and multiple placements of the `::`
|
||||
* elision, so the same address has many textual spellings. The canonical
|
||||
* form is required for IPv6 literals in URIs by RFC 5952 Section 6 and
|
||||
* never changes what the URI refers to. Native `Uri` instances already
|
||||
* guarantee canonical output; for other implementations, the canonical
|
||||
* host is requested through `withHost()` and the result is kept only when
|
||||
* the returned `getHost()` exactly matches the requested spelling,
|
||||
* otherwise this step leaves the URI unchanged while other selected
|
||||
* normalizations still apply, and setter exceptions propagate.
|
||||
*
|
||||
* Example: http://[::0:0a]/ → http://[::a]/
|
||||
*/
|
||||
public const CANONICALIZE_IPV6_HOST = 256;
|
||||
|
||||
/**
|
||||
* Returns a normalized URI.
|
||||
*
|
||||
* The scheme and host component are already normalized to lowercase per PSR-7 UriInterface.
|
||||
* This methods adds additional normalizations that can be configured with the $flags parameter.
|
||||
* The scheme and host component are already normalized to lowercase per
|
||||
* PSR-7 UriInterface. This method adds additional normalizations that can
|
||||
* be configured with the `$flags` parameter, which is a bitmask of
|
||||
* normalizations to apply.
|
||||
*
|
||||
* PSR-7 UriInterface cannot distinguish between an empty component and a missing component as
|
||||
* getQuery(), getFragment() etc. always return a string. This 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.
|
||||
* PSR-7 UriInterface cannot distinguish between an empty component and a
|
||||
* missing component as `getQuery()`, `getFragment()` etc. always return a
|
||||
* string. This 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.
|
||||
*
|
||||
* @param UriInterface $uri The URI to normalize
|
||||
* @param int $flags A bitmask of normalizations to apply, see constants
|
||||
|
|
@ -145,12 +195,24 @@ final class UriNormalizer
|
|||
$uri = $uri->withPort(null);
|
||||
}
|
||||
|
||||
if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) {
|
||||
$uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath()));
|
||||
}
|
||||
$removeDotSegments = ($flags & self::REMOVE_DOT_SEGMENTS) && !Uri::isRelativePathReference($uri);
|
||||
|
||||
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
|
||||
$uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath()));
|
||||
if ($removeDotSegments || $flags & self::REMOVE_DUPLICATE_SLASHES) {
|
||||
$path = Uri::rawPath($uri);
|
||||
|
||||
if ($removeDotSegments) {
|
||||
$path = UriResolver::removeDotSegments($path);
|
||||
}
|
||||
|
||||
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
|
||||
$path = preg_replace('#//++#', '/', $path);
|
||||
|
||||
if ($path === null) {
|
||||
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
$uri = $uri->withPath(UriResolver::guardedPath($uri, $path));
|
||||
}
|
||||
|
||||
if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
|
||||
|
|
@ -159,16 +221,22 @@ final class UriNormalizer
|
|||
$uri = $uri->withQuery(implode('&', $queryKeyValues));
|
||||
}
|
||||
|
||||
if ($flags & self::CANONICALIZE_IPV6_HOST) {
|
||||
$uri = self::canonicalizeIpv6Host($uri);
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether two URIs can be considered equivalent.
|
||||
*
|
||||
* Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also
|
||||
* accepts relative URI references and returns true when they are equivalent. This of course assumes they will be
|
||||
* resolved against the same base URI. If this is not the case, determination of equivalence or difference of
|
||||
* relative references does not mean anything.
|
||||
* Both URIs are normalized automatically before comparison with the given
|
||||
* `$normalizations` bitmask. The method also accepts relative URI
|
||||
* references and returns true when they are equivalent. This of course
|
||||
* assumes they will be resolved against the same base URI. If this is not
|
||||
* the case, determination of equivalence or difference of relative
|
||||
* references does not mean anything.
|
||||
*
|
||||
* @param UriInterface $uri1 An URI to compare
|
||||
* @param UriInterface $uri2 An URI to compare
|
||||
|
|
@ -183,14 +251,17 @@ final class UriNormalizer
|
|||
|
||||
private static function capitalizePercentEncoding(UriInterface $uri): UriInterface
|
||||
{
|
||||
$regex = '/(?:%[A-Fa-f0-9]{2})++/';
|
||||
$regex = '/(?:%'.Rfc3986::HEX_OCTET.')++/';
|
||||
|
||||
$callback = function (array $match): string {
|
||||
return strtoupper($match[0]);
|
||||
return Utils::asciiToUpper($match[0]);
|
||||
};
|
||||
|
||||
$uri = self::withNormalizedUserInfo($uri, $regex, $callback);
|
||||
$uri = self::withNormalizedHost($uri, $regex, $callback);
|
||||
|
||||
return $uri
|
||||
->withPath(self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
|
||||
->withPath(self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
|
||||
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
|
||||
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
|
||||
}
|
||||
|
|
@ -203,26 +274,151 @@ final class UriNormalizer
|
|||
return rawurldecode($match[0]);
|
||||
};
|
||||
|
||||
// The host is case-insensitive and PSR-7 requires it to be lowercase,
|
||||
// so decoded ALPHA octets (e.g. "%41") must land lowercase even for
|
||||
// implementations whose withHost() does not normalize the case.
|
||||
$hostCallback = function (array $match): string {
|
||||
return Utils::asciiToLower(rawurldecode($match[0]));
|
||||
};
|
||||
|
||||
$uri = self::withNormalizedUserInfo($uri, $regex, $callback);
|
||||
$uri = self::withNormalizedHost($uri, $regex, $hostCallback);
|
||||
|
||||
return $uri
|
||||
->withPath(self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
|
||||
->withPath(self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))
|
||||
->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
|
||||
->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(array): string $callback
|
||||
*/
|
||||
private static function withNormalizedUserInfo(UriInterface $uri, string $regex, callable $callback): UriInterface
|
||||
{
|
||||
$userInfo = $uri->getUserInfo();
|
||||
|
||||
if (!str_contains($userInfo, '%')) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$normalized = self::normalizePercentEncodingInComponent($userInfo, $regex, $callback);
|
||||
|
||||
if ($normalized === $userInfo) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
// Normalization cannot create a colon: decoding is confined to
|
||||
// unreserved characters and capitalization keeps octets encoded. So
|
||||
// splitting on the first colon preserves the user/password boundary.
|
||||
$parts = explode(':', $normalized, 2);
|
||||
|
||||
// PSR-7 defines withUserInfo('') as removing the userinfo, so a
|
||||
// userinfo with an empty user segment (e.g. ":pass") cannot be
|
||||
// expressed through the setter and is preserved as-is instead.
|
||||
if ($parts[0] === '') {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$candidate = $uri->withUserInfo($parts[0], $parts[1] ?? null);
|
||||
|
||||
// Normalization must never lose or corrupt information, so verify the
|
||||
// representation the setter returned and leave the component untouched
|
||||
// when the implementation cannot represent the normalized form.
|
||||
if ($candidate->getUserInfo() !== $normalized) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(array): string $callback
|
||||
*/
|
||||
private static function withNormalizedHost(UriInterface $uri, string $regex, callable $callback): UriInterface
|
||||
{
|
||||
$host = $uri->getHost();
|
||||
|
||||
// Bracketed IP-literal hosts are skipped as a legacy tolerance for
|
||||
// nonstandard values other implementations may carry, such as a zone
|
||||
// identifier in "[fe80::1%25eth0]"; that text was briefly valid URI
|
||||
// syntax under RFC 6874, which RFC 9844 obsoleted and reverted.
|
||||
if (str_starts_with($host, '[') || !str_contains($host, '%')) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$normalized = self::normalizePercentEncodingInComponent($host, $regex, $callback);
|
||||
|
||||
if ($normalized === $host) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$candidate = $uri->withHost($normalized);
|
||||
|
||||
// Normalization must never lose or corrupt information, so verify the
|
||||
// representation the setter returned and leave the component untouched
|
||||
// when the implementation cannot represent the normalized form.
|
||||
if ($candidate->getHost() !== $normalized) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(array): string $callback
|
||||
*/
|
||||
private static function normalizePercentEncodingInComponent(string $component, string $regex, callable $callback): string
|
||||
{
|
||||
// Decoding a valid triplet that follows a dangling "%" would complete
|
||||
// the malformed sequence into a new valid triplet ("example%6%31com"
|
||||
// becomes "example%61com"), turning malformed text valid and breaking
|
||||
// idempotence, so a component containing malformed percent syntax is
|
||||
// returned unchanged.
|
||||
$malformed = preg_match('/%(?!'.Rfc3986::HEX_OCTET.')/', $component);
|
||||
|
||||
if ($malformed === false) {
|
||||
throw new \RuntimeException('Unable to scan URI component percent-encoding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($malformed === 1) {
|
||||
return $component;
|
||||
}
|
||||
|
||||
$normalized = preg_replace_callback($regex, $callback, $component);
|
||||
|
||||
if ($normalized === null) {
|
||||
throw new \RuntimeException('Unable to normalize URI component percent-encoding');
|
||||
throw new \RuntimeException('Unable to normalize URI component percent-encoding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private static function canonicalizeIpv6Host(UriInterface $uri): UriInterface
|
||||
{
|
||||
$host = $uri->getHost();
|
||||
if (!str_starts_with($host, '[') || !str_ends_with($host, ']')) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
// Foreign UriInterface implementations may carry IPvFuture literals,
|
||||
// IPv6 zone identifiers, uppercase text, or invalid spellings;
|
||||
// tryCanonicalizeIpv6() canonicalizes only what is unambiguously an
|
||||
// IPv6 address and leaves everything else untouched.
|
||||
$canonical = Rfc3986::tryCanonicalizeIpv6(substr($host, 1, -1));
|
||||
if ($canonical === null || '['.$canonical.']' === $host) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$candidate = $uri->withHost('['.$canonical.']');
|
||||
// Normalization must never corrupt a component, so keep the original
|
||||
// host when the implementation does not retain the canonical form.
|
||||
if ($candidate->getHost() !== '['.$canonical.']') {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
// cannot be instantiated
|
||||
|
|
|
|||
145
vendor/guzzlehttp/psr7/src/UriParser.php
vendored
Normal file
145
vendor/guzzlehttp/psr7/src/UriParser.php
vendored
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class UriParser
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 aware \parse_url() replacement.
|
||||
*
|
||||
* The internal function produces broken output for non ASCII domain names
|
||||
* (IDN) when used with locales other than "C".
|
||||
*
|
||||
* On the other hand, cURL understands IDN correctly only when UTF-8 locale
|
||||
* is configured ("C.UTF-8", "en_US.UTF-8", etc.).
|
||||
*
|
||||
* @see https://bugs.php.net/bug.php?id=52923
|
||||
* @see https://www.php.net/manual/en/function.parse-url.php#114817
|
||||
* @see https://curl.se/libcurl/c/CURLOPT_URL.html#ENCODING
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public static function parse(string $url)
|
||||
{
|
||||
if (self::isPathNoSchemeReference($url)) {
|
||||
return self::parsePathNoSchemeReference($url);
|
||||
}
|
||||
|
||||
// Preserve bracketed IP-literals (IPv6 or IPvFuture) in scheme, userinfo,
|
||||
// and network-path authorities before encoding. Userinfo is encoded
|
||||
// separately so raw bytes cannot reach parse_url(), which mutates
|
||||
// control characters instead of failing.
|
||||
$prefix = '';
|
||||
$ipv6Prefix = preg_match('%\A((?:[0-9A-Za-z+.-]+:)?//)(?:([^/?#@]*)(@))?(\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
|
||||
|
||||
if ($ipv6Prefix === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ipv6Prefix === 1) {
|
||||
/** @var array{0:string, 1:string, 2:string, 3:string, 4:string, 5:string} $matches */
|
||||
$suffix = $matches[5];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// RFC 3986 IP-literals contain no percent-encoding, so reject any
|
||||
// "%" in the bracketed host rather than letting the urldecode()
|
||||
// below turn an encoded octet into a different literal. This keeps
|
||||
// parsing aligned with withHost()/Rfc3986::isValidHost().
|
||||
if (str_contains($matches[4], '%')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = $matches[1];
|
||||
|
||||
if ($matches[3] === '@') {
|
||||
/** @var string|null */
|
||||
$encodedUserInfo = preg_replace_callback(
|
||||
'%[^:/@?&=#]+%usD',
|
||||
static function (array $matches): string {
|
||||
return urlencode($matches[0]);
|
||||
},
|
||||
$matches[2]
|
||||
);
|
||||
|
||||
if ($encodedUserInfo === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix .= $encodedUserInfo.'@';
|
||||
}
|
||||
|
||||
$prefix .= $matches[4];
|
||||
$url = $suffix;
|
||||
}
|
||||
|
||||
/** @var string|null */
|
||||
$encodedUrl = preg_replace_callback(
|
||||
'%[^:/@?&=#]+%usD',
|
||||
static function (array $matches): string {
|
||||
return urlencode($matches[0]);
|
||||
},
|
||||
$url
|
||||
);
|
||||
|
||||
if ($encodedUrl === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = parse_url($prefix.$encodedUrl);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_map('urldecode', $result);
|
||||
}
|
||||
|
||||
private static function isPathNoSchemeReference(string $url): bool
|
||||
{
|
||||
if ($url === '' || str_starts_with($url, '/') || str_starts_with($url, '?') || str_starts_with($url, '#')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstSegment = substr($url, 0, strcspn($url, '/?#'));
|
||||
|
||||
return !str_contains($firstSegment, ':');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{path: string, query?: string, fragment?: string}
|
||||
*/
|
||||
private static function parsePathNoSchemeReference(string $url): array
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (false !== ($fragmentPosition = strpos($url, '#'))) {
|
||||
$parts['fragment'] = substr($url, $fragmentPosition + 1);
|
||||
$url = substr($url, 0, $fragmentPosition);
|
||||
}
|
||||
|
||||
if (false !== ($queryPosition = strpos($url, '?'))) {
|
||||
$parts['query'] = substr($url, $queryPosition + 1);
|
||||
$url = substr($url, 0, $queryPosition);
|
||||
}
|
||||
|
||||
$parts['path'] = $url;
|
||||
|
||||
return $parts;
|
||||
}
|
||||
}
|
||||
161
vendor/guzzlehttp/psr7/src/UriResolver.php
vendored
161
vendor/guzzlehttp/psr7/src/UriResolver.php
vendored
|
|
@ -16,7 +16,15 @@ use Psr\Http\Message\UriInterface;
|
|||
final class UriResolver
|
||||
{
|
||||
/**
|
||||
* Removes dot segments from a path and returns the new path.
|
||||
* Removes dot segments from a path and returns the new path according to
|
||||
* RFC 3986 Section 5.2.4.
|
||||
*
|
||||
* Excess `..` segments above the root of an absolute path are dropped
|
||||
* without consuming the root, so the result can begin with `//` (e.g.
|
||||
* `/..//a` becomes `//a`). Such a path is not valid for a URI without an
|
||||
* authority (RFC 3986 Section 3.3); `resolve()` and
|
||||
* `UriNormalizer::normalize()` serialize it with a `/.` prefix in that
|
||||
* case, like the WHATWG URL Standard.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
|
||||
*/
|
||||
|
|
@ -28,9 +36,15 @@ final class UriResolver
|
|||
|
||||
$results = [];
|
||||
$segments = explode('/', $path);
|
||||
// The first segment of an absolute path is the empty root marker producing the
|
||||
// leading slash. RFC 3986 Section 5.2.4 (2C) drops ".." segments in excess of
|
||||
// the path hierarchy without consuming the root, so it must never be popped.
|
||||
$floor = $segments[0] === '' ? 1 : 0;
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment === '..') {
|
||||
array_pop($results);
|
||||
if (count($results) > $floor) {
|
||||
array_pop($results);
|
||||
}
|
||||
} elseif ($segment !== '.') {
|
||||
$results[] = $segment;
|
||||
}
|
||||
|
|
@ -38,7 +52,7 @@ final class UriResolver
|
|||
|
||||
$newPath = implode('/', $results);
|
||||
|
||||
if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
|
||||
if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) {
|
||||
// Re-add the leading slash if necessary for cases like "/.."
|
||||
$newPath = '/'.$newPath;
|
||||
} elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
|
||||
|
|
@ -51,7 +65,37 @@ final class UriResolver
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts the relative URI into a new URI that is resolved against the base URI.
|
||||
* Returns the path, prefixed with "/." when it would otherwise start the
|
||||
* URI's string form with an authority-like "//".
|
||||
*
|
||||
* A URI without an authority cannot hold a path beginning with "//" (RFC
|
||||
* 3986 Section 3.3), but removeDotSegments() can produce one. The "/."
|
||||
* prefix serializes such a path unambiguously, the same way the WHATWG URL
|
||||
* Standard does, and resolves back to the same path. Hostless http and
|
||||
* https Uri instances gain the default localhost host when the path is
|
||||
* written, so the path cannot be mistaken for an authority and the prefix
|
||||
* is not added.
|
||||
*
|
||||
* @see https://url.spec.whatwg.org/#url-serializing
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function guardedPath(UriInterface $uri, string $path): string
|
||||
{
|
||||
if (!str_starts_with($path, '//') || $uri->getAuthority() !== '') {
|
||||
return $path;
|
||||
}
|
||||
|
||||
if ($uri instanceof Uri && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https')) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return '/.'.$path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the relative URI into a new URI that is resolved against the
|
||||
* base URI.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
|
||||
*/
|
||||
|
|
@ -63,61 +107,69 @@ final class UriResolver
|
|||
}
|
||||
|
||||
if ($rel->getScheme() != '') {
|
||||
return $rel->withPath(self::removeDotSegments($rel->getPath()));
|
||||
return $rel->withPath(self::guardedPath($rel, self::removeDotSegments(Uri::rawPath($rel))));
|
||||
}
|
||||
|
||||
if ($rel->getAuthority() != '') {
|
||||
return $rel
|
||||
->withScheme($base->getScheme())
|
||||
->withPath(self::removeDotSegments($rel->getPath()));
|
||||
->withPath(self::removeDotSegments(Uri::rawPath($rel)));
|
||||
}
|
||||
|
||||
if ($rel->getPath() === '') {
|
||||
$targetPath = $base->getPath();
|
||||
$targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
|
||||
$relPath = Uri::rawPath($rel);
|
||||
|
||||
if ($relPath === '') {
|
||||
// the base path is used as-is per RFC 3986 Section 5.2.2, so it must not be
|
||||
// rewritten through a getPath()/withPath() round-trip
|
||||
return $base
|
||||
->withQuery($rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery())
|
||||
->withFragment($rel->getFragment());
|
||||
}
|
||||
|
||||
if (str_starts_with($relPath, '/')) {
|
||||
$targetPath = $relPath;
|
||||
} else {
|
||||
if ($rel->getPath()[0] === '/') {
|
||||
$targetPath = $rel->getPath();
|
||||
$basePath = Uri::rawPath($base);
|
||||
if ($base->getAuthority() != '' && $basePath === '') {
|
||||
$targetPath = '/'.$relPath;
|
||||
} else {
|
||||
if ($base->getAuthority() != '' && $base->getPath() === '') {
|
||||
$targetPath = '/'.$rel->getPath();
|
||||
$lastSlashPos = strrpos($basePath, '/');
|
||||
if ($lastSlashPos === false) {
|
||||
$targetPath = $relPath;
|
||||
} else {
|
||||
$lastSlashPos = strrpos($base->getPath(), '/');
|
||||
if ($lastSlashPos === false) {
|
||||
$targetPath = $rel->getPath();
|
||||
} else {
|
||||
$targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
|
||||
}
|
||||
$targetPath = substr($basePath, 0, $lastSlashPos + 1).$relPath;
|
||||
}
|
||||
}
|
||||
$targetPath = self::removeDotSegments($targetPath);
|
||||
$targetQuery = $rel->getQuery();
|
||||
}
|
||||
$targetPath = self::removeDotSegments($targetPath);
|
||||
|
||||
return $base
|
||||
->withPath($targetPath)
|
||||
->withQuery($targetQuery)
|
||||
->withPath(self::guardedPath($base, $targetPath))
|
||||
->withQuery($rel->getQuery())
|
||||
->withFragment($rel->getFragment());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target URI as a relative reference from the base URI.
|
||||
*
|
||||
* This method is the counterpart to resolve():
|
||||
* This method is the counterpart to `resolve()`:
|
||||
*
|
||||
* (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
|
||||
*
|
||||
* One use-case is to use the current request URI as base URI and then generate relative links in your documents
|
||||
* to reduce the document size or offer self-contained downloadable document archives.
|
||||
* One use case is to use the current request URI as the base URI and then
|
||||
* generate relative links in your documents to reduce the document size or
|
||||
* offer self-contained downloadable document archives.
|
||||
*
|
||||
* $base = new Uri('http://example.com/a/b/');
|
||||
* echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
|
||||
* echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
|
||||
* echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
|
||||
* echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
|
||||
* echo UriResolver::relativize($base, new Uri('http://example.com')); // prints '//example.com'.
|
||||
*
|
||||
* This method also accepts a target that is already relative and will try to relativize it further. Only a
|
||||
* relative-path reference will be returned as-is.
|
||||
* This method also accepts a target that is already relative and will try
|
||||
* to relativize it further. Only a relative-path reference will be returned
|
||||
* as-is.
|
||||
*
|
||||
* echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
|
||||
*/
|
||||
|
|
@ -140,37 +192,68 @@ final class UriResolver
|
|||
return $target->withScheme('');
|
||||
}
|
||||
|
||||
// A same-authority target with an empty path can only be expressed by a
|
||||
// network-path reference (RFC 3986 Section 5.2.2).
|
||||
if (self::needsNetworkPathReference($base, $target)) {
|
||||
return $target->withScheme('');
|
||||
}
|
||||
|
||||
// We must remove the path before removing the authority because if the path starts with two slashes, the URI
|
||||
// would turn invalid. And we also cannot set a relative path before removing the authority, as that is also
|
||||
// invalid.
|
||||
$emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
|
||||
|
||||
if ($base->getPath() !== $target->getPath()) {
|
||||
if (Uri::rawPath($base) !== Uri::rawPath($target)) {
|
||||
return $emptyPathUri->withPath(self::getRelativePath($base, $target));
|
||||
}
|
||||
|
||||
if ($base->getQuery() === $target->getQuery()) {
|
||||
if ($base->getQuery() === $target->getQuery() && ($target->getFragment() !== '' || $base->getFragment() === '')) {
|
||||
// Only the target fragment is left. And it must be returned even if base and target fragment are the same.
|
||||
return $emptyPathUri->withQuery('');
|
||||
}
|
||||
|
||||
// If the base URI has a query but the target has none, we cannot return an empty path reference as it would
|
||||
// inherit the base query component when resolving.
|
||||
// If the base URI has a query or fragment that the target lacks, we cannot return an empty path
|
||||
// reference as it would inherit that base component when resolving.
|
||||
if ($target->getQuery() === '') {
|
||||
$segments = explode('/', $target->getPath());
|
||||
$segments = explode('/', Uri::rawPath($target));
|
||||
/** @var string $lastSegment */
|
||||
$lastSegment = end($segments);
|
||||
|
||||
return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment);
|
||||
// A reference to an empty last segment must be prefixed with "./". The same applies
|
||||
// to a segment with a colon character, which would be mistaken for a scheme name.
|
||||
if ($lastSegment === '' || str_contains($lastSegment, ':')) {
|
||||
$lastSegment = "./$lastSegment";
|
||||
}
|
||||
|
||||
return $emptyPathUri->withPath($lastSegment);
|
||||
}
|
||||
|
||||
return $emptyPathUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether relativizing to $target requires a network-path reference.
|
||||
*
|
||||
* A same-authority target with an empty path is expressible by a shorter
|
||||
* relative reference unless resolving one would inherit a base component
|
||||
* the target lacks: the base path (kept by any empty-path reference), or
|
||||
* the base query or fragment (inherited by the empty reference).
|
||||
*/
|
||||
private static function needsNetworkPathReference(UriInterface $base, UriInterface $target): bool
|
||||
{
|
||||
if ($target->getAuthority() === '' || Uri::rawPath($target) !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Uri::rawPath($base) !== ''
|
||||
|| ($base->getQuery() !== '' && $target->getQuery() === '')
|
||||
|| ($base->getFragment() !== '' && $target->getFragment() === '' && $base->getQuery() === $target->getQuery());
|
||||
}
|
||||
|
||||
private static function getRelativePath(UriInterface $base, UriInterface $target): string
|
||||
{
|
||||
$sourceSegments = explode('/', $base->getPath());
|
||||
$targetSegments = explode('/', $target->getPath());
|
||||
$sourceSegments = explode('/', Uri::rawPath($base));
|
||||
$targetSegments = explode('/', Uri::rawPath($target));
|
||||
array_pop($sourceSegments);
|
||||
$targetLastSegment = array_pop($targetSegments);
|
||||
foreach ($sourceSegments as $i => $segment) {
|
||||
|
|
@ -186,10 +269,10 @@ final class UriResolver
|
|||
// A reference to am empty last segment or an empty first sub-segment must be prefixed with "./".
|
||||
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
|
||||
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name.
|
||||
if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
|
||||
if ($relativePath === '' || str_contains(explode('/', $relativePath, 2)[0], ':')) {
|
||||
$relativePath = "./$relativePath";
|
||||
} elseif ('/' === $relativePath[0]) {
|
||||
if ($base->getAuthority() != '' && $base->getPath() === '') {
|
||||
} elseif (str_starts_with($relativePath, '/')) {
|
||||
if ($base->getAuthority() != '' && Uri::rawPath($base) === '') {
|
||||
// In this case an extra slash is added by resolve() automatically. So we must not add one here.
|
||||
$relativePath = ".$relativePath";
|
||||
} else {
|
||||
|
|
|
|||
447
vendor/guzzlehttp/psr7/src/Utils.php
vendored
447
vendor/guzzlehttp/psr7/src/Utils.php
vendored
|
|
@ -4,27 +4,91 @@ declare(strict_types=1);
|
|||
|
||||
namespace GuzzleHttp\Psr7;
|
||||
|
||||
use GuzzleHttp\Psr7\Exception\TimeoutException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
final class Utils
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the items given by the keys, case insensitively from the data.
|
||||
* Converts ASCII uppercase letters in a string to lowercase.
|
||||
*
|
||||
* @param (string|int)[] $keys
|
||||
* 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 from the data, case-insensitively.
|
||||
*
|
||||
* @param array<array-key, string|int> $keys
|
||||
*/
|
||||
public static function caselessRemove(array $keys, array $data): array
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
@ -34,12 +98,18 @@ final class Utils
|
|||
|
||||
/**
|
||||
* Copy the contents of a stream into another stream until the given number
|
||||
* of bytes have been read.
|
||||
* of bytes have been read, returning the number of bytes copied as an
|
||||
* `int`. On 32-bit PHP, an unbounded copy larger than `PHP_INT_MAX` bytes
|
||||
* cannot be represented by that return type. 64-bit PHP is not affected.
|
||||
*
|
||||
* The copy stops if the destination write returns 0, for example a
|
||||
* BufferStream at its high water mark or a full DroppingStream. For a
|
||||
* guaranteed full copy use a normal writable stream such as a file or
|
||||
* php://temp stream.
|
||||
* The destination must accept writes that make positive progress. Streams
|
||||
* that return 0 as a backpressure or drop signal (a `BufferStream` at its
|
||||
* high water mark, or a full `DroppingStream`) will cause this method to
|
||||
* throw. For full copies, use a normal writable stream such as a file or
|
||||
* `php://temp` stream.
|
||||
*
|
||||
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
|
||||
* after a source read or destination write cannot make progress.
|
||||
*
|
||||
* @param StreamInterface $source Stream to read from
|
||||
* @param StreamInterface $dest Stream to write to
|
||||
|
|
@ -48,63 +118,71 @@ final class Utils
|
|||
*
|
||||
* @throws \RuntimeException on error.
|
||||
*/
|
||||
public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void
|
||||
public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): int
|
||||
{
|
||||
$bufferSize = 8192;
|
||||
$copied = 0;
|
||||
|
||||
if ($maxLen === -1) {
|
||||
while (!$source->eof()) {
|
||||
$buf = $source->read($bufferSize);
|
||||
$buf = StreamTimeout::read($source, $bufferSize, 'Unable to read from stream: timed out');
|
||||
if ($buf === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!self::writeAll($dest, $buf)) {
|
||||
break;
|
||||
}
|
||||
self::writeAll($dest, $buf);
|
||||
$copied = Integers::add($copied, strlen($buf));
|
||||
}
|
||||
} else {
|
||||
$remaining = $maxLen;
|
||||
while ($remaining > 0 && !$source->eof()) {
|
||||
$buf = $source->read(min($bufferSize, $remaining));
|
||||
$buf = StreamTimeout::read($source, min($bufferSize, $remaining), 'Unable to read from stream: timed out');
|
||||
$len = strlen($buf);
|
||||
if (!$len) {
|
||||
break;
|
||||
}
|
||||
$remaining -= $len;
|
||||
if (!self::writeAll($dest, $buf)) {
|
||||
break;
|
||||
}
|
||||
self::writeAll($dest, $buf);
|
||||
$copied = Integers::add($copied, $len);
|
||||
}
|
||||
}
|
||||
|
||||
return $copied;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the full buffer to the destination, retrying short writes.
|
||||
*
|
||||
* Returns false when the destination write returns 0 or less.
|
||||
*/
|
||||
private static function writeAll(StreamInterface $dest, string $buf): bool
|
||||
private static function writeAll(StreamInterface $dest, string $buf): void
|
||||
{
|
||||
$written = 0;
|
||||
$len = strlen($buf);
|
||||
|
||||
while ($written < $len) {
|
||||
$result = $dest->write(substr($buf, $written));
|
||||
try {
|
||||
$result = $dest->write(substr($buf, $written));
|
||||
} catch (TimeoutException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
StreamTimeout::throwIfWriteTimedOut($dest, $e);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($result <= 0) {
|
||||
return false;
|
||||
StreamTimeout::throwIfWriteTimedOut($dest);
|
||||
|
||||
throw new \RuntimeException('Unable to write to stream');
|
||||
}
|
||||
|
||||
$written += $result;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the contents of a stream into a string until the given number of
|
||||
* bytes have been read.
|
||||
*
|
||||
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
|
||||
* after a stream read cannot make progress.
|
||||
*
|
||||
* @param StreamInterface $stream Stream to read
|
||||
* @param int $maxLen Maximum number of bytes to read. Pass -1
|
||||
* to read the entire stream.
|
||||
|
|
@ -117,7 +195,7 @@ final class Utils
|
|||
|
||||
if ($maxLen === -1) {
|
||||
while (!$stream->eof()) {
|
||||
$buf = $stream->read(1048576);
|
||||
$buf = StreamTimeout::read($stream, 1048576, 'Unable to read from stream: timed out');
|
||||
if ($buf === '') {
|
||||
break;
|
||||
}
|
||||
|
|
@ -129,7 +207,7 @@ final class Utils
|
|||
|
||||
$len = 0;
|
||||
while (!$stream->eof() && $len < $maxLen) {
|
||||
$buf = $stream->read($maxLen - $len);
|
||||
$buf = StreamTimeout::read($stream, $maxLen - $len, 'Unable to read from stream: timed out');
|
||||
if ($buf === '') {
|
||||
break;
|
||||
}
|
||||
|
|
@ -143,8 +221,11 @@ final class Utils
|
|||
/**
|
||||
* Calculate a hash of a stream.
|
||||
*
|
||||
* This method reads the entire stream to calculate a rolling hash, based
|
||||
* on PHP's `hash_init` functions.
|
||||
* This method reads the entire stream to calculate a rolling hash, based on
|
||||
* PHP's `hash_init` functions.
|
||||
*
|
||||
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
|
||||
* after a stream read cannot make progress.
|
||||
*
|
||||
* @param StreamInterface $stream Stream to calculate the hash for
|
||||
* @param string $algo Hash algorithm (e.g. md5, crc32, etc)
|
||||
|
|
@ -162,7 +243,12 @@ final class Utils
|
|||
|
||||
$ctx = hash_init($algo);
|
||||
while (!$stream->eof()) {
|
||||
hash_update($ctx, $stream->read(1048576));
|
||||
$buf = StreamTimeout::read($stream, 1048576, 'Unable to calculate stream hash: timed out');
|
||||
if ($buf === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
hash_update($ctx, $buf);
|
||||
}
|
||||
|
||||
$out = hash_final($ctx, $rawOutput);
|
||||
|
|
@ -183,16 +269,27 @@ final class Utils
|
|||
* or non-empty arrays of strings.
|
||||
* - remove_headers: (array) Remove the given headers. Values may be
|
||||
* strings or integers.
|
||||
* - body: (mixed) Sets the given body. Present non-null values are converted
|
||||
* with self::streamFor(), including scalar values, resources, streams,
|
||||
* iterators, callable arrays, closures, invokable objects, and objects
|
||||
* with __toString(). String inputs remain literal bodies.
|
||||
* - uri: (UriInterface) Set the URI.
|
||||
* - body: (mixed) Sets the given body. Present non-null values are
|
||||
* converted with self::streamFor(), including resources, streams,
|
||||
* iterators, callable arrays, closures, invokable objects, and stringable
|
||||
* objects. String inputs remain literal bodies.
|
||||
* - uri: (UriInterface) Set the URI. When the URI contains a host, the
|
||||
* Host header is updated from it, and combining this with an explicit
|
||||
* Host entry in set_headers throws an InvalidArgumentException. Apply
|
||||
* an intentional Host override separately with withHeader() afterwards.
|
||||
* - query: (string) Set the query string value of the URI.
|
||||
* - version: (string) Set the protocol version.
|
||||
*
|
||||
* @param RequestInterface $request Request to clone and modify.
|
||||
* @param array $changes Changes to apply.
|
||||
* @param array{
|
||||
* method?: string,
|
||||
* set_headers?: array<array-key, string|non-empty-array<array-key, string>>,
|
||||
* remove_headers?: array<array-key, string|int>,
|
||||
* body?: resource|string|StreamInterface|callable|\Iterator|\Stringable,
|
||||
* uri?: UriInterface,
|
||||
* query?: string,
|
||||
* version?: string
|
||||
* } $changes Changes to apply.
|
||||
*/
|
||||
public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface
|
||||
{
|
||||
|
|
@ -200,19 +297,23 @@ final class Utils
|
|||
return $request;
|
||||
}
|
||||
|
||||
self::warnOnInvalidModifyRequestChanges($changes);
|
||||
self::assertValidModifyRequestChanges($changes);
|
||||
|
||||
$headers = $request->getHeaders();
|
||||
|
||||
if (!isset($changes['uri'])) {
|
||||
$uri = $request->getUri();
|
||||
} else {
|
||||
// Remove the host header if one is on the URI
|
||||
$host = $changes['uri']->getHost();
|
||||
/** @var UriInterface */
|
||||
$uri = $changes['uri'];
|
||||
|
||||
$host = $uri->getHost();
|
||||
if ($host !== '') {
|
||||
Uri::assertValidHost($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.'
|
||||
);
|
||||
|
|
@ -222,15 +323,15 @@ final class Utils
|
|||
|
||||
$changes['set_headers']['Host'] = $host;
|
||||
|
||||
if ($port = $changes['uri']->getPort()) {
|
||||
$port = $uri->getPort();
|
||||
if ($port !== null) {
|
||||
$standardPorts = ['http' => 80, 'https' => 443];
|
||||
$scheme = $changes['uri']->getScheme();
|
||||
if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
|
||||
$scheme = $uri->getScheme();
|
||||
if (!isset($standardPorts[$scheme]) || $port != $standardPorts[$scheme]) {
|
||||
$changes['set_headers']['Host'] .= ':'.$port;
|
||||
}
|
||||
}
|
||||
}
|
||||
$uri = $changes['uri'];
|
||||
}
|
||||
|
||||
if (!empty($changes['remove_headers'])) {
|
||||
|
|
@ -248,7 +349,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;
|
||||
}
|
||||
|
|
@ -257,6 +358,7 @@ final class Utils
|
|||
// Match Request::__construct() by adding a Host header when one is not provided.
|
||||
if (!$hasHost && $uri->getHost() !== '') {
|
||||
$host = $uri->getHost();
|
||||
Uri::assertValidHost($host);
|
||||
|
||||
if (($port = $uri->getPort()) !== null) {
|
||||
$host .= ':'.$port;
|
||||
|
|
@ -284,7 +386,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 */
|
||||
|
|
@ -313,45 +415,45 @@ final class Utils
|
|||
/**
|
||||
* @param array<array-key, mixed> $changes
|
||||
*/
|
||||
private static function warnOnInvalidModifyRequestChanges(array $changes): void
|
||||
private static function assertValidModifyRequestChanges(array $changes): void
|
||||
{
|
||||
foreach (['method', 'query', 'version'] as $key) {
|
||||
if (\array_key_exists($key, $changes) && !\is_string($changes[$key])) {
|
||||
self::warnOnInvalidModifyRequestChange($key, 'string', $changes[$key]);
|
||||
self::assertValidModifyRequestChange($key, 'string', $changes[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof UriInterface) {
|
||||
self::warnOnInvalidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
|
||||
self::assertValidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
|
||||
}
|
||||
|
||||
if (\array_key_exists('body', $changes) && $changes['body'] === null) {
|
||||
self::warnOnInvalidModifyRequestChange('body', 'resource|string|int|float|bool|StreamInterface|callable|\Iterator|\Stringable', $changes['body']);
|
||||
self::assertValidModifyRequestChange('body', 'resource|string|StreamInterface|callable|\Iterator|\Stringable', $changes['body']);
|
||||
}
|
||||
|
||||
if (\array_key_exists('set_headers', $changes)) {
|
||||
if (!\is_array($changes['set_headers'])) {
|
||||
self::warnOnInvalidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
|
||||
self::assertValidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
|
||||
} else {
|
||||
foreach ($changes['set_headers'] as $header => $value) {
|
||||
$headerPath = \sprintf('set_headers.%s', (string) $header);
|
||||
|
||||
if (\is_array($value)) {
|
||||
if ($value === []) {
|
||||
self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
|
||||
self::assertValidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($value as $index => $item) {
|
||||
if (!\is_string($item)) {
|
||||
self::warnOnInvalidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
|
||||
self::assertValidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
|
||||
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
} elseif (!\is_string($value)) {
|
||||
self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
|
||||
self::assertValidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -364,14 +466,14 @@ final class Utils
|
|||
}
|
||||
|
||||
if (!\is_array($changes['remove_headers'])) {
|
||||
self::warnOnInvalidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
|
||||
self::assertValidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($changes['remove_headers'] as $index => $header) {
|
||||
if (!\is_string($header) && !\is_int($header)) {
|
||||
self::warnOnInvalidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
|
||||
self::assertValidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -381,21 +483,17 @@ final class Utils
|
|||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function warnOnInvalidModifyRequestChange(string $key, string $expected, $value): void
|
||||
private static function assertValidModifyRequestChange(string $key, string $expected, $value): void
|
||||
{
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
'Passing %s to Utils::modifyRequest() change "%s" is deprecated; guzzlehttp/psr7 3.0 requires %s.',
|
||||
\get_debug_type($value),
|
||||
$key,
|
||||
$expected
|
||||
);
|
||||
throw new \InvalidArgumentException(\sprintf('Utils::modifyRequest() change "%s" must be %s; %s provided.', DiagnosticValue::escape($key), $expected, \get_debug_type($value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a line from the stream up to the maximum allowed buffer length.
|
||||
*
|
||||
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
|
||||
* after a stream read cannot make progress.
|
||||
*
|
||||
* @param StreamInterface $stream Stream to read from
|
||||
* @param int|null $maxLength Maximum buffer length
|
||||
*/
|
||||
|
|
@ -405,7 +503,7 @@ final class Utils
|
|||
$size = 0;
|
||||
|
||||
while (!$stream->eof()) {
|
||||
if ('' === ($byte = $stream->read(1))) {
|
||||
if ('' === ($byte = StreamTimeout::read($stream, 1, 'Unable to read line from stream: timed out'))) {
|
||||
return $buffer;
|
||||
}
|
||||
$buffer .= $byte;
|
||||
|
|
@ -419,60 +517,129 @@ final class Utils
|
|||
}
|
||||
|
||||
/**
|
||||
* Redact the password in the user info part of a URI.
|
||||
* Redact the user info part of a URI.
|
||||
*
|
||||
* Returns the URI with the whole userinfo component replaced by "***"
|
||||
* when one is present, so neither the username nor the password survives
|
||||
* into logs and diagnostics. A URI without userinfo is returned
|
||||
* unchanged.
|
||||
*/
|
||||
public static function redactUserInfo(UriInterface $uri): UriInterface
|
||||
{
|
||||
$userInfo = $uri->getUserInfo();
|
||||
public static function redactUserInfo(
|
||||
#[\SensitiveParameter]
|
||||
UriInterface $uri
|
||||
): UriInterface {
|
||||
return $uri->getUserInfo() === '' ? $uri : $uri->withUserInfo('***');
|
||||
}
|
||||
|
||||
if (false !== ($pos = \strpos($userInfo, ':'))) {
|
||||
return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***');
|
||||
/**
|
||||
* Redacts the userinfo of a raw URI string wherever it appears in a
|
||||
* subject string.
|
||||
*
|
||||
* The needle is taken verbatim from the raw URI rather than from parsed
|
||||
* components, so credentials that URI normalization would rewrite, such
|
||||
* as raw control bytes or unencoded reserved characters, are still found
|
||||
* in text that embeds the URI exactly as given, for example transport
|
||||
* error messages. A URI without "://" is treated as authority-form: a
|
||||
* host and port with optional 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.
|
||||
*
|
||||
* @param string $subject Text that may embed the URI
|
||||
* @param string $uri Raw URI whose userinfo is redacted in the text
|
||||
*/
|
||||
public static function redactUserInfoInString(string $subject, string $uri): string
|
||||
{
|
||||
if (\strpos($uri, '@') === false) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
$schemePosition = \strpos($uri, '://');
|
||||
$remainder = $schemePosition === false ? $uri : \substr($uri, $schemePosition + 3);
|
||||
|
||||
if (\parse_url($schemePosition === false ? 'http://'.$uri : $uri) === false) {
|
||||
// Raw '/', '?', or '#' separators may sit inside the credentials
|
||||
// of a URI that defeats parse_url(), so the redaction cannot stop
|
||||
// at the apparent authority.
|
||||
$atPosition = \strrpos($remainder, '@');
|
||||
|
||||
if ($atPosition === false || $atPosition === 0) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
return \str_replace(\substr($remainder, 0, $atPosition).'@', '***@', $subject);
|
||||
}
|
||||
|
||||
$authority = \substr($remainder, 0, \strcspn($remainder, '/?#'));
|
||||
$atPosition = \strrpos($authority, '@');
|
||||
|
||||
if ($atPosition === false || $atPosition === 0) {
|
||||
// A parseable URI with '@' only past its authority, or with an
|
||||
// empty userinfo, carries no credentials to redact.
|
||||
return $subject;
|
||||
}
|
||||
|
||||
return \str_replace(\substr($authority, 0, $atPosition).'@', '***@', $subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new stream based on the input type.
|
||||
*
|
||||
* Options is an associative array that can contain the following keys:
|
||||
* Options are provided as an associative array that can contain the
|
||||
* following keys:
|
||||
* - metadata: Array of custom metadata.
|
||||
* - size: Size of the stream.
|
||||
*
|
||||
* This method accepts the following `$resource` types:
|
||||
* - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
|
||||
* - `string`: Creates a stream object that uses the given string as the contents.
|
||||
* - `resource`: Creates a stream object that wraps the given PHP stream resource.
|
||||
* - `Iterator`: If the provided value implements `Iterator`, then a read-only
|
||||
* stream object will be created that wraps the given iterable. Each time the
|
||||
* stream is read from, data from the iterator will fill a buffer and will be
|
||||
* continuously called until the buffer is equal to the requested read size.
|
||||
* Subsequent read calls will first read from the buffer and then call `next`
|
||||
* on the underlying iterator until it is exhausted.
|
||||
* - `object` with `__toString()`: If the object has the `__toString()` method,
|
||||
* the object will be cast to a string and then a stream will be returned that
|
||||
* uses the string value.
|
||||
* - `string`: Creates a stream object that uses the given string as the
|
||||
* contents.
|
||||
* - `resource`: Creates a stream object that wraps the given PHP stream
|
||||
* resource.
|
||||
* - `Iterator`: If the provided value implements `Iterator`, then a
|
||||
* read-only stream object will be created that wraps the given iterable.
|
||||
* Each time the stream is read from, data from the iterator will fill a
|
||||
* buffer and will be continuously called until the buffer is equal to the
|
||||
* requested read size. Yielded strings, integers, finite floats,
|
||||
* booleans, `null`, and stringable objects are converted to string
|
||||
* chunks; non-finite floats and other values throw
|
||||
* `UnexpectedValueException` when the stream is read. Values that
|
||||
* stringify to an empty string are skipped while the iterator advances.
|
||||
* Subsequent read calls will first read from the buffer and then call
|
||||
* `next` on the underlying iterator until it is exhausted.
|
||||
* - `object` with `__toString()`: If the object has the `__toString()`
|
||||
* method, the object will be cast to a string and then a stream will be
|
||||
* returned that uses the string value.
|
||||
* - `NULL`: When `null` is passed, an empty stream object is returned.
|
||||
* - `callable`: When a callable array, closure, or invokable object is passed
|
||||
* and no earlier resource or object rule applies, a read-only stream object
|
||||
* will be created that invokes the given callable. The callable is invoked
|
||||
* with the suggested number of bytes to read. The callable can return fewer
|
||||
* or more bytes than requested, but MUST return `false` or `null` when there
|
||||
* is no more data to return. Any additional bytes will be buffered and used
|
||||
* in subsequent reads. String inputs are always treated as string bodies,
|
||||
* even when they name callable functions.
|
||||
* - `callable`: When a callable array, closure, or invokable object is
|
||||
* passed and no earlier resource or object rule applies, a read-only
|
||||
* stream object will be created that invokes the given callable. The
|
||||
* callable is invoked with the suggested number of bytes to read. The
|
||||
* callable can return fewer or more bytes than requested, but MUST return
|
||||
* a non-empty string to provide data and MUST return `false` or `null`
|
||||
* when there is no more data to return. Any additional bytes will be
|
||||
* buffered and used in subsequent reads. String inputs are always treated
|
||||
* as string bodies, even when they name callable functions.
|
||||
*
|
||||
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
|
||||
* @param array{size?: int, metadata?: array} $options Additional options
|
||||
* @param resource|string|StreamInterface|callable|\Iterator|\Stringable|null $resource Entity body data
|
||||
* @param array{size?: int, metadata?: array} $options Additional options
|
||||
*
|
||||
* @throws \InvalidArgumentException if the $resource arg is not valid.
|
||||
*/
|
||||
public static function streamFor($resource = '', array $options = []): StreamInterface
|
||||
{
|
||||
if (is_scalar($resource)) {
|
||||
if (!is_string($resource)) {
|
||||
throw new \InvalidArgumentException(\sprintf(
|
||||
'Cannot create a stream from %s; pass a string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
|
||||
\get_debug_type($resource)
|
||||
));
|
||||
}
|
||||
|
||||
$stream = self::tryFopen('php://temp', 'r+');
|
||||
if ($resource !== '') {
|
||||
fwrite($stream, (string) $resource);
|
||||
fwrite($stream, $resource);
|
||||
fseek($stream, 0);
|
||||
}
|
||||
|
||||
|
|
@ -500,14 +667,29 @@ final class Utils
|
|||
if ($resource instanceof StreamInterface) {
|
||||
return $resource;
|
||||
} elseif ($resource instanceof \Iterator) {
|
||||
return new PumpStream(function () use ($resource) {
|
||||
if (!$resource->valid()) {
|
||||
return false;
|
||||
}
|
||||
$result = $resource->current();
|
||||
$resource->next();
|
||||
return new PumpStream(function (int $length) use ($resource) {
|
||||
while ($resource->valid()) {
|
||||
$result = $resource->current();
|
||||
$resource->next();
|
||||
|
||||
return $result;
|
||||
if (is_float($result) && !is_finite($result)) {
|
||||
throw new \UnexpectedValueException('Iterator must not yield non-finite float values');
|
||||
}
|
||||
|
||||
if ($result === null || is_scalar($result)) {
|
||||
$data = (string) $result;
|
||||
} elseif (is_object($result) && method_exists($result, '__toString')) {
|
||||
$data = (string) $result;
|
||||
} else {
|
||||
throw new \UnexpectedValueException('Iterator must yield scalar, null, or stringable values');
|
||||
}
|
||||
|
||||
if ($data !== '') {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, $options);
|
||||
} elseif (method_exists($resource, '__toString')) {
|
||||
return self::streamFor((string) $resource, $options);
|
||||
|
|
@ -521,14 +703,14 @@ final class Utils
|
|||
return new PumpStream($resource, $options);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Invalid resource type: '.gettype($resource));
|
||||
throw new \InvalidArgumentException('Invalid resource type: '.\get_debug_type($resource));
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely opens a PHP stream resource using a filename.
|
||||
*
|
||||
* When fopen fails, PHP normally raises a warning. This function adds an
|
||||
* error handler that checks for errors and throws an exception instead.
|
||||
* When `fopen()` fails, PHP normally raises a warning. This function adds
|
||||
* an error handler that checks for errors and throws an exception instead.
|
||||
*
|
||||
* @param string $filename File to open
|
||||
* @param string $mode Mode used to open the file
|
||||
|
|
@ -541,12 +723,7 @@ final class Utils
|
|||
{
|
||||
$ex = null;
|
||||
set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool {
|
||||
$ex = new \RuntimeException(sprintf(
|
||||
'Unable to open "%s" using mode "%s": %s',
|
||||
$filename,
|
||||
$mode,
|
||||
$errstr
|
||||
));
|
||||
$ex = new \RuntimeException(sprintf('Unable to open %s using mode %s: %s', DiagnosticValue::escape($filename), DiagnosticValue::escape($mode), DiagnosticValue::escape($errstr)));
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
@ -555,12 +732,7 @@ final class Utils
|
|||
/** @var resource $handle */
|
||||
$handle = fopen($filename, $mode);
|
||||
} catch (\Throwable $e) {
|
||||
$ex = new \RuntimeException(sprintf(
|
||||
'Unable to open "%s" using mode "%s": %s',
|
||||
$filename,
|
||||
$mode,
|
||||
$e->getMessage()
|
||||
), 0, $e);
|
||||
$ex = new \RuntimeException(sprintf('Unable to open %s using mode %s: %s', DiagnosticValue::escape($filename), DiagnosticValue::escape($mode), $e->getMessage()), 0, $e);
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
|
|
@ -576,10 +748,13 @@ final class Utils
|
|||
/**
|
||||
* Safely gets the contents of a given stream.
|
||||
*
|
||||
* When stream_get_contents fails, PHP normally raises a warning. This
|
||||
* When `stream_get_contents()` fails, PHP normally raises a warning. This
|
||||
* function adds an error handler that checks for errors and throws an
|
||||
* exception instead.
|
||||
*
|
||||
* Throws `TimeoutException` when PHP-style timeout metadata can be detected
|
||||
* after a stream read cannot make progress.
|
||||
*
|
||||
* @param resource $stream
|
||||
*
|
||||
* @throws \RuntimeException if the stream cannot be read
|
||||
|
|
@ -588,10 +763,7 @@ final class Utils
|
|||
{
|
||||
$ex = null;
|
||||
set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool {
|
||||
$ex = new \RuntimeException(sprintf(
|
||||
'Unable to read stream contents: %s',
|
||||
$errstr
|
||||
));
|
||||
$ex = new \RuntimeException(sprintf('Unable to read stream contents: %s', DiagnosticValue::escape($errstr)));
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
@ -601,13 +773,18 @@ final class Utils
|
|||
$contents = stream_get_contents($stream);
|
||||
|
||||
if ($contents === false) {
|
||||
$ex = new \RuntimeException('Unable to read stream contents');
|
||||
$ex = StreamTimeout::isResourceReadTimedOut($stream)
|
||||
? new TimeoutException('Unable to read stream contents: timed out')
|
||||
: new \RuntimeException('Unable to read stream contents');
|
||||
} elseif (StreamTimeout::isResourceReadTimedOut($stream)) {
|
||||
$ex = new TimeoutException('Unable to read stream contents: timed out');
|
||||
}
|
||||
} catch (TimeoutException $e) {
|
||||
$ex = $e;
|
||||
} catch (\Throwable $e) {
|
||||
$ex = new \RuntimeException(sprintf(
|
||||
'Unable to read stream contents: %s',
|
||||
$e->getMessage()
|
||||
), 0, $e);
|
||||
$ex = StreamTimeout::isResourceReadTimedOut($stream)
|
||||
? new TimeoutException('Unable to read stream contents: timed out', 0, $e)
|
||||
: new \RuntimeException(sprintf('Unable to read stream contents: %s', $e->getMessage()), 0, $e);
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
|
|
@ -621,11 +798,11 @@ final class Utils
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a UriInterface for the given value.
|
||||
* Returns a `UriInterface` for the given value.
|
||||
*
|
||||
* This function accepts a string or UriInterface and returns a
|
||||
* UriInterface for the given value. If the value is already a
|
||||
* UriInterface, it is returned as-is.
|
||||
* This function accepts a string or `UriInterface` and returns a
|
||||
* `UriInterface` for the given value. If the value is already a
|
||||
* `UriInterface`, it is returned as-is.
|
||||
*
|
||||
* @param string|UriInterface $uri
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue