This commit is contained in:
Roland Gruber 2025-09-01 20:41:04 +02:00
parent d78ddb43b1
commit 0593b55ed9
345 changed files with 3911 additions and 1907 deletions

428
lam/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -60,9 +60,7 @@ abstract class AbstractErrorParser
foreach ($errors as $key => $error) {
// If error code matches a known error shape, populate the body
if ($data['code'] == $error['name']
&& $error instanceof StructureShape
) {
if ($this->errorCodeMatches($data, $error)) {
$modeledError = $error;
$data['body'] = $this->extractPayload(
$modeledError,
@ -92,4 +90,10 @@ abstract class AbstractErrorParser
return $data;
}
private function errorCodeMatches(array $data, $error): bool
{
return $data['code'] == $error['name']
|| (isset($error['error']['code']) && $data['code'] === $error['error']['code']);
}
}

View file

@ -12,41 +12,133 @@ trait JsonParserTrait
{
use PayloadParserTrait;
private function genericHandler(ResponseInterface $response)
private function genericHandler(ResponseInterface $response): array
{
$code = (string) $response->getStatusCode();
$error_code = null;
$error_type = null;
// Parse error code and type for query compatible services
if ($this->api
&& !is_null($this->api->getMetadata('awsQueryCompatible'))
&& $response->getHeaderLine('x-amzn-query-error')
&& $response->hasHeader('x-amzn-query-error')
) {
$queryError = $response->getHeaderLine('x-amzn-query-error');
$parts = explode(';', $queryError);
if (isset($parts) && count($parts) == 2 && $parts[0] && $parts[1]) {
$error_code = $parts[0];
$error_type = $parts[1];
$awsQueryError = $this->parseAwsQueryCompatibleHeader($response);
if ($awsQueryError) {
$error_code = $awsQueryError['code'];
$error_type = $awsQueryError['type'];
}
}
// Parse error code from X-Amzn-Errortype header
if (!$error_code && $response->hasHeader('X-Amzn-Errortype')) {
$error_code = $this->extractErrorCode(
$response->getHeaderLine('X-Amzn-Errortype')
);
}
$parsedBody = null;
$body = $response->getBody();
if (!$body->isSeekable() || $body->getSize()) {
$parsedBody = $this->parseJson((string) $body, $response);
}
// Parse error code from response body
if (!$error_code && $parsedBody) {
$error_code = $this->parseErrorFromBody($parsedBody);
}
if (!isset($error_type)) {
$error_type = $code[0] == '4' ? 'client' : 'server';
}
return [
'request_id' => (string) $response->getHeaderLine('x-amzn-requestid'),
'code' => isset($error_code) ? $error_code : null,
'request_id' => $response->getHeaderLine('x-amzn-requestid'),
'code' => $error_code ?? null,
'message' => null,
'type' => $error_type,
'parsed' => $this->parseJson($response->getBody(), $response)
'parsed' => $parsedBody
];
}
/**
* Parse AWS Query Compatible error from header
*
* @param ResponseInterface $response
* @return array|null Returns ['code' => string, 'type' => string] or null
*/
private function parseAwsQueryCompatibleHeader(ResponseInterface $response): ?array
{
$queryError = $response->getHeaderLine('x-amzn-query-error');
$parts = explode(';', $queryError);
if (count($parts) === 2 && $parts[0] && $parts[1]) {
return [
'code' => $parts[0],
'type' => $parts[1]
];
}
return null;
}
/**
* Parse error code from response body
*
* @param array|null $parsedBody
* @return string|null
*/
private function parseErrorFromBody(?array $parsedBody): ?string
{
if (!$parsedBody
|| (!isset($parsedBody['code']) && !isset($parsedBody['__type']))
) {
return null;
}
$error_code = $parsedBody['code'] ?? $parsedBody['__type'];
return $this->extractErrorCode($error_code);
}
/**
* Extract error code from raw error string containing # and/or : delimiters
*
* @param string $rawErrorCode
* @return string
*/
private function extractErrorCode(string $rawErrorCode): string
{
// Handle format with both # and uri (e.g., "namespace#http://foo-bar")
if (str_contains($rawErrorCode, ':') && str_contains($rawErrorCode, '#')) {
$start = strpos($rawErrorCode, '#') + 1;
$end = strpos($rawErrorCode, ':', $start);
return substr($rawErrorCode, $start, $end - $start);
}
// Handle format with uri only : (e.g., "ErrorCode:http://foo-bar.com/baz")
if (str_contains($rawErrorCode, ':')) {
return substr($rawErrorCode, 0, strpos($rawErrorCode, ':'));
}
// Handle format with only # (e.g., "namespace#ErrorCode")
if (str_contains($rawErrorCode, '#')) {
return substr($rawErrorCode, strpos($rawErrorCode, '#') + 1);
}
return $rawErrorCode;
}
protected function payload(
ResponseInterface $response,
StructureShape $member
) {
$jsonBody = $this->parseJson($response->getBody(), $response);
$body = $response->getBody();
if (!$body->isSeekable() || $body->getSize()) {
$jsonBody = $this->parseJson($body, $response);
} else {
$jsonBody = (string) $body;
}
if ($jsonBody) {
return $this->parser->parse($member, $jsonBody);
}
}
}

View file

@ -37,9 +37,7 @@ class JsonRpcErrorParser extends AbstractErrorParser
$parts = explode('#', $data['parsed']['__type']);
$data['code'] = isset($parts[1]) ? $parts[1] : $parts[0];
}
$data['message'] = isset($data['parsed']['message'])
? $data['parsed']['message']
: null;
$data['message'] = $data['parsed']['message'] ?? null;
}
$this->populateShape($data, $response, $command);

View file

@ -30,7 +30,7 @@ class RestJsonErrorParser extends AbstractErrorParser
// Merge in error data from the JSON body
if ($json = $data['parsed']) {
$data = array_replace($data, $json);
$data = array_replace($json, $data);
}
// Correct error type from services like Amazon Glacier
@ -38,18 +38,9 @@ class RestJsonErrorParser extends AbstractErrorParser
$data['type'] = strtolower($data['type']);
}
// Retrieve the error code from services like Amazon Elastic Transcoder
if ($code = $response->getHeaderLine('x-amzn-errortype')) {
$colon = strpos($code, ':');
$data['code'] = $colon ? substr($code, 0, $colon) : $code;
}
// Retrieve error message directly
$data['message'] = isset($data['parsed']['message'])
? $data['parsed']['message']
: (isset($data['parsed']['Message'])
? $data['parsed']['Message']
: null);
$data['message'] = $data['parsed']['message']
?? ($data['parsed']['Message'] ?? null);
$this->populateShape($data, $response, $command);

View file

@ -55,8 +55,9 @@ abstract class AbstractRestParser extends AbstractParser
}
}
$body = $response->getBody();
if (!$payload
&& $response->getBody()->getSize() > 0
&& (!$body->isSeekable() || $body->getSize())
&& count($output->getMembers()) > 0
) {
// if no payload was found, then parse the contents of the body
@ -73,20 +74,26 @@ abstract class AbstractRestParser extends AbstractParser
array &$result
) {
$member = $output->getMember($payload);
$body = $response->getBody();
if (!empty($member['eventstream'])) {
$result[$payload] = new EventParsingIterator(
$response->getBody(),
$body,
$member,
$this
);
} elseif ($member instanceof StructureShape) {
// Structure members parse top-level data into a specific key.
//Unions must have at least one member set to a non-null value
// If the body is empty, we can assume it is unset
if (!empty($member['union']) && ($body->isSeekable() && !$body->getSize())) {
return;
}
$result[$payload] = [];
$this->payload($response, $member, $result[$payload]);
} else {
// Streaming data is just the stream from the response body.
$result[$payload] = $response->getBody();
// Always set the payload to the body stream, regardless of content
$result[$payload] = $body;
}
}
@ -100,13 +107,21 @@ abstract class AbstractRestParser extends AbstractParser
&$result
) {
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
// Empty headers should not be deserialized
if ($value === null || $value === '') {
return;
}
switch ($shape->getType()) {
case 'float':
case 'double':
$value = (float) $value;
$value = match ($value) {
'NaN', 'Infinity', '-Infinity' => $value,
default => (float) $value
};
break;
case 'long':
case 'integer':
$value = (int) $value;
break;
case 'boolean':
@ -143,6 +158,23 @@ abstract class AbstractRestParser extends AbstractParser
//output structure.
return;
}
case 'list':
$listMember = $shape->getMember();
$type = $listMember->getType();
// Only boolean lists require special handling
// other types can be returned as-is
if ($type !== 'boolean') {
break;
}
$items = array_map('trim', explode(',', $value));
$value = array_map(
static fn($item) => filter_var($item, FILTER_VALIDATE_BOOLEAN),
$items
);
break;
}
$result[$name] = $value;

View file

@ -50,8 +50,11 @@ class JsonParser
$values = $shape->getValue();
$target = [];
foreach ($value as $k => $v) {
// null map values should not be deserialized
if (!is_null($v)) {
$target[$k] = $this->parse($values, $v);
}
}
return $target;
case 'timestamp':

View file

@ -17,6 +17,10 @@ trait MetadataParserTrait
&$result
) {
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
// Empty values should not be deserialized
if ($value === null || $value === '') {
return;
}
switch ($shape->getType()) {
case 'float':
@ -24,6 +28,7 @@ trait MetadataParserTrait
$value = (float) $value;
break;
case 'long':
case 'integer':
$value = (int) $value;
break;
case 'boolean':

View file

@ -40,7 +40,15 @@ class QueryParser extends AbstractParser
ResponseInterface $response
) {
$output = $this->api->getOperation($command->getName())->getOutput();
$xml = $this->parseXml($response->getBody(), $response);
$body = $response->getBody();
$xml = !$body->isSeekable() || $body->getSize()
? $this->parseXml($body, $response)
: null;
// Empty request bodies should not be deserialized.
if (is_null($xml)) {
return new Result();
}
if ($this->honorResultWrapper && $output['resultWrapper']) {
$xml = $xml->{$output['resultWrapper']};

View file

@ -28,10 +28,25 @@ class RestJsonParser extends AbstractRestParser
StructureShape $member,
array &$result
) {
$jsonBody = $this->parseJson($response->getBody(), $response);
$responseBody = (string) $response->getBody();
if ($jsonBody) {
$result += $this->parser->parse($member, $jsonBody);
// Parse JSON if we have content
$parsedJson = null;
if (!empty($responseBody)) {
$parsedJson = $this->parseJson($responseBody, $response);
} else {
// An empty response body should be deserialized as null
$result = $parsedJson;
return;
}
$parsedBody = $this->parser->parse($member, $parsedJson);
if (is_string($parsedBody) && $member['document']) {
// Document types can be strings: replace entire result
$result = $parsedBody;
} else {
// Merge array/object results into existing result
$result = array_merge($result, (array) $parsedBody);
}
}

View file

@ -76,16 +76,19 @@ class XmlParser
private function memberKey(Shape $shape, $name)
{
if (null !== $shape['locationName']) {
return $shape['locationName'];
}
if ($shape instanceof ListShape && $shape['flattened']) {
return $shape->getMember()['locationName'] ?: $name;
}
// Check if locationName came from shape definition
if ($shape instanceof StructureShape && isset($shape['locationName'])) {
$originalDef = $shape->getOriginalDefinition($shape->getName());
if ($originalDef && isset($originalDef['locationName'])
&& $originalDef['locationName'] === $shape['locationName']
) {
return $name;
}
}
return $shape['locationName'] ?? $name;
}
private function parse_list(ListShape $shape, \SimpleXMLElement $value)
{
@ -132,7 +135,12 @@ class XmlParser
private function parse_float(Shape $shape, $value)
{
return (float) (string) $value;
$value = (string) $value;
return match ($value) {
'NaN', 'Infinity', '-Infinity' => $value,
default => (float) $value
};
}
private function parse_integer(Shape $shape, $value)
@ -162,12 +170,8 @@ class XmlParser
private function parse_xml_attribute(Shape $shape, Shape $memberShape, $value)
{
$namespace = $shape['xmlNamespace']['uri']
? $shape['xmlNamespace']['uri']
: '';
$prefix = $shape['xmlNamespace']['prefix']
? $shape['xmlNamespace']['prefix']
: '';
$namespace = $shape['xmlNamespace']['uri'] ?? '';
$prefix = $shape['xmlNamespace']['prefix'] ?? '';
if (!empty($prefix)) {
$prefix .= ':';
}

View file

@ -45,14 +45,22 @@ class JsonBody
* Builds the JSON body based on an array of arguments.
*
* @param Shape $shape Operation being constructed
* @param array $args Associative array of arguments
* @param array|string $args Associative array of arguments, or a string.
*
* @return string
*/
public function build(Shape $shape, array $args)
public function build(Shape $shape, array|string $args)
{
$result = json_encode($this->format($shape, $args));
return $result == '[]' ? '{}' : $result;
try {
$result = json_encode($this->format($shape, $args), JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new InvalidJsonException(
'Unable to encode JSON document ' . $shape->getName() . ': ' .
$e->getMessage() . PHP_EOL
);
}
return $result === '[]' ? '{}' : $result;
}
private function format(Shape $shape, $value)
@ -60,7 +68,7 @@ class JsonBody
switch ($shape['type']) {
case 'structure':
$data = [];
if (isset($shape['document']) && $shape['document']) {
if ($shape['document'] ?? false) {
return $value;
}
foreach ($value as $k => $v) {

View file

@ -62,23 +62,26 @@ class JsonRpcSerializer
$operationName = $command->getName();
$operation = $this->api->getOperation($operationName);
$commandArgs = $command->toArray();
$body = $this->jsonFormatter->build($operation->getInput(), $commandArgs);
$headers = [
'X-Amz-Target' => $this->api->getMetadata('targetPrefix') . '.' . $operationName,
'Content-Type' => $this->contentType
'Content-Type' => $this->contentType,
'Content-Length' => strlen($body)
];
if ($endpoint instanceof RulesetEndpoint) {
$this->setEndpointV2RequestOptions($endpoint, $headers);
}
$requestUri = $operation['http']['requestUri'] ?? null;
$absoluteUri = str_ends_with($this->endpoint, '/')
? $this->endpoint : $this->endpoint . $requestUri;
return new Request(
$operation['http']['method'],
$this->endpoint,
$absoluteUri,
$headers,
$this->jsonFormatter->build(
$operation->getInput(),
$commandArgs
)
$body
);
}
}

View file

@ -98,7 +98,8 @@ class QueryParamBuilder
if (!$this->isFlat($shape)) {
$locationName = $shape->getMember()['locationName'] ?: 'member';
$prefix .= ".$locationName";
} elseif ($name = $this->queryName($items)) {
// flattened lists can also model a `locationName`
} elseif ($name = $shape['locationName'] ?? $this->queryName($items)) {
$parts = explode('.', $prefix);
$parts[count($parts) - 1] = $name;
$prefix = implode('.', $parts);

View file

@ -36,9 +36,7 @@ class QuerySerializer
* containing "method", "uri", "headers", and "body" key value pairs.
*
* @param CommandInterface $command Command to serialize into a request.
* @param $endpointProvider Provider used for dynamic endpoint resolution.
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
*
* @param null $endpoint Endpoint resolved using EndpointProviderV2
* @return RequestInterface
*/
public function __invoke(
@ -66,14 +64,17 @@ class QuerySerializer
'Content-Length' => strlen($body),
'Content-Type' => 'application/x-www-form-urlencoded'
];
$requestUri = $operation['http']['requestUri'] ?? null;
if ($endpoint instanceof RulesetEndpoint) {
$this->setEndpointV2RequestOptions($endpoint, $headers);
}
$absoluteUri = str_ends_with($this->endpoint, '/')
? $this->endpoint : $this->endpoint . $requestUri;
return new Request(
'POST',
$this->endpoint,
$absoluteUri,
$headers,
$body
);

View file

@ -31,12 +31,11 @@ class RestJsonSerializer extends RestSerializer
$this->jsonFormatter = $jsonFormatter ?: new JsonBody($api);
}
protected function payload(StructureShape $member, array $value, array &$opts)
protected function payload(StructureShape $member, array|string $value, array &$opts)
{
$body = isset($value) ?
((string) $this->jsonFormatter->build($member, $value))
: "{}";
$opts['headers']['Content-Type'] = $this->contentType;
$body = $this->jsonFormatter->build($member, $value);
$opts['headers']['Content-Length'] = strlen($body);
$opts['body'] = $body;
}
}

View file

@ -1,6 +1,7 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Service;
use Aws\Api\Operation;
@ -10,11 +11,13 @@ use Aws\Api\TimestampShape;
use Aws\CommandInterface;
use Aws\EndpointV2\EndpointV2SerializerTrait;
use Aws\EndpointV2\Ruleset\RulesetEndpoint;
use DateTimeInterface;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* Serializes HTTP locations like header, uri, payload, etc...
@ -22,10 +25,15 @@ use Psr\Http\Message\RequestInterface;
*/
abstract class RestSerializer
{
use EndpointV2SerializerTrait;
private const TEMPLATE_STRING_REGEX = '/\{([^\}]+)\}/';
private static array $excludeContentType = [
's3' => true,
'glacier' => true
];
/** @var Service */
private $api;
private Service $api;
/** @var Uri */
private $endpoint;
@ -33,6 +41,8 @@ abstract class RestSerializer
/** @var bool */
private $isUseEndpointV2;
use EndpointV2SerializerTrait;
/**
* @param Service $api Service API description
* @param string $endpoint Endpoint to connect to
@ -45,19 +55,18 @@ abstract class RestSerializer
/**
* @param CommandInterface $command Command to serialize into a request.
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
*
* @param mixed|null $endpoint
* @return RequestInterface
*/
public function __invoke(
CommandInterface $command,
$endpoint = null
mixed $endpoint = null
)
{
$operation = $this->api->getOperation($command->getName());
$commandArgs = $command->toArray();
$opts = $this->serialize($operation, $commandArgs);
$headers = isset($opts['headers']) ? $opts['headers'] : [];
$headers = $opts['headers'] ?? [];
if ($endpoint instanceof RulesetEndpoint) {
$this->isUseEndpointV2 = true;
@ -70,7 +79,7 @@ abstract class RestSerializer
$operation['http']['method'],
$uri,
$headers,
isset($opts['body']) ? $opts['body'] : null
$opts['body'] ?? null
);
}
@ -103,20 +112,20 @@ abstract class RestSerializer
$location = $member['location'];
if (!$payload && !$location) {
$bodyMembers[$name] = $value;
} elseif ($location == 'header') {
} elseif ($location === 'header') {
$this->applyHeader($name, $member, $value, $opts);
} elseif ($location == 'querystring') {
} elseif ($location === 'querystring') {
$this->applyQuery($name, $member, $value, $opts);
} elseif ($location == 'headers') {
} elseif ($location === 'headers') {
$this->applyHeaderMap($name, $member, $value, $opts);
}
}
}
if (isset($bodyMembers)) {
$this->payload($operation->getInput(), $bodyMembers, $opts);
$this->payload($input, $bodyMembers, $opts);
} else if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
$this->payload($operation->getInput(), [], $opts);
$this->payload($input, [], $opts);
}
return $opts;
@ -130,12 +139,32 @@ abstract class RestSerializer
$m = $input->getMember($name);
$type = $m->getType();
if ($m['streaming'] ||
($m['type'] == 'string' || $m['type'] == 'blob')
($type === 'string' || $type === 'blob')
) {
// This path skips setting the content-type header usually done in
// RestJsonSerializer and RestXmlSerializer.certain S3 and glacier
// operations determine content type in Middleware::ContentType()
if (!isset(self::$excludeContentType[$this->api->getServiceName()])) {
switch ($type) {
case 'string':
$opts['headers']['Content-Type'] = 'text/plain';
break;
case 'blob':
$opts['headers']['Content-Type'] = 'application/octet-stream';
break;
}
}
$body = $args[$name];
if (!$m['streaming'] && is_string($body)) {
$opts['headers']['Content-Length'] = strlen($body);
}
// Streaming bodies or payloads that are strings are
// always just a stream of data.
$opts['body'] = Psr7\Utils::streamFor($args[$name]);
$opts['body'] = Psr7\Utils::streamFor($body);
return;
}
@ -144,13 +173,29 @@ abstract class RestSerializer
private function applyHeader($name, Shape $member, $value, array &$opts)
{
if ($member->getType() === 'timestamp') {
$timestampFormat = !empty($member['timestampFormat'])
? $member['timestampFormat']
: 'rfc822';
$value = TimestampShape::format($value, $timestampFormat);
} elseif ($member->getType() === 'boolean') {
$value = $value ? 'true' : 'false';
// Handle lists by recursively applying header logic to each element
if ($member instanceof ListShape) {
$listMember = $member->getMember();
$headerValues = [];
foreach ($value as $listValue) {
$tempOpts = ['headers' => []];
$this->applyHeader('temp', $listMember, $listValue, $tempOpts);
$convertedValue = $tempOpts['headers']['temp'];
$headerValues[] = $convertedValue;
}
$value = $headerValues;
} elseif (!is_null($value)) {
switch ($member->getType()) {
case 'timestamp':
$timestampFormat = $member['timestampFormat'] ?? 'rfc822';
$value = $this->formatTimestamp($value, $timestampFormat);
break;
case 'boolean':
$value = $this->formatBoolean($value);
break;
}
}
if ($member['jsonvalue']) {
@ -183,148 +228,259 @@ abstract class RestSerializer
$opts['query'] = isset($opts['query']) && is_array($opts['query'])
? $opts['query'] + $value
: $value;
} elseif ($value !== null) {
$type = $member->getType();
if ($type === 'boolean') {
$value = $value ? 'true' : 'false';
} elseif ($type === 'timestamp') {
$timestampFormat = !empty($member['timestampFormat'])
? $member['timestampFormat']
: 'iso8601';
$value = TimestampShape::format($value, $timestampFormat);
} elseif ($member instanceof ListShape) {
$listMember = $member->getMember();
$paramName = $member['locationName'] ?: $name;
foreach ($value as $listValue) {
// Recursively call applyQuery for each list element
$tempOpts = ['query' => []];
$this->applyQuery('temp', $listMember, $listValue, $tempOpts);
$opts['query'][$paramName][] = $tempOpts['query']['temp'];
}
} elseif (!is_null($value)) {
switch ($member->getType()) {
case 'timestamp':
$timestampFormat = $member['timestampFormat'] ?? 'iso8601';
$value = $this->formatTimestamp($value, $timestampFormat);
break;
case 'boolean':
$value = $this->formatBoolean($value);
break;
}
$opts['query'][$member['locationName'] ?: $name] = $value;
}
}
private function buildEndpoint(Operation $operation, array $args, array $opts)
private function buildEndpoint(
Operation $operation,
array $args,
array $opts
): UriInterface
{
// Expand `requestUri` field members
$relativeUri = $this->expandUriTemplate($operation, $args);
// Add query members to relativeUri
if (!empty($opts['query'])) {
$relativeUri = $this->appendQuery($opts['query'], $relativeUri);
}
// Special case - S3 keys that need path preservation
if ($this->api->getServiceName() === 's3'
&& isset($args['Key'])
&& $this->shouldPreservePath($args['Key'])
) {
return new Uri($this->endpoint . $relativeUri);
}
return $this->resolveUri($relativeUri, $opts);
}
/**
* Expands `requestUri` members
*
* @param Operation $operation
* @param array $args
*
* @return string
*/
private function expandUriTemplate(Operation $operation, array $args): string
{
$serviceName = $this->api->getServiceName();
// Create an associative array of variable definitions used in expansions
$varDefinitions = $this->getVarDefinitions($operation, $args);
$relative = preg_replace_callback(
'/\{([^\}]+)\}/',
function (array $matches) use ($varDefinitions) {
$isGreedy = substr($matches[1], -1, 1) == '+';
$k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
if (!isset($varDefinitions[$k])) {
return preg_replace_callback(
self::TEMPLATE_STRING_REGEX,
static function (array $matches) use ($varDefinitions) {
$isGreedy = str_ends_with($matches[1], '+');
$varName = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
if (!isset($varDefinitions[$varName])) {
return '';
}
$value = $varDefinitions[$varName];
if ($isGreedy) {
return str_replace('%2F', '/', rawurlencode($varDefinitions[$k]));
return str_replace('%2F', '/', rawurlencode($value));
}
return rawurlencode($varDefinitions[$k]);
return rawurlencode($value);
},
$operation['http']['requestUri']
);
// Add the query string variables or appending to one if needed.
if (!empty($opts['query'])) {
$relative = $this->appendQuery($opts['query'], $relative);
}
$path = $this->endpoint->getPath();
if ($this->isUseEndpointV2 && $serviceName === 's3') {
if (substr($path, -1) === '/' && $relative[0] === '/') {
$path = rtrim($path, '/');
/**
* Checks for path-like key names. If detected, traditional
* URI resolution is bypassed.
*
* @param string $key
* @return bool
*/
private function shouldPreservePath(string $key): bool
{
// Keys with dot segments
if (str_contains($key, '.')) {
$segments = explode('/', $key);
foreach ($segments as $segment) {
if ($segment === '.' || $segment === '..') {
return true;
}
}
}
$relative = $path . $relative;
if (strpos($relative, '../') !== false
|| substr($relative, -2) === '..'
// Keys starting with slash
if (str_starts_with($key, '/')) {
return true;
}
return false;
}
/**
* @param string $relativeUri
* @param array $opts
*
* @return UriInterface
*/
private function resolveUri(string $relativeUri, array $opts): UriInterface
{
$basePath = $this->endpoint->getPath();
// Only process if we have a non-empty base path
if (!empty($basePath) && $basePath !== '/') {
// if relative is just '/', we want just the base path without trailing slash
if ($relativeUri === '/' || empty($relativeUri)) {
// Remove trailing slash if present
return $this->endpoint->withPath(rtrim($basePath, '/'));
}
// if relative is '/?query', we want base path without trailing slash + query
// for now, this is only seen with S3 GetBucketLocation after processing the model
if (empty($opts['query'])
&& str_starts_with($relativeUri, '/?')
) {
if ($relative[0] !== '/') {
$relative = '/' . $relative;
$query = substr($relativeUri, 2); // Remove '/?'
return $this->endpoint->withQuery($query);
}
return new Uri($this->endpoint->withPath('') . $relative);
// Ensure base path has trailing slash
if (!str_ends_with($basePath, '/')) {
$this->endpoint = $this->endpoint->withPath($basePath . '/');
}
// Remove leading slash from relative path to make it relative
if (str_starts_with($relativeUri, '/')) {
$relativeUri = substr($relativeUri, 1);
}
}
if (((!empty($relative) && $relative !== '/')
&& !$this->isUseEndpointV2)
|| (isset($serviceName) && str_starts_with($serviceName, 'geo-'))
) {
$this->normalizePath($path);
}
// If endpoint has path, remove leading '/' to preserve URI resolution.
if ($path && $relative[0] === '/') {
$relative = substr($relative, 1);
}
//Append path to endpoint when leading '//...'
// present as uri cannot be properly resolved
if ($this->isUseEndpointV2 && strpos($relative, '//') === 0) {
return new Uri($this->endpoint . $relative);
}
// Expand path place holders using Amazon's slightly different URI
// template syntax.
return UriResolver::resolve($this->endpoint, new Uri($relative));
return UriResolver::resolve($this->endpoint, new Uri($relativeUri));
}
/**
* @param StructureShape $input
* @param $payload
*
* @return bool
*/
private function hasPayloadParam(StructureShape $input, $payload)
{
if ($payload) {
$potentiallyEmptyTypes = ['blob','string'];
if ($this->api->getMetadata('protocol') == 'rest-xml') {
if ($this->api->getProtocol() === 'rest-xml') {
$potentiallyEmptyTypes[] = 'structure';
}
$payloadMember = $input->getMember($payload);
if (in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
//unions may also be empty/unset
if (!empty($payloadMember['union'])
|| in_array($payloadMember['type'], $potentiallyEmptyTypes)
) {
return false;
}
}
foreach ($input->getMembers() as $member) {
if (!isset($member['location'])) {
return true;
}
}
return false;
}
private function appendQuery($query, $endpoint)
/**
* @param $query
* @param $relativeUri
*
* @return string
*/
private function appendQuery($query, $relativeUri): string
{
$append = Psr7\Query::build($query);
return $endpoint .= strpos($endpoint, '?') !== false ? "&{$append}" : "?{$append}";
return $relativeUri
. (str_contains($relativeUri, '?') ? "&{$append}" : "?{$append}");
}
private function getVarDefinitions($command, $args)
/**
* @param CommandInterface $command
* @param array $args
*
* @return array
*/
private function getVarDefinitions(
Operation $operation,
array $args
): array
{
$varDefinitions = [];
foreach ($command->getInput()->getMembers() as $name => $member) {
if ($member['location'] == 'uri') {
$varDefinitions[$member['locationName'] ?: $name] =
isset($args[$name])
? $args[$name]
: null;
foreach ($operation->getInput()->getMembers() as $name => $member) {
if ($member['location'] === 'uri') {
$value = $args[$name] ?? null;
if (!is_null($value)) {
switch ($member->getType()) {
case 'timestamp':
$timestampFormat = $member['timestampFormat'] ?? 'iso8601';
$value = $this->formatTimestamp($value, $timestampFormat);
break;
case 'boolean':
$value = $this->formatBoolean($value);
break;
}
}
$varDefinitions[$member['locationName'] ?: $name] = $value;
}
}
return $varDefinitions;
}
/**
* Appends trailing slash to non-empty paths with at least one segment
* to ensure proper URI resolution
* @param DateTimeInterface|string|int $value
* @param string $timestampFormat
*
* @param string $path
*
* @return void
* @return string
*/
private function normalizePath(string $path): void
private function formatTimestamp(
DateTimeInterface|string|int $value,
string $timestampFormat
): string
{
if (!empty($path) && $path !== '/' && substr($path, -1) !== '/') {
$this->endpoint = $this->endpoint->withPath($path . '/');
}
return TimestampShape::format($value, $timestampFormat);
}
/**
* @param $value
*
* @return string
*/
private function formatBoolean($value): string
{
return $value ? 'true' : 'false';
}
}

View file

@ -29,7 +29,9 @@ class RestXmlSerializer extends RestSerializer
protected function payload(StructureShape $member, array $value, array &$opts)
{
$opts['headers']['Content-Type'] = 'application/xml';
$opts['body'] = $this->getXmlBody($member, $value);
$body = $this->getXmlBody($member, $value);
$opts['headers']['Content-Length'] = strlen($body);
$opts['body'] = $body;
}
/**

View file

@ -14,8 +14,8 @@ use XMLWriter;
*/
class XmlBody
{
/** @var \Aws\Api\Service */
private $api;
/** @var Service */
private Service $api;
/**
* @param Service $api API being used to create the XML body.
@ -38,7 +38,10 @@ class XmlBody
$xml = new XMLWriter();
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$this->format($shape, $shape['locationName'] ?: $shape['name'], $args, $xml);
$rootElementName = $this->determineRootElementName($shape);
$this->format($shape, $rootElementName, $args, $xml);
$xml->endDocument();
return $xml->outputMemory();
@ -51,7 +54,7 @@ class XmlBody
if ($ns = $shape['xmlNamespace']) {
$xml->writeAttribute(
isset($ns['prefix']) ? "xmlns:{$ns['prefix']}" : 'xmlns',
$shape['xmlNamespace']['uri']
$ns['uri']
);
}
}
@ -93,9 +96,19 @@ class XmlBody
$this->startElement($shape, $name, $xml);
foreach ($this->getStructureMembers($shape, $value) as $k => $definition) {
// Default to member name
$elementName = $k;
// Only use locationName for non-structure members
if (!($definition['member'] instanceof StructureShape)
&& $definition['member']['locationName']
) {
$elementName = $definition['member']['locationName'];
}
$this->format(
$definition['member'],
$definition['member']['locationName'] ?: $k,
$elementName,
$definition['value'],
$xml
);
@ -157,11 +170,13 @@ class XmlBody
array $value,
XMLWriter $xml
) {
$xmlEntry = $shape['flattened'] ? $shape['locationName'] : 'entry';
$xmlEntry = $shape['flattened'] ? $name : 'entry';
$xmlKey = $shape->getKey()['locationName'] ?: 'key';
$xmlValue = $shape->getValue()['locationName'] ?: 'value';
if (!$shape['flattened']) {
$this->startElement($shape, $name, $xml);
}
foreach ($value as $key => $v) {
$this->startElement($shape, $xmlEntry, $xml);
@ -170,8 +185,10 @@ class XmlBody
$xml->endElement();
}
if (!$shape['flattened']) {
$xml->endElement();
}
}
private function add_blob(Shape $shape, $name, $value, XMLWriter $xml)
{
@ -217,4 +234,23 @@ class XmlBody
$this->defaultShape($shape, $name, $value, $xml);
}
}
private function determineRootElementName(Shape $shape): string
{
$shapeName = $shape->getName();
// Look up the shape definition first
if ($shapeName && $shapeMap = $shape->getShapeMap()) {
if (isset($shapeMap[$shapeName]['locationName'])) {
return $shapeMap[$shapeName]['locationName'];
}
}
// Fall back to shape's current locationName
if ($shape['locationName']) {
return $shape['locationName'];
}
return $shapeName;
}
}

View file

@ -4,7 +4,7 @@ namespace Aws\Api;
/**
* Builds shape based on shape references.
*/
class ShapeMap
class ShapeMap implements \ArrayAccess
{
/** @var array */
private $definitions;
@ -65,4 +65,45 @@ class ShapeMap
return $result;
}
/**
* @param mixed $offset
* @return bool
*/
public function offsetExists(mixed $offset): bool
{
return isset($this->definitions[$offset]);
}
/**
* @param mixed $offset
* @return mixed
*/
public function offsetGet(mixed $offset): mixed
{
return $this->definitions[$offset] ?? null;
}
/**
* @param mixed $offset
* @param mixed $value
* @throws \BadMethodCallException
*/
public function offsetSet(mixed $offset, mixed $value): void
{
throw new \BadMethodCallException(
'ShapeMap is read-only and cannot be modified.'
);
}
/**
* @param mixed $offset
* @throws \BadMethodCallException
*/
public function offsetUnset(mixed $offset): void
{
throw new \BadMethodCallException(
'ShapeMap is read-only and cannot be modified.'
);
}
}

View file

@ -67,6 +67,32 @@ class StructureShape extends Shape
return $members[$name];
}
/**
* Used to look up the shape's original definition.
* ShapeMap::resolve() merges properties from both
* member and target shape definitions, causing certain
* properties like `locationName` to be overwritten.
*
* @return ShapeMap
* @internal This method is for internal use only and should not be used
* by external code. It may be changed or removed without notice.
*/
public function getShapeMap(): ShapeMap
{
return $this->shapeMap;
}
/**
* Used to look up a shape's original definition.
*
* @param string $name
*
* @return array|null
*/
public function getOriginalDefinition(string $name): ?array
{
return $this->shapeMap[$name] ?? null;
}
private function generateMembersHash()
{

View file

@ -13,9 +13,9 @@ interface AuthSchemeResolverInterface
* Selects an auth scheme for request signing.
*
* @param array $authSchemes a priority-ordered list of authentication schemes.
* @param IdentityInterface $identity Credentials to be used in request signing.
* @param array $args
*
* @return string
* @return string|null
*/
public function selectAuthScheme(
array $authSchemes,

View file

@ -28,38 +28,50 @@ class AuthSelectionMiddleware
/** @var Service */
private $api;
/** @var array|null */
private ?array $configuredAuthSchemes;
/**
* Create a middleware wrapper function
*
* @param AuthSchemeResolverInterface $authResolver
* @param Service $api
* @param array|null $configuredAuthSchemes
*
* @return Closure
*/
public static function wrap(
AuthSchemeResolverInterface $authResolver,
Service $api
Service $api,
?array $configuredAuthSchemes
): Closure
{
return function (callable $handler) use ($authResolver, $api) {
return new self($handler, $authResolver, $api);
return function (callable $handler) use (
$authResolver,
$api,
$configuredAuthSchemes
) {
return new self($handler, $authResolver, $api, $configuredAuthSchemes);
};
}
/**
* @param callable $nextHandler
* @param $authResolver
* @param callable $identityProvider
* @param AuthSchemeResolverInterface $authResolver
* @param Service $api
* @param array|null $configuredAuthSchemes
*/
public function __construct(
callable $nextHandler,
AuthSchemeResolverInterface $authResolver,
Service $api
Service $api,
?array $configuredAuthSchemes = null
)
{
$this->nextHandler = $nextHandler;
$this->authResolver = $authResolver;
$this->api = $api;
$this->configuredAuthSchemes = $configuredAuthSchemes;
}
/**
@ -86,21 +98,62 @@ class AuthSelectionMiddleware
}
try {
$selectedAuthScheme = $resolver->selectAuthScheme(
$authSchemeList = $this->buildAuthSchemeList(
$resolvableAuth,
$command['@context']['auth_scheme_preference']
?? null,
);
$selectedAuthScheme = $resolver->selectAuthScheme(
$authSchemeList,
['unsigned_payload' => $unsignedPayload]
);
} catch (UnresolvedAuthSchemeException $e) {
// There was an error resolving auth
// The signature version will fall back to the modeled `signatureVersion`
// or auth schemes resolved during endpoint resolution
}
if (!empty($selectedAuthScheme)) {
$command['@context']['signature_version'] = $selectedAuthScheme;
}
} catch (UnresolvedAuthSchemeException $ignored) {
// There was an error resolving auth
// The signature version will fall back to the modeled `signatureVersion`
// or auth schemes resolved during endpoint resolution
}
}
return $nextHandler($command);
}
/**
* Prioritizes auth schemes according to user preference order.
* User-preferred schemes that are available will be placed first,
* followed by remaining available schemes.
*
* @param array $resolvableAuthSchemeList Available auth schemes
* @param array|null $commandConfiguredAuthSchemes Command-level preferences (overrides config)
*
* @return array Reordered auth schemes with user preferences first
*/
private function buildAuthSchemeList(
array $resolvableAuthSchemeList,
?array $commandConfiguredAuthSchemes,
): array
{
$userConfiguredAuthSchemes = $commandConfiguredAuthSchemes
?? $this->configuredAuthSchemes;
if (empty($userConfiguredAuthSchemes)) {
return $resolvableAuthSchemeList;
}
$prioritizedAuthSchemes = array_intersect(
$userConfiguredAuthSchemes,
$resolvableAuthSchemeList
);
// Get remaining schemes not in user preferences
$remainingAuthSchemes = array_diff(
$resolvableAuthSchemeList,
$prioritizedAuthSchemes
);
return array_merge($prioritizedAuthSchemes, $remainingAuthSchemes);
}
}

View file

@ -272,7 +272,7 @@ class AwsClient implements AwsClientInterface
if ($this->isUseEndpointV2()) {
$this->addEndpointV2Middleware();
}
$this->addAuthSelectionMiddleware();
$this->addAuthSelectionMiddleware($config);
if (!is_null($this->api->getMetadata('awsQueryCompatible'))) {
$this->addQueryCompatibleInputMiddleware($this->api);
@ -303,6 +303,12 @@ class AwsClient implements AwsClientInterface
return $fn();
}
public function getToken()
{
$fn = $this->tokenProvider;
return $fn();
}
public function getEndpoint()
{
@ -589,14 +595,15 @@ class AwsClient implements AwsClientInterface
);
}
private function addAuthSelectionMiddleware()
private function addAuthSelectionMiddleware(array $args)
{
$list = $this->getHandlerList();
$list->prependBuild(
AuthSelectionMiddleware::wrap(
$this->authSchemeResolver,
$this->getApi()
$this->getApi(),
$args['auth_scheme_preference'] ?? null
),
'auth-selection'
);

View file

@ -34,6 +34,7 @@ use Aws\Retry\ConfigurationProvider as RetryConfigProvider;
use Aws\Signature\SignatureProvider;
use Aws\Token\Token;
use Aws\Token\TokenInterface;
use Aws\Token\BedrockTokenProvider;
use Aws\Token\TokenProvider;
use GuzzleHttp\Promise\PromiseInterface;
use InvalidArgumentException as IAE;
@ -206,6 +207,13 @@ class ClientResolver
'fn' => [__CLASS__, '_apply_credentials'],
'default' => [__CLASS__, '_default_credential_provider'],
],
'auth_scheme_preference' => [
'type' => 'value',
'valid' => ['string', 'array'],
'doc' => 'Comma-separated list of authentication scheme preferences in priority order. Configure via environment variable `AWS_AUTH_SCHEME_PREFERENCE`, INI config file `auth_scheme_preference`, or client constructor parameter `auth_scheme_preference` (string or array).\nExample: `AWS_AUTH_SCHEME_PREFERENCE=aws.auth#sigv4a,aws.auth#sigv4,smithy.api#httpBearerAuth`',
'default' => self::DEFAULT_FROM_ENV_INI,
'fn' => [__CLASS__, '_apply_auth_scheme_preference'],
],
'token' => [
'type' => 'value',
'valid' => [TokenInterface::class, CacheInterface::class, 'array', 'bool', 'callable'],
@ -704,8 +712,17 @@ class ClientResolver
}
}
public static function _default_token_provider(array $args)
public static function _default_token_provider(array &$args)
{
if (($args['config']['signing_name'] ?? '') === 'bedrock') {
// Checks for env value, if present, sets auth_scheme_preference
// to bearer auth and returns a provider
$provider = BedrockTokenProvider::createIfAvailable($args);
if (!is_null($provider)) {
return $provider;
}
}
return TokenProvider::defaultProvider($args);
}
@ -1122,6 +1139,32 @@ class ClientResolver
return new AuthSchemeResolver($args['credentials'], $args['token']);
}
public static function _apply_auth_scheme_preference(
string|array|null &$value,
array &$args
): void
{
// Not provided user's preference auth scheme list
if (empty($value)) {
$value = null;
$args['config']['auth_scheme_preference'] = $value;
return;
}
// Normalize it as an array
if (is_string($value)) {
$value = explode(',', $value);
}
// Let`s trim each value to remove break lines, spaces and/or tabs
foreach ($value as &$val) {
$val = trim($val);
}
// Assign user's preferred auth scheme list
$args['auth_scheme_preference'] = $value;
}
public static function _default_signature_version(array &$args)
{
if (isset($args['config']['signature_version'])) {

View file

@ -68,7 +68,7 @@ class ConfigurationResolver
*
* @return null | mixed
*/
public static function env($key, $expectedType)
public static function env($key, $expectedType = 'string')
{
// Use config from environment variables, if available
$envValue = getenv(self::$envPrefix . strtoupper($key));
@ -203,6 +203,7 @@ class ConfigurationResolver
) {
$value = intVal($value);
}
return $value;
}

View file

@ -52,6 +52,7 @@ class CredentialProvider
const ENV_SESSION = 'AWS_SESSION_TOKEN';
const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE';
const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE';
public const REFRESH_WINDOW = 60;
/**
* Create a default credential provider that
@ -224,10 +225,14 @@ class CredentialProvider
return $creds;
}
// Refresh expired credentials.
if (!$creds->isExpired()) {
// Check if credentials are expired or will expire in 1 minute
$needsRefresh = $creds->getExpiration() - time() <= self::REFRESH_WINDOW;
// Refresh if expired or expiring soon
if (!$needsRefresh && !$creds->isExpired()) {
return $creds;
}
// Refresh the result and forward the promise.
return $result = $provider($creds);
})

View file

@ -142,8 +142,12 @@ final class Middleware
*
* @return callable
*/
public static function signer(callable $credProvider, callable $signatureFunction, $tokenProvider = null, $config = [])
{
public static function signer(
callable $credProvider,
callable $signatureFunction,
$tokenProvider = null,
$config = []
) {
return function (callable $handler) use ($signatureFunction, $credProvider, $tokenProvider, $config) {
return function (
CommandInterface $command,

View file

@ -51,9 +51,7 @@ class RequestCompressionMiddleware
}
$nextHandler = $this->nextHandler;
$operation = $this->api->getOperation($command->getName());
$compressionInfo = isset($operation['requestcompression'])
? $operation['requestcompression']
: null;
$compressionInfo = $operation['requestcompression'] ?? null;
if (!$this->shouldCompressRequestBody(
$compressionInfo,
@ -87,8 +85,12 @@ class RequestCompressionMiddleware
$body = $request->getBody()->getContents();
$compressedBody = $fn($body);
return $request->withBody(Psr7\Utils::streamFor($compressedBody))
->withHeader('content-encoding', $this->encoding);
$request = $request->withBody(Psr7\Utils::streamFor($compressedBody));
if ($request->hasHeader('Content-Encoding')) {
return $request->withAddedHeader('Content-Encoding', $this->encoding);
}
return $request->withHeader('Content-Encoding', $this->encoding);
}
private function determineEncoding()

View file

@ -2,6 +2,7 @@
namespace Aws\S3;
use Aws\CommandInterface;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;
/**
@ -16,22 +17,36 @@ class BucketEndpointMiddleware
{
private static $exclusions = ['GetBucketLocation' => true];
private $nextHandler;
private bool $useEndpointV2;
private ?string $endpoint;
/**
* Create a middleware wrapper function.
*
* @param bool $useEndpointV2
* @param string|null $endpoint
*
* @return callable
*/
public static function wrap()
public static function wrap(
bool $useEndpointV2 = false,
?string $endpoint = null
): callable
{
return function (callable $handler) {
return new self($handler);
return function (callable $handler) use ($useEndpointV2, $endpoint) {
return new self($handler, $useEndpointV2, $endpoint);
};
}
public function __construct(callable $nextHandler)
public function __construct(
callable $nextHandler,
bool $useEndpointV2,
?string $endpoint = null
)
{
$this->nextHandler = $nextHandler;
$this->useEndpointV2 = $useEndpointV2;
$this->endpoint = $endpoint;
}
public function __invoke(CommandInterface $command, RequestInterface $request)
@ -47,74 +62,50 @@ class BucketEndpointMiddleware
}
/**
* Performs a one-time removal of Bucket from path, then if
* the bucket name is duplicated in the path, performs additional
* removal which is dependent on the number of occurrences of the bucket
* name in a path-like format in the key name.
* @param string $path
* @param string $bucket
*
* @return string
*/
private function removeBucketFromPath($path, $bucket, $key)
private function removeBucketFromPath(string $path, string $bucket): string
{
$occurrencesInKey = $this->getBucketNameOccurrencesInKey($key, $bucket);
do {
$len = strlen($bucket) + 1;
if (substr($path, 0, $len) === "/{$bucket}") {
if (str_starts_with($path, "/{$bucket}")) {
$path = substr($path, $len);
}
} while (substr_count($path, "/{$bucket}") > $occurrencesInKey + 1);
return $path ?: '/';
}
private function removeDuplicateBucketFromHost($host, $bucket)
{
if (substr_count($host, $bucket) > 1) {
while (strpos($host, "{$bucket}.{$bucket}") === 0) {
$hostArr = explode('.', $host);
array_shift($hostArr);
$host = implode('.', $hostArr);
}
}
return $host;
}
private function getBucketNameOccurrencesInKey($key, $bucket)
{
$occurrences = 0;
if (empty($key)) {
return $occurrences;
}
$segments = explode('/', $key);
foreach($segments as $segment) {
if (strpos($segment, $bucket) === 0) {
$occurrences++;
}
}
return $occurrences;
}
/**
* @param RequestInterface $request
* @param CommandInterface $command
*
* @return RequestInterface
*/
private function modifyRequest(
RequestInterface $request,
CommandInterface $command
) {
$key = isset($command['Key']) ? $command['Key'] : null;
): RequestInterface
{
$uri = $request->getUri();
$path = $uri->getPath();
$host = $uri->getHost();
$bucket = $command['Bucket'];
$path = $this->removeBucketFromPath($path, $bucket, $key);
$host = $this->removeDuplicateBucketFromHost($host, $bucket);
if ($this->useEndpointV2 && !empty($this->endpoint)) {
// V2 provider adds bucket name to host by default
// preserve original host
$host = (new Uri($this->endpoint))->getHost();
}
$path = $this->removeBucketFromPath($path, $bucket);
// Modify the Key to make sure the key is encoded, but slashes are not.
if ($key) {
if ($command['Key']) {
$path = S3Client::encodeKey(rawurldecode($path));
}
return $request->withUri(
$uri->withHost($host)
->withPath($path)
);
return $request->withUri($uri->withPath($path)->withHost($host));
}
}

View file

@ -41,6 +41,8 @@ use Psr\Http\Message\RequestInterface;
* @method \GuzzleHttp\Promise\Promise copyObjectAsync(array $args = [])
* @method \Aws\Result createBucket(array $args = [])
* @method \GuzzleHttp\Promise\Promise createBucketAsync(array $args = [])
* @method \Aws\Result createBucketMetadataConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise createBucketMetadataConfigurationAsync(array $args = [])
* @method \Aws\Result createBucketMetadataTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise createBucketMetadataTableConfigurationAsync(array $args = [])
* @method \Aws\Result createMultipartUpload(array $args = [])
@ -61,6 +63,8 @@ use Psr\Http\Message\RequestInterface;
* @method \GuzzleHttp\Promise\Promise deleteBucketInventoryConfigurationAsync(array $args = [])
* @method \Aws\Result deleteBucketLifecycle(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteBucketLifecycleAsync(array $args = [])
* @method \Aws\Result deleteBucketMetadataConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteBucketMetadataConfigurationAsync(array $args = [])
* @method \Aws\Result deleteBucketMetadataTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteBucketMetadataTableConfigurationAsync(array $args = [])
* @method \Aws\Result deleteBucketMetricsConfiguration(array $args = [])
@ -105,6 +109,8 @@ use Psr\Http\Message\RequestInterface;
* @method \GuzzleHttp\Promise\Promise getBucketLocationAsync(array $args = [])
* @method \Aws\Result getBucketLogging(array $args = [])
* @method \GuzzleHttp\Promise\Promise getBucketLoggingAsync(array $args = [])
* @method \Aws\Result getBucketMetadataConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise getBucketMetadataConfigurationAsync(array $args = [])
* @method \Aws\Result getBucketMetadataTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise getBucketMetadataTableConfigurationAsync(array $args = [])
* @method \Aws\Result getBucketMetricsConfiguration(array $args = [])
@ -233,6 +239,10 @@ use Psr\Http\Message\RequestInterface;
* @method \GuzzleHttp\Promise\Promise restoreObjectAsync(array $args = [])
* @method \Aws\Result selectObjectContent(array $args = [])
* @method \GuzzleHttp\Promise\Promise selectObjectContentAsync(array $args = [])
* @method \Aws\Result updateBucketMetadataInventoryTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateBucketMetadataInventoryTableConfigurationAsync(array $args = [])
* @method \Aws\Result updateBucketMetadataJournalTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateBucketMetadataJournalTableConfigurationAsync(array $args = [])
* @method \Aws\Result uploadPart(array $args = [])
* @method \GuzzleHttp\Promise\Promise uploadPartAsync(array $args = [])
* @method \Aws\Result uploadPartCopy(array $args = [])
@ -422,6 +432,7 @@ class S3Client extends AwsClient implements S3ClientInterface
$this->addBuiltIns($args);
parent::__construct($args);
$stack = $this->getHandlerList();
$config = $this->getConfig();
$stack->appendInit(SSECMiddleware::wrap($this->getEndpoint()->getScheme()), 's3.ssec');
$stack->appendBuild(
ApplyChecksumMiddleware::wrap($this->getApi(), $this->getConfig()),
@ -433,7 +444,9 @@ class S3Client extends AwsClient implements S3ClientInterface
);
if ($this->getConfig('bucket_endpoint')) {
$stack->appendBuild(BucketEndpointMiddleware::wrap(), 's3.bucket_endpoint');
$stack->appendBuild(BucketEndpointMiddleware::wrap(
$this->isUseEndpointV2(), $args['endpoint'] ?? null), 's3.bucket_endpoint'
);
} elseif (!$this->isUseEndpointV2()) {
$stack->appendBuild(
S3EndpointMiddleware::wrap(
@ -916,6 +929,10 @@ class S3Client extends AwsClient implements S3ClientInterface
$requestUri = str_replace('/{Bucket}', '/', $requestUri);
} else {
$requestUri = str_replace('/{Bucket}', '', $requestUri);
// If we're left with just a query string, prepend '/'
if (str_starts_with($requestUri, '?')) {
$requestUri = '/' . $requestUri;
}
}
$operation['http']['requestUri'] = $requestUri;
}
@ -924,7 +941,7 @@ class S3Client extends AwsClient implements S3ClientInterface
foreach ($definition['shapes'] as $key => &$value) {
$suffix = 'Output';
if (substr($key, -strlen($suffix)) === $suffix) {
if (str_ends_with($key, $suffix)) {
if (isset($value['members']['Expires'])) {
$value['members']['Expires']['deprecated'] = true;
$value['members']['ExpiresString'] = [

View file

@ -20,6 +20,8 @@ use GuzzleHttp\Promise;
* @method \GuzzleHttp\Promise\Promise copyObjectAsync(array $args = [])
* @method \Aws\Result createBucket(array $args = [])
* @method \GuzzleHttp\Promise\Promise createBucketAsync(array $args = [])
* @method \Aws\Result createBucketMetadataConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise createBucketMetadataConfigurationAsync(array $args = [])
* @method \Aws\Result createBucketMetadataTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise createBucketMetadataTableConfigurationAsync(array $args = [])
* @method \Aws\Result createMultipartUpload(array $args = [])
@ -40,6 +42,8 @@ use GuzzleHttp\Promise;
* @method \GuzzleHttp\Promise\Promise deleteBucketInventoryConfigurationAsync(array $args = [])
* @method \Aws\Result deleteBucketLifecycle(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteBucketLifecycleAsync(array $args = [])
* @method \Aws\Result deleteBucketMetadataConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteBucketMetadataConfigurationAsync(array $args = [])
* @method \Aws\Result deleteBucketMetadataTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteBucketMetadataTableConfigurationAsync(array $args = [])
* @method \Aws\Result deleteBucketMetricsConfiguration(array $args = [])
@ -84,6 +88,8 @@ use GuzzleHttp\Promise;
* @method \GuzzleHttp\Promise\Promise getBucketLocationAsync(array $args = [])
* @method \Aws\Result getBucketLogging(array $args = [])
* @method \GuzzleHttp\Promise\Promise getBucketLoggingAsync(array $args = [])
* @method \Aws\Result getBucketMetadataConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise getBucketMetadataConfigurationAsync(array $args = [])
* @method \Aws\Result getBucketMetadataTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise getBucketMetadataTableConfigurationAsync(array $args = [])
* @method \Aws\Result getBucketMetricsConfiguration(array $args = [])
@ -212,6 +218,10 @@ use GuzzleHttp\Promise;
* @method \GuzzleHttp\Promise\Promise restoreObjectAsync(array $args = [])
* @method \Aws\Result selectObjectContent(array $args = [])
* @method \GuzzleHttp\Promise\Promise selectObjectContentAsync(array $args = [])
* @method \Aws\Result updateBucketMetadataInventoryTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateBucketMetadataInventoryTableConfigurationAsync(array $args = [])
* @method \Aws\Result updateBucketMetadataJournalTableConfiguration(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateBucketMetadataJournalTableConfigurationAsync(array $args = [])
* @method \Aws\Result uploadPart(array $args = [])
* @method \GuzzleHttp\Promise\Promise uploadPartAsync(array $args = [])
* @method \Aws\Result uploadPartCopy(array $args = [])

View file

@ -259,7 +259,7 @@ class StreamWrapper
$this->initProtocol($path);
// Some paths come through as S3:// for some reason.
$split = explode('://', $path);
$split = explode('://', $path, 2);
$path = strtolower($split[0]) . '://' . $split[1];
// Check if this path is in the url_stat cache
@ -703,7 +703,7 @@ class StreamWrapper
private function getBucketKey($path)
{
// Remove the protocol
$parts = explode('://', $path);
$parts = explode('://', $path, 2);
// Get the bucket, key
$parts = explode('/', $parts[1], 2);

View file

@ -8,6 +8,8 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionACMPCA(array $args = [])
* @method \Aws\AIOps\AIOpsClient createAIOps(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionAIOps(array $args = [])
* @method \Aws\ARCRegionSwitch\ARCRegionSwitchClient createARCRegionSwitch(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionARCRegionSwitch(array $args = [])
* @method \Aws\ARCZonalShift\ARCZonalShiftClient createARCZonalShift(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionARCZonalShift(array $args = [])
* @method \Aws\AccessAnalyzer\AccessAnalyzerClient createAccessAnalyzer(array $args = [])
@ -74,10 +76,14 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionAutoScalingPlans(array $args = [])
* @method \Aws\B2bi\B2biClient createB2bi(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionB2bi(array $args = [])
* @method \Aws\BCMDashboards\BCMDashboardsClient createBCMDashboards(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBCMDashboards(array $args = [])
* @method \Aws\BCMDataExports\BCMDataExportsClient createBCMDataExports(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBCMDataExports(array $args = [])
* @method \Aws\BCMPricingCalculator\BCMPricingCalculatorClient createBCMPricingCalculator(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBCMPricingCalculator(array $args = [])
* @method \Aws\BCMRecommendedActions\BCMRecommendedActionsClient createBCMRecommendedActions(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBCMRecommendedActions(array $args = [])
* @method \Aws\Backup\BackupClient createBackup(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBackup(array $args = [])
* @method \Aws\BackupGateway\BackupGatewayClient createBackupGateway(array $args = [])
@ -90,6 +96,10 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionBedrock(array $args = [])
* @method \Aws\BedrockAgent\BedrockAgentClient createBedrockAgent(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBedrockAgent(array $args = [])
* @method \Aws\BedrockAgentCore\BedrockAgentCoreClient createBedrockAgentCore(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBedrockAgentCore(array $args = [])
* @method \Aws\BedrockAgentCoreControl\BedrockAgentCoreControlClient createBedrockAgentCoreControl(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBedrockAgentCoreControl(array $args = [])
* @method \Aws\BedrockAgentRuntime\BedrockAgentRuntimeClient createBedrockAgentRuntime(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionBedrockAgentRuntime(array $args = [])
* @method \Aws\BedrockDataAutomation\BedrockDataAutomationClient createBedrockDataAutomation(array $args = [])
@ -408,6 +418,8 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionKendraRanking(array $args = [])
* @method \Aws\Keyspaces\KeyspacesClient createKeyspaces(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionKeyspaces(array $args = [])
* @method \Aws\KeyspacesStreams\KeyspacesStreamsClient createKeyspacesStreams(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionKeyspacesStreams(array $args = [])
* @method \Aws\Kinesis\KinesisClient createKinesis(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionKinesis(array $args = [])
* @method \Aws\KinesisAnalytics\KinesisAnalyticsClient createKinesisAnalytics(array $args = [])
@ -548,16 +560,14 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionOSIS(array $args = [])
* @method \Aws\ObservabilityAdmin\ObservabilityAdminClient createObservabilityAdmin(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionObservabilityAdmin(array $args = [])
* @method \Aws\Odb\OdbClient createOdb(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOdb(array $args = [])
* @method \Aws\Omics\OmicsClient createOmics(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOmics(array $args = [])
* @method \Aws\OpenSearchServerless\OpenSearchServerlessClient createOpenSearchServerless(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOpenSearchServerless(array $args = [])
* @method \Aws\OpenSearchService\OpenSearchServiceClient createOpenSearchService(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOpenSearchService(array $args = [])
* @method \Aws\OpsWorks\OpsWorksClient createOpsWorks(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOpsWorks(array $args = [])
* @method \Aws\OpsWorksCM\OpsWorksCMClient createOpsWorksCM(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOpsWorksCM(array $args = [])
* @method \Aws\Organizations\OrganizationsClient createOrganizations(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionOrganizations(array $args = [])
* @method \Aws\Outposts\OutpostsClient createOutposts(array $args = [])
@ -666,6 +676,8 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionS3Outposts(array $args = [])
* @method \Aws\S3Tables\S3TablesClient createS3Tables(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionS3Tables(array $args = [])
* @method \Aws\S3Vectors\S3VectorsClient createS3Vectors(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionS3Vectors(array $args = [])
* @method \Aws\SSMContacts\SSMContactsClient createSSMContacts(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionSSMContacts(array $args = [])
* @method \Aws\SSMGuiConnect\SSMGuiConnectClient createSSMGuiConnect(array $args = [])
@ -800,6 +812,8 @@ namespace Aws;
* @method \Aws\MultiRegionClient createMultiRegionWorkSpacesThinClient(array $args = [])
* @method \Aws\WorkSpacesWeb\WorkSpacesWebClient createWorkSpacesWeb(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionWorkSpacesWeb(array $args = [])
* @method \Aws\WorkspacesInstances\WorkspacesInstancesClient createWorkspacesInstances(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionWorkspacesInstances(array $args = [])
* @method \Aws\XRay\XRayClient createXRay(array $args = [])
* @method \Aws\MultiRegionClient createMultiRegionXRay(array $args = [])
* @method \Aws\drs\drsClient createdrs(array $args = [])
@ -819,7 +833,7 @@ namespace Aws;
*/
class Sdk
{
const VERSION = '3.346.2';
const VERSION = '3.356.8';
/** @var array Arguments for creating clients */
private $args;

View file

@ -73,7 +73,7 @@ class StreamRequestPayloadMiddleware
. ' calculated.');
}
$request = $request->withHeader(
'content-length',
'Content-Length',
$size
);
}

View file

@ -45,7 +45,7 @@ use GuzzleHttp\Promise\PromiseInterface;
class ConfigurationProvider extends AbstractConfigurationProvider
implements ConfigurationProviderInterface
{
const DEFAULT_ENDPOINTS_TYPE = 'legacy';
const DEFAULT_ENDPOINTS_TYPE = 'regional';
const ENV_ENDPOINTS_TYPE = 'AWS_STS_REGIONAL_ENDPOINTS';
const ENV_PROFILE = 'AWS_PROFILE';
const INI_ENDPOINTS_TYPE = 'sts_regional_endpoints';

View file

@ -0,0 +1,102 @@
<?php
namespace Aws\Token;
use Aws\Configuration\ConfigurationResolver;
use Aws\Exception\TokenException;
use GuzzleHttp\Promise;
/**
* Token provider for Bedrock that sources bearer tokens from environment variables.
*/
class BedrockTokenProvider extends TokenProvider
{
/** @var string used to resolve the AWS_BEARER_TOKEN_BEDROCK env var */
public const TOKEN_ENV_KEY = 'bearer_token_bedrock';
public const BEARER_AUTH = 'smithy.api#httpBearerAuth';
/**
* Create a default Bedrock token provider that checks for a bearer token
* in the AWS_BEARER_TOKEN_BEDROCK environment variable.
*
* This provider is automatically wrapped in a memoize function that caches
* previously provided tokens.
*
* @param array $config Optional array of token provider options.
*
* @return callable
*/
public static function defaultProvider(array $config = []): callable
{
$defaultChain = ['env' => self::env(self::TOKEN_ENV_KEY)];
return self::memoize(
call_user_func_array(
[TokenProvider::class, 'chain'],
array_values($defaultChain)
)
);
}
/**
* Token provider that creates a token from an environment variable.
*
* @param string $configKey The configuration key that will be transformed
* to an environment variable name by ConfigurationResolver
*
* @return callable
*/
public static function env(string $configKey): callable
{
return static function () use ($configKey) {
$tokenValue = ConfigurationResolver::env($configKey);
if (empty($tokenValue)) {
return Promise\Create::rejectionFor(
new TokenException(
"No token found in environment variable " .
ConfigurationResolver::$envPrefix . strtoupper($configKey)
)
);
}
return Promise\Create::promiseFor(new Token($tokenValue));
};
}
/**
* Create a token provider from a raw token value string.
* Bedrock bearer tokens sourced from env do not have an expiration
*
* @param string $tokenValue The bearer token value
*
* @return callable
*/
public static function fromTokenValue(string $tokenValue): callable
{
$token = new Token($tokenValue);
return self::fromToken($token);
}
/**
* Create a Bedrock token provider if the service is 'bedrock' and a token is available.
* Sets auth scheme preference to `bearer` auth.
*
* @param array $args Configuration arguments containing 'config' array
*
* @return callable|null Returns a token provider if conditions are met, null otherwise
*/
public static function createIfAvailable(array &$args): ?callable
{
$tokenValue = ConfigurationResolver::env(self::TOKEN_ENV_KEY);
if ($tokenValue) {
$authSchemePreference = $args['config']['auth_scheme_preference'] ?? [];
array_unshift($authSchemePreference, self::BEARER_AUTH);
$args['config']['auth_scheme_preference'] = $authSchemePreference;
return self::fromTokenValue($tokenValue);
}
return null;
}
}

View file

@ -2,7 +2,6 @@
namespace Aws\Token;
use Aws\Identity\BearerTokenIdentity;
use Aws\Token\TokenInterface;
/**
* Basic implementation of the AWS Token interface that allows callers to

View file

@ -2,7 +2,6 @@
namespace Aws\Token;
use Aws;
use Aws\Api\DateTimeResult;
use Aws\CacheInterface;
use Aws\Exception\TokenException;
use GuzzleHttp\Promise;
@ -28,9 +27,10 @@ use GuzzleHttp\Promise;
*/
class TokenProvider
{
use ParsesIniTrait;
const ENV_PROFILE = 'AWS_PROFILE';
use ParsesIniTrait;
/**
* Create a default token provider tha checks for cached a SSO token from
* the CLI
@ -44,15 +44,13 @@ class TokenProvider
*/
public static function defaultProvider(array $config = [])
{
$cacheable = [
'sso',
];
$defaultChain = [];
if (
!isset($config['use_aws_shared_config_files'])
if (!isset($config['use_aws_shared_config_files'])
|| $config['use_aws_shared_config_files'] !== false
) {
$profileName = getenv(self::ENV_PROFILE) ?: 'default';
@ -79,7 +77,7 @@ class TokenProvider
return self::memoize(
call_user_func_array(
[TokenProvider::class, 'chain'],
[__CLASS__, 'chain'],
array_values($defaultChain)
)
);
@ -96,7 +94,7 @@ class TokenProvider
{
$promise = Promise\Create::promiseFor($token);
return function () use ($promise) {
return static function () use ($promise) {
return $promise;
};
}
@ -113,12 +111,12 @@ class TokenProvider
$links = func_get_args();
//Common use case for when aws_shared_config_files is false
if (empty($links)) {
return function () {
return static function () {
return Promise\Create::promiseFor(false);
};
}
return function () use ($links) {
return static function () use ($links) {
/** @var callable $parent */
$parent = array_shift($links);
$promise = $parent();
@ -138,7 +136,7 @@ class TokenProvider
*/
public static function memoize(callable $provider)
{
return function () use ($provider) {
return static function () use ($provider) {
static $result;
static $isConstant;
@ -193,7 +191,7 @@ class TokenProvider
){
$cacheKey = $cacheKey ?: 'aws_cached_token';
return function () use ($provider, $cache, $cacheKey) {
return static function () use ($provider, $cache, $cacheKey) {
$found = $cache->get($cacheKey);
if (is_array($found) && isset($found['token'])) {
$foundToken = $found['token'];
@ -214,7 +212,7 @@ class TokenProvider
) {
$cache->set(
$cacheKey,
$token,
['token' => $token],
null === $token->getExpiration() ?
0 : $token->getExpiration() - time()
);
@ -227,7 +225,8 @@ class TokenProvider
/**
* Gets profiles from the ~/.aws/config ini file
*/
private static function loadDefaultProfiles() {
private static function loadDefaultProfiles()
{
$profiles = [];
$configFile = self::getHomeDir() . '/.aws/config';
@ -260,11 +259,13 @@ class TokenProvider
* @return SsoTokenProvider
* @see Aws\Token\SsoTokenProvider for $config details.
*/
public static function sso($profileName, $filename, $config = [])
{
$ssoClient = isset($config['ssoClient']) ? $config['ssoClient'] : null;
public static function sso(
$profileName,
$filename,
$config = []
){
$ssoClient = $config['ssoClient'] ?? null;
return new SsoTokenProvider($profileName, $filename, $ssoClient);
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,3 @@
<?php
// This file was auto-generated from sdk-root/src/data/grandfathered-services.json
return [ 'grandfathered-services' => [ 'AccessAnalyzer', 'Account', 'ACMPCA', 'ACM', 'PrometheusService', 'Amplify', 'AmplifyBackend', 'AmplifyUIBuilder', 'APIGateway', 'ApiGatewayManagementApi', 'ApiGatewayV2', 'AppConfig', 'AppConfigData', 'Appflow', 'AppIntegrationsService', 'ApplicationAutoScaling', 'ApplicationInsights', 'ApplicationCostProfiler', 'AppMesh', 'AppRunner', 'AppStream', 'AppSync', 'Athena', 'AuditManager', 'AutoScalingPlans', 'AutoScaling', 'BackupGateway', 'Backup', 'Batch', 'BillingConductor', 'Braket', 'Budgets', 'CostExplorer', 'ChimeSDKIdentity', 'ChimeSDKMediaPipelines', 'ChimeSDKMeetings', 'ChimeSDKMessaging', 'Chime', 'Cloud9', 'CloudControlApi', 'CloudDirectory', 'CloudFormation', 'CloudFront', 'CloudHSM', 'CloudHSMV2', 'CloudSearch', 'CloudSearchDomain', 'CloudTrail', 'CodeArtifact', 'CodeBuild', 'CodeCommit', 'CodeDeploy', 'CodeGuruReviewer', 'CodeGuruProfiler', 'CodePipeline', 'CodeStarconnections', 'CodeStarNotifications', 'CodeStar', 'CognitoIdentity', 'CognitoIdentityProvider', 'CognitoSync', 'Comprehend', 'ComprehendMedical', 'ComputeOptimizer', 'ConfigService', 'ConnectContactLens', 'Connect', 'ConnectCampaignService', 'ConnectParticipant', 'CostandUsageReportService', 'CustomerProfiles', 'IoTDataPlane', 'GlueDataBrew', 'DataExchange', 'DataPipeline', 'DataSync', 'DAX', 'Detective', 'DeviceFarm', 'DevOpsGuru', 'DirectConnect', 'ApplicationDiscoveryService', 'DLM', 'DatabaseMigrationService', 'DocDB', 'drs', 'DirectoryService', 'DynamoDB', 'EBS', 'EC2InstanceConnect', 'EC2', 'ECRPublic', 'ECR', 'ECS', 'EKS', 'ElastiCache', 'ElasticBeanstalk', 'EFS', 'ElasticLoadBalancing', 'ElasticLoadBalancingv2', 'EMR', 'ElasticTranscoder', 'SES', 'EMRContainers', 'EMRServerless', 'MarketplaceEntitlementService', 'ElasticsearchService', 'EventBridge', 'CloudWatchEvents', 'CloudWatchEvidently', 'FinSpaceData', 'finspace', 'Firehose', 'FIS', 'FMS', 'ForecastService', 'ForecastQueryService', 'FraudDetector', 'FSx', 'GameLift', 'Glacier', 'GlobalAccelerator', 'Glue', 'ManagedGrafana', 'Greengrass', 'GreengrassV2', 'GroundStation', 'GuardDuty', 'Health', 'HealthLake', 'IAM', 'IdentityStore', 'imagebuilder', 'ImportExport', 'Inspector', 'Inspector2', 'IoTJobsDataPlane', 'IoT', 'IoTAnalytics', 'IoTDeviceAdvisor', 'IoTEventsData', 'IoTEvents', 'IoTFleetHub', 'IoTSecureTunneling', 'IoTSiteWise', 'IoTThingsGraph', 'IoTTwinMaker', 'IoTWireless', 'IVS', 'ivschat', 'Kafka', 'KafkaConnect', 'kendra', 'Keyspaces', 'KinesisVideoArchivedMedia', 'KinesisVideoMedia', 'KinesisVideoSignalingChannels', 'Kinesis', 'KinesisAnalytics', 'KinesisAnalyticsV2', 'KinesisVideo', 'KMS', 'LakeFormation', 'Lambda', 'LexModelBuildingService', 'LicenseManager', 'Lightsail', 'LocationService', 'CloudWatchLogs', 'LookoutEquipment', 'LookoutMetrics', 'LookoutforVision', 'MainframeModernization', 'MachineLearning', 'Macie2', 'ManagedBlockchain', 'MarketplaceCatalog', 'MarketplaceCommerceAnalytics', 'MediaConnect', 'MediaConvert', 'MediaLive', 'MediaPackageVod', 'MediaPackage', 'MediaStoreData', 'MediaStore', 'MediaTailor', 'MemoryDB', 'MarketplaceMetering', 'MigrationHub', 'mgn', 'MigrationHubRefactorSpaces', 'MigrationHubConfig', 'MigrationHubStrategyRecommendations', 'LexModelsV2', 'CloudWatch', 'MQ', 'MTurk', 'MWAA', 'Neptune', 'NetworkFirewall', 'NetworkManager', 'OpenSearchService', 'OpsWorks', 'OpsWorksCM', 'Organizations', 'Outposts', 'Panorama', 'PersonalizeEvents', 'PersonalizeRuntime', 'Personalize', 'PI', 'PinpointEmail', 'PinpointSMSVoiceV2', 'Pinpoint', 'Polly', 'Pricing', 'Proton', 'QLDBSession', 'QLDB', 'QuickSight', 'RAM', 'RecycleBin', 'RDSDataService', 'RDS', 'RedshiftDataAPIService', 'RedshiftServerless', 'Redshift', 'Rekognition', 'ResilienceHub', 'ResourceGroups', 'ResourceGroupsTaggingAPI', 'RoboMaker', 'Route53RecoveryCluster', 'Route53RecoveryControlConfig', 'Route53RecoveryReadiness', 'Route53', 'Route53Domains', 'Route53Resolver', 'CloudWatchRUM', 'LexRuntimeV2', 'LexRuntimeService', 'SageMakerRuntime', 'S3', 'S3Control', 'S3Outposts', 'AugmentedAIRuntime', 'SagemakerEdgeManager', 'SageMakerFeatureStoreRuntime', 'SageMaker', 'SavingsPlans', 'Schemas', 'SecretsManager', 'SecurityHub', 'ServerlessApplicationRepository', 'ServiceQuotas', 'AppRegistry', 'ServiceCatalog', 'ServiceDiscovery', 'SESV2', 'Shield', 'signer', 'PinpointSMSVoice', 'SMS', 'SnowDeviceManagement', 'Snowball', 'SNS', 'SQS', 'SSMContacts', 'SSMIncidents', 'SSM', 'SSOAdmin', 'SSOOIDC', 'SSO', 'SFN', 'StorageGateway', 'DynamoDBStreams', 'STS', 'Support', 'SWF', 'Synthetics', 'Textract', 'TimestreamQuery', 'TimestreamWrite', 'TranscribeService', 'Transfer', 'Translate', 'VoiceID', 'WAFRegional', 'WAF', 'WAFV2', 'WellArchitected', 'ConnectWisdomService', 'WorkDocs', 'WorkMail', 'WorkMailMessageFlow', 'WorkSpacesWeb', 'WorkSpaces', 'XRay', ],];
return [ 'grandfathered-services' => [ 'AccessAnalyzer', 'Account', 'ACMPCA', 'ACM', 'PrometheusService', 'Amplify', 'AmplifyBackend', 'AmplifyUIBuilder', 'APIGateway', 'ApiGatewayManagementApi', 'ApiGatewayV2', 'AppConfig', 'AppConfigData', 'Appflow', 'AppIntegrationsService', 'ApplicationAutoScaling', 'ApplicationInsights', 'ApplicationCostProfiler', 'AppMesh', 'AppRunner', 'AppStream', 'AppSync', 'Athena', 'AuditManager', 'AutoScalingPlans', 'AutoScaling', 'BackupGateway', 'Backup', 'Batch', 'BillingConductor', 'Braket', 'Budgets', 'CostExplorer', 'ChimeSDKIdentity', 'ChimeSDKMediaPipelines', 'ChimeSDKMeetings', 'ChimeSDKMessaging', 'Chime', 'Cloud9', 'CloudControlApi', 'CloudDirectory', 'CloudFormation', 'CloudFront', 'CloudHSM', 'CloudHSMV2', 'CloudSearch', 'CloudSearchDomain', 'CloudTrail', 'CodeArtifact', 'CodeBuild', 'CodeCommit', 'CodeDeploy', 'CodeGuruReviewer', 'CodeGuruProfiler', 'CodePipeline', 'CodeStarconnections', 'CodeStarNotifications', 'CodeStar', 'CognitoIdentity', 'CognitoIdentityProvider', 'CognitoSync', 'Comprehend', 'ComprehendMedical', 'ComputeOptimizer', 'ConfigService', 'ConnectContactLens', 'Connect', 'ConnectCampaignService', 'ConnectParticipant', 'CostandUsageReportService', 'CustomerProfiles', 'IoTDataPlane', 'GlueDataBrew', 'DataExchange', 'DataPipeline', 'DataSync', 'DAX', 'Detective', 'DeviceFarm', 'DevOpsGuru', 'DirectConnect', 'ApplicationDiscoveryService', 'DLM', 'DatabaseMigrationService', 'DocDB', 'drs', 'DirectoryService', 'DynamoDB', 'EBS', 'EC2InstanceConnect', 'EC2', 'ECRPublic', 'ECR', 'ECS', 'EKS', 'ElastiCache', 'ElasticBeanstalk', 'EFS', 'ElasticLoadBalancing', 'ElasticLoadBalancingv2', 'EMR', 'ElasticTranscoder', 'SES', 'EMRContainers', 'EMRServerless', 'MarketplaceEntitlementService', 'ElasticsearchService', 'EventBridge', 'CloudWatchEvents', 'CloudWatchEvidently', 'FinSpaceData', 'finspace', 'Firehose', 'FIS', 'FMS', 'ForecastService', 'ForecastQueryService', 'FraudDetector', 'FSx', 'GameLift', 'Glacier', 'GlobalAccelerator', 'Glue', 'ManagedGrafana', 'Greengrass', 'GreengrassV2', 'GroundStation', 'GuardDuty', 'Health', 'HealthLake', 'IAM', 'IdentityStore', 'imagebuilder', 'ImportExport', 'Inspector', 'Inspector2', 'IoTJobsDataPlane', 'IoT', 'IoTAnalytics', 'IoTDeviceAdvisor', 'IoTEventsData', 'IoTEvents', 'IoTFleetHub', 'IoTSecureTunneling', 'IoTSiteWise', 'IoTThingsGraph', 'IoTTwinMaker', 'IoTWireless', 'IVS', 'ivschat', 'Kafka', 'KafkaConnect', 'kendra', 'Keyspaces', 'KinesisVideoArchivedMedia', 'KinesisVideoMedia', 'KinesisVideoSignalingChannels', 'Kinesis', 'KinesisAnalytics', 'KinesisAnalyticsV2', 'KinesisVideo', 'KMS', 'LakeFormation', 'Lambda', 'LexModelBuildingService', 'LicenseManager', 'Lightsail', 'LocationService', 'CloudWatchLogs', 'LookoutEquipment', 'LookoutMetrics', 'LookoutforVision', 'MainframeModernization', 'MachineLearning', 'Macie2', 'ManagedBlockchain', 'MarketplaceCatalog', 'MarketplaceCommerceAnalytics', 'MediaConnect', 'MediaConvert', 'MediaLive', 'MediaPackageVod', 'MediaPackage', 'MediaStoreData', 'MediaStore', 'MediaTailor', 'MemoryDB', 'MarketplaceMetering', 'MigrationHub', 'mgn', 'MigrationHubRefactorSpaces', 'MigrationHubConfig', 'MigrationHubStrategyRecommendations', 'LexModelsV2', 'CloudWatch', 'MQ', 'MTurk', 'MWAA', 'Neptune', 'NetworkFirewall', 'NetworkManager', 'OpenSearchService', 'Organizations', 'Outposts', 'Panorama', 'PersonalizeEvents', 'PersonalizeRuntime', 'Personalize', 'PI', 'PinpointEmail', 'PinpointSMSVoiceV2', 'Pinpoint', 'Polly', 'Pricing', 'Proton', 'QLDBSession', 'QLDB', 'QuickSight', 'RAM', 'RecycleBin', 'RDSDataService', 'RDS', 'RedshiftDataAPIService', 'RedshiftServerless', 'Redshift', 'Rekognition', 'ResilienceHub', 'ResourceGroups', 'ResourceGroupsTaggingAPI', 'RoboMaker', 'Route53RecoveryCluster', 'Route53RecoveryControlConfig', 'Route53RecoveryReadiness', 'Route53', 'Route53Domains', 'Route53Resolver', 'CloudWatchRUM', 'LexRuntimeV2', 'LexRuntimeService', 'SageMakerRuntime', 'S3', 'S3Control', 'S3Outposts', 'AugmentedAIRuntime', 'SagemakerEdgeManager', 'SageMakerFeatureStoreRuntime', 'SageMaker', 'SavingsPlans', 'Schemas', 'SecretsManager', 'SecurityHub', 'ServerlessApplicationRepository', 'ServiceQuotas', 'AppRegistry', 'ServiceCatalog', 'ServiceDiscovery', 'SESV2', 'Shield', 'signer', 'PinpointSMSVoice', 'SMS', 'SnowDeviceManagement', 'Snowball', 'SNS', 'SQS', 'SSMContacts', 'SSMIncidents', 'SSM', 'SSOAdmin', 'SSOOIDC', 'SSO', 'SFN', 'StorageGateway', 'DynamoDBStreams', 'STS', 'Support', 'SWF', 'Synthetics', 'Textract', 'TimestreamQuery', 'TimestreamWrite', 'TranscribeService', 'Transfer', 'Translate', 'VoiceID', 'WAFRegional', 'WAF', 'WAFV2', 'WellArchitected', 'ConnectWisdomService', 'WorkDocs', 'WorkMail', 'WorkMailMessageFlow', 'WorkSpacesWeb', 'WorkSpaces', 'XRay', ],];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,3 @@
<?php
// This file was auto-generated from sdk-root/src/data/sso/2019-06-10/endpoint-rule-set-1.json
return [ 'version' => '1.0', 'parameters' => [ 'Region' => [ 'builtIn' => 'AWS::Region', 'required' => false, 'documentation' => 'The AWS region used to dispatch the request.', 'type' => 'String', ], 'UseDualStack' => [ 'builtIn' => 'AWS::UseDualStack', 'required' => true, 'default' => false, 'documentation' => 'When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.', 'type' => 'Boolean', ], 'UseFIPS' => [ 'builtIn' => 'AWS::UseFIPS', 'required' => true, 'default' => false, 'documentation' => 'When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.', 'type' => 'Boolean', ], 'Endpoint' => [ 'builtIn' => 'SDK::Endpoint', 'required' => false, 'documentation' => 'Override the endpoint used to send this request', 'type' => 'String', ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'isSet', 'argv' => [ [ 'ref' => 'Endpoint', ], ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseFIPS', ], true, ], ], ], 'error' => 'Invalid Configuration: FIPS and custom endpoint are not supported', 'type' => 'error', ], [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseDualStack', ], true, ], ], ], 'error' => 'Invalid Configuration: Dualstack and custom endpoint are not supported', 'type' => 'error', ], [ 'conditions' => [], 'endpoint' => [ 'url' => [ 'ref' => 'Endpoint', ], 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], ], [ 'conditions' => [ [ 'fn' => 'isSet', 'argv' => [ [ 'ref' => 'Region', ], ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'aws.partition', 'argv' => [ [ 'ref' => 'Region', ], ], 'assign' => 'PartitionResult', ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseFIPS', ], true, ], ], [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseDualStack', ], true, ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsFIPS', ], ], ], ], [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsDualStack', ], ], ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso-fips.{Region}.{PartitionResult#dualStackDnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], ], [ 'conditions' => [], 'error' => 'FIPS and DualStack are enabled, but this partition does not support one or both', 'type' => 'error', ], ], ], [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseFIPS', ], true, ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsFIPS', ], ], ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'stringEquals', 'argv' => [ 'aws-us-gov', [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'name', ], ], ], ], ], 'endpoint' => [ 'url' => 'https://portal.sso.{Region}.amazonaws.com', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso-fips.{Region}.{PartitionResult#dnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], ], [ 'conditions' => [], 'error' => 'FIPS is enabled but this partition does not support FIPS', 'type' => 'error', ], ], ], [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseDualStack', ], true, ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsDualStack', ], ], ], ], ], 'type' => 'tree', 'rules' => [ [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso.{Region}.{PartitionResult#dualStackDnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], ], [ 'conditions' => [], 'error' => 'DualStack is enabled but this partition does not support DualStack', 'type' => 'error', ], ], ], [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso.{Region}.{PartitionResult#dnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], ], ], ], [ 'conditions' => [], 'error' => 'Invalid Configuration: Missing Region', 'type' => 'error', ], ],];
return [ 'version' => '1.0', 'parameters' => [ 'Region' => [ 'builtIn' => 'AWS::Region', 'required' => false, 'documentation' => 'The AWS region used to dispatch the request.', 'type' => 'String', ], 'UseDualStack' => [ 'builtIn' => 'AWS::UseDualStack', 'required' => true, 'default' => false, 'documentation' => 'When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.', 'type' => 'Boolean', ], 'UseFIPS' => [ 'builtIn' => 'AWS::UseFIPS', 'required' => true, 'default' => false, 'documentation' => 'When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.', 'type' => 'Boolean', ], 'Endpoint' => [ 'builtIn' => 'SDK::Endpoint', 'required' => false, 'documentation' => 'Override the endpoint used to send this request', 'type' => 'String', ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'isSet', 'argv' => [ [ 'ref' => 'Endpoint', ], ], ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseFIPS', ], true, ], ], ], 'error' => 'Invalid Configuration: FIPS and custom endpoint are not supported', 'type' => 'error', ], [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseDualStack', ], true, ], ], ], 'error' => 'Invalid Configuration: Dualstack and custom endpoint are not supported', 'type' => 'error', ], [ 'conditions' => [], 'endpoint' => [ 'url' => [ 'ref' => 'Endpoint', ], 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], 'type' => 'tree', ], [ 'conditions' => [ [ 'fn' => 'isSet', 'argv' => [ [ 'ref' => 'Region', ], ], ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'aws.partition', 'argv' => [ [ 'ref' => 'Region', ], ], 'assign' => 'PartitionResult', ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseFIPS', ], true, ], ], [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseDualStack', ], true, ], ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsFIPS', ], ], ], ], [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsDualStack', ], ], ], ], ], 'rules' => [ [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso-fips.{Region}.{PartitionResult#dualStackDnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], 'type' => 'tree', ], [ 'conditions' => [], 'error' => 'FIPS and DualStack are enabled, but this partition does not support one or both', 'type' => 'error', ], ], 'type' => 'tree', ], [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseFIPS', ], true, ], ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsFIPS', ], ], true, ], ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'stringEquals', 'argv' => [ [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'name', ], ], 'aws-us-gov', ], ], ], 'endpoint' => [ 'url' => 'https://portal.sso.{Region}.amazonaws.com', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso-fips.{Region}.{PartitionResult#dnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], 'type' => 'tree', ], [ 'conditions' => [], 'error' => 'FIPS is enabled but this partition does not support FIPS', 'type' => 'error', ], ], 'type' => 'tree', ], [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ [ 'ref' => 'UseDualStack', ], true, ], ], ], 'rules' => [ [ 'conditions' => [ [ 'fn' => 'booleanEquals', 'argv' => [ true, [ 'fn' => 'getAttr', 'argv' => [ [ 'ref' => 'PartitionResult', ], 'supportsDualStack', ], ], ], ], ], 'rules' => [ [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso.{Region}.{PartitionResult#dualStackDnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], 'type' => 'tree', ], [ 'conditions' => [], 'error' => 'DualStack is enabled but this partition does not support DualStack', 'type' => 'error', ], ], 'type' => 'tree', ], [ 'conditions' => [], 'endpoint' => [ 'url' => 'https://portal.sso.{Region}.{PartitionResult#dnsSuffix}', 'properties' => [], 'headers' => [], ], 'type' => 'endpoint', ], ], 'type' => 'tree', ], ], 'type' => 'tree', ], [ 'conditions' => [], 'error' => 'Invalid Configuration: Missing Region', 'type' => 'error', ], ],];

View file

@ -8,12 +8,12 @@ $baseDir = dirname(dirname(dirname($vendorDir)));
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
'72579e7bd17821bb1321b87411366eae' => $vendorDir . '/illuminate/support/helpers.php',

View file

@ -9,12 +9,12 @@ class ComposerStaticInited73ceb9c1bdec18b7c6d09764d1bce5
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php',
'3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
'72579e7bd17821bb1321b87411366eae' => __DIR__ . '/..' . '/illuminate/support/helpers.php',

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
<?php return array(
'root' => array(
'name' => 'ldap-account-manager/ldap-account-manager',
'pretty_version' => '9.2',
'version' => '9.2.0.0',
'pretty_version' => '9.3',
'version' => '9.3.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../../../',
@ -20,9 +20,9 @@
'dev_requirement' => false,
),
'aws/aws-sdk-php' => array(
'pretty_version' => '3.346.2',
'version' => '3.346.2.0',
'reference' => 'd1403b5a39af7ab7af4fc538deb33013c19c8d33',
'pretty_version' => '3.356.8',
'version' => '3.356.8.0',
'reference' => '3efa8c62c11fedb17b90f60b2d3a9f815b406e63',
'type' => 'library',
'install_path' => __DIR__ . '/../aws/aws-sdk-php',
'aliases' => array(),
@ -65,9 +65,9 @@
'dev_requirement' => false,
),
'duosecurity/duo_universal_php' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => 'a2852c46949a2de9ca6da908e4353a81c61b43a3',
'pretty_version' => '1.1.1',
'version' => '1.1.1.0',
'reference' => '4ee7253863d84653a60a8cad4b03aa3b66fcfd35',
'type' => 'library',
'install_path' => __DIR__ . '/../duosecurity/duo_universal_php',
'aliases' => array(),
@ -101,27 +101,27 @@
'dev_requirement' => false,
),
'guzzlehttp/guzzle' => array(
'pretty_version' => '7.9.3',
'version' => '7.9.3.0',
'reference' => '7b2f29fe81dc4da0ca0ea7d42107a0845946ea77',
'pretty_version' => '7.10.0',
'version' => '7.10.0.0',
'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/promises' => array(
'pretty_version' => '2.2.0',
'version' => '2.2.0.0',
'reference' => '7c69f28996b0a6920945dd20b3857e499d9ca96c',
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'reference' => '481557b130ef3790cf82b713667b43030dc9c957',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/promises',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/psr7' => array(
'pretty_version' => '2.7.1',
'version' => '2.7.1.0',
'reference' => 'c2270caaabe631b3b44c85f99e5a04bbb8060d16',
'pretty_version' => '2.8.0',
'version' => '2.8.0.0',
'reference' => '21dc724a0583619cd1652f673303492272778051',
'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
'aliases' => array(),
@ -173,8 +173,8 @@
'dev_requirement' => false,
),
'ldap-account-manager/ldap-account-manager' => array(
'pretty_version' => '9.2',
'version' => '9.2.0.0',
'pretty_version' => '9.3',
'version' => '9.3.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../../../',
@ -200,9 +200,9 @@
'dev_requirement' => false,
),
'nesbot/carbon' => array(
'pretty_version' => '3.9.1',
'version' => '3.9.1.0',
'reference' => 'ced71f79398ece168e24f7f7710462f462310d4d',
'pretty_version' => '3.10.2',
'version' => '3.10.2.0',
'reference' => '76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24',
'type' => 'library',
'install_path' => __DIR__ . '/../nesbot/carbon',
'aliases' => array(),
@ -266,9 +266,9 @@
'dev_requirement' => false,
),
'phpseclib/phpseclib' => array(
'pretty_version' => '3.0.43',
'version' => '3.0.43.0',
'reference' => '709ec107af3cb2f385b9617be72af8cf62441d02',
'pretty_version' => '3.0.46',
'version' => '3.0.46.0',
'reference' => '56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6',
'type' => 'library',
'install_path' => __DIR__ . '/../phpseclib/phpseclib',
'aliases' => array(),
@ -344,9 +344,9 @@
'psr/http-factory-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
1 => '^1.0',
2 => '*',
0 => '^1.0',
1 => '*',
2 => '1.0',
),
),
'psr/http-message' => array(
@ -361,8 +361,8 @@
'psr/http-message-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0',
1 => '*',
0 => '*',
1 => '1.0',
),
),
'psr/http-server-handler' => array(
@ -436,63 +436,63 @@
'dev_requirement' => false,
),
'spomky-labs/cbor-php' => array(
'pretty_version' => '3.1.0',
'version' => '3.1.0.0',
'reference' => '499d9bff0a6d59c4f1b813cc617fc3fd56d6dca4',
'pretty_version' => '3.1.1',
'version' => '3.1.1.0',
'reference' => '5404f3e21cbe72f5cf612aa23db2b922fd2f43bf',
'type' => 'library',
'install_path' => __DIR__ . '/../spomky-labs/cbor-php',
'aliases' => array(),
'dev_requirement' => false,
),
'spomky-labs/pki-framework' => array(
'pretty_version' => '1.2.3',
'version' => '1.2.3.0',
'reference' => '5ff1dcc21e961b60149a80e77f744fc047800b31',
'pretty_version' => '1.3.0',
'version' => '1.3.0.0',
'reference' => 'eced5b5ce70518b983ff2be486e902bbd15135ae',
'type' => 'library',
'install_path' => __DIR__ . '/../spomky-labs/pki-framework',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/clock' => array(
'pretty_version' => 'v6.4.13',
'version' => '6.4.13.0',
'reference' => 'b2bf55c4dd115003309eafa87ee7df9ed3dde81b',
'pretty_version' => 'v6.4.24',
'version' => '6.4.24.0',
'reference' => '5e15a9c9aeeb44a99f7cf24aa75aa9607795f6f8',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/console' => array(
'pretty_version' => 'v6.4.21',
'version' => '6.4.21.0',
'reference' => 'a3011c7b7adb58d89f6c0d822abb641d7a5f9719',
'pretty_version' => 'v6.4.25',
'version' => '6.4.25.0',
'reference' => '273fd29ff30ba0a88ca5fb83f7cf1ab69306adae',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/console',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.5.1',
'version' => '3.5.1.0',
'reference' => '74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6',
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-client' => array(
'pretty_version' => 'v6.4.19',
'version' => '6.4.19.0',
'reference' => '3294a433fc9d12ae58128174896b5b1822c28dad',
'pretty_version' => 'v6.4.25',
'version' => '6.4.25.0',
'reference' => 'b8e9dce2d8acba3c32af467bb58e0c3656886181',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-client',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/http-client-contracts' => array(
'pretty_version' => 'v3.5.2',
'version' => '3.5.2.0',
'reference' => 'ee8d807ab20fcb51267fdace50fbe3494c31e645',
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => '75d7043853a42837e68111812f4d964b01e5101c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-client-contracts',
'aliases' => array(),
@ -505,17 +505,17 @@
),
),
'symfony/http-foundation' => array(
'pretty_version' => 'v6.4.21',
'version' => '6.4.21.0',
'reference' => '3f0c7ea41db479383b81d436b836d37168fd5b99',
'pretty_version' => 'v6.4.25',
'version' => '6.4.25.0',
'reference' => '6bc974c0035b643aa497c58d46d9e25185e4b272',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/http-foundation',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
@ -523,17 +523,17 @@
'dev_requirement' => false,
),
'symfony/polyfill-intl-grapheme' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'reference' => 'b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe',
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '380872130d3a5dd3ace2f4010d95125fde5d5c70',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-intl-normalizer' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '3833d7255cc303546435cb650316bff708a1c75c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
@ -541,8 +541,8 @@
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
@ -550,17 +550,17 @@
'dev_requirement' => false,
),
'symfony/polyfill-php83' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'reference' => '2fb86d65e2d424369ad2905e83b236a8805ba491',
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '17f6f9a6b1735c0f163024d959f700cfbc5155e5',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php83',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-uuid' => array(
'pretty_version' => 'v1.32.0',
'version' => '1.32.0.0',
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '21533be36c24be3f4b1669c4725c7d1d2bab4ae2',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-uuid',
@ -568,45 +568,45 @@
'dev_requirement' => false,
),
'symfony/psr-http-message-bridge' => array(
'pretty_version' => 'v6.4.13',
'version' => '6.4.13.0',
'reference' => 'c9cf83326a1074f83a738fc5320945abf7fb7fec',
'pretty_version' => 'v6.4.24',
'version' => '6.4.24.0',
'reference' => '6954b4e8aef0e5d46f8558c90edcf27bb01b4724',
'type' => 'symfony-bridge',
'install_path' => __DIR__ . '/../symfony/psr-http-message-bridge',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/service-contracts' => array(
'pretty_version' => 'v3.5.1',
'version' => '3.5.1.0',
'reference' => 'e53260aabf78fb3d63f8d79d69ece59f80d5eda0',
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => 'f021b05a130d35510bd6b25fe9053c2a8a15d5d4',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/service-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/string' => array(
'pretty_version' => 'v6.4.21',
'version' => '6.4.21.0',
'reference' => '73e2c6966a5aef1d4892873ed5322245295370c6',
'pretty_version' => 'v6.4.25',
'version' => '6.4.25.0',
'reference' => '7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/string',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/translation' => array(
'pretty_version' => 'v6.4.21',
'version' => '6.4.21.0',
'reference' => 'bb92ea5588396b319ba43283a5a3087a034cb29c',
'pretty_version' => 'v6.4.24',
'version' => '6.4.24.0',
'reference' => '300b72643e89de0734d99a9e3f8494a3ef6936e1',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/translation',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/translation-contracts' => array(
'pretty_version' => 'v3.5.1',
'version' => '3.5.1.0',
'reference' => '4667ff3bd513750603a09c8dedbea942487fb07c',
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => 'df210c7a2573f1913b2d17cc95f90f53a73d8f7d',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/translation-contracts',
'aliases' => array(),
@ -619,9 +619,9 @@
),
),
'symfony/uid' => array(
'pretty_version' => 'v6.4.13',
'version' => '6.4.13.0',
'reference' => '18eb207f0436a993fffbdd811b5b8fa35fa5e007',
'pretty_version' => 'v6.4.24',
'version' => '6.4.24.0',
'reference' => '17da16a750541a42cf2183935e0f6008316c23f7',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/uid',
'aliases' => array(),
@ -634,9 +634,9 @@
),
),
'web-auth/cose-lib' => array(
'pretty_version' => '4.4.0',
'version' => '4.4.0.0',
'reference' => '2166016e48e0214f4f63320a7758a9386d14c92a',
'pretty_version' => '4.4.2',
'version' => '4.4.2.0',
'reference' => 'a93b61c48fb587855f64a9ec11ad7b60e867cb15',
'type' => 'library',
'install_path' => __DIR__ . '/../web-auth/cose-lib',
'aliases' => array(),

View file

@ -38,7 +38,7 @@ class Client
const JWT_LEEWAY = 60;
const SUCCESS_STATUS_CODE = 200;
const USER_AGENT = "duo_universal_php/1.1.0";
const USER_AGENT = "duo_universal_php/1.1.1";
const SIG_ALGORITHM = "HS512";
const GRANT_TYPE = "authorization_code";
const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
@ -63,6 +63,7 @@ class Client
public $redirect_url;
public $use_duo_code_attribute;
private $client_secret;
private $user_agent_extension;
/**
* Retrieves exception message for DuoException from HTTPS result message.
@ -190,6 +191,34 @@ class Client
$this->redirect_url = $redirect_url;
$this->use_duo_code_attribute = $use_duo_code_attribute;
$this->http_proxy = $http_proxy;
$this->user_agent_extension = null;
}
/**
* Append custom information to the user agent string.
*
* @param string $user_agent_extension Custom user agent information
*
* @return void
*/
public function appendToUserAgent(string $user_agent_extension): void
{
$this->user_agent_extension = trim($user_agent_extension);
}
/**
* Build the complete user agent string.
*
* @return string The complete user agent string
*/
private function buildUserAgent(): string
{
$base_user_agent = self::USER_AGENT . " php/" . phpversion() . " "
. php_uname();
if (!empty($this->user_agent_extension)) {
return $base_user_agent . " " . $this->user_agent_extension;
}
return $base_user_agent;
}
/**
@ -282,7 +311,7 @@ class Client
public function exchangeAuthorizationCodeFor2FAResult(string $duoCode, string $username, ?string $nonce = null): array
{
$token_endpoint = "https://" . $this->api_host . self::TOKEN_ENDPOINT;
$useragent = self::USER_AGENT . " php/" . phpversion() . " " . php_uname();
$useragent = $this->buildUserAgent();
$jwt = $this->createJwtPayload($token_endpoint);
$request = ["grant_type" => self::GRANT_TYPE,
"code" => $duoCode,

View file

@ -514,4 +514,144 @@ final class ClientTest extends TestCase
$this->assertStringContainsString("scope=openid", $duo_uri);
$this->assertStringContainsString($expected_redir_uri, $duo_uri);
}
/**
* Test that the user agent extension can be set and is included in requests.
*/
public function testAppendToUserAgent(): void
{
$custom_extension = "MyApp/1.0.0";
$id_token = $this->createIdToken();
$result = $this->createTokenResult($id_token);
// Mock the client to capture the user agent sent in HTTP requests
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$this->client_id, $this->client_secret, $this->api_host, $this->redirect_url])
->setMethods(['makeHttpsCall'])
->getMock();
// Set up the mock to capture the user agent parameter
$captured_user_agent = null;
$client->method('makeHttpsCall')
->willReturnCallback(function ($endpoint, $request, $user_agent = null) use (&$captured_user_agent, $result) {
$captured_user_agent = $user_agent;
return $result;
});
// Append custom user agent extension
$client->appendToUserAgent($custom_extension);
// Make a call that uses the user agent
$client->exchangeAuthorizationCodeFor2FAResult($this->code, $this->username);
// Verify the user agent includes our custom extension
$this->assertNotNull($captured_user_agent);
$this->assertStringContainsString($custom_extension, $captured_user_agent);
$this->assertStringContainsString(Client::USER_AGENT, $captured_user_agent);
$this->assertStringContainsString("php/" . phpversion(), $captured_user_agent);
}
/**
* Test that user agent works correctly without any extension.
*/
public function testUserAgentWithoutExtension(): void
{
$id_token = $this->createIdToken();
$result = $this->createTokenResult($id_token);
// Mock the client to capture the user agent sent in HTTP requests
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$this->client_id, $this->client_secret, $this->api_host, $this->redirect_url])
->setMethods(['makeHttpsCall'])
->getMock();
// Set up the mock to capture the user agent parameter
$captured_user_agent = null;
$client->method('makeHttpsCall')
->willReturnCallback(function ($endpoint, $request, $user_agent = null) use (&$captured_user_agent, $result) {
$captured_user_agent = $user_agent;
return $result;
});
// Make a call without setting any custom user agent extension
$client->exchangeAuthorizationCodeFor2FAResult($this->code, $this->username);
// Verify the user agent contains default information but no custom extension
$this->assertNotNull($captured_user_agent);
$this->assertStringContainsString(Client::USER_AGENT, $captured_user_agent);
$this->assertStringContainsString("php/" . phpversion(), $captured_user_agent);
$this->assertStringContainsString(php_uname(), $captured_user_agent);
}
/**
* Test that empty user agent extension is handled correctly.
*/
public function testAppendToUserAgentEmpty(): void
{
$id_token = $this->createIdToken();
$result = $this->createTokenResult($id_token);
// Mock the client to capture the user agent sent in HTTP requests
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$this->client_id, $this->client_secret, $this->api_host, $this->redirect_url])
->setMethods(['makeHttpsCall'])
->getMock();
// Set up the mock to capture the user agent parameter
$captured_user_agent = null;
$client->method('makeHttpsCall')
->willReturnCallback(function ($endpoint, $request, $user_agent = null) use (&$captured_user_agent, $result) {
$captured_user_agent = $user_agent;
return $result;
});
// Append empty user agent extension
$client->appendToUserAgent("");
// Make a call
$client->exchangeAuthorizationCodeFor2FAResult($this->code, $this->username);
// Verify the user agent contains default information but no trailing space
$this->assertNotNull($captured_user_agent);
$this->assertStringContainsString(Client::USER_AGENT, $captured_user_agent);
$this->assertStringContainsString("php/" . phpversion(), $captured_user_agent);
$this->assertStringContainsString(php_uname(), $captured_user_agent);
// Ensure no trailing spaces from empty extension
$expected_base = Client::USER_AGENT . " php/" . phpversion() . " " . php_uname();
$this->assertEquals($expected_base, $captured_user_agent);
}
/**
* Test that whitespace-only user agent extension is handled correctly.
*/
public function testAppendToUserAgentWhitespace(): void
{
$id_token = $this->createIdToken();
$result = $this->createTokenResult($id_token);
// Mock the client to capture the user agent sent in HTTP requests
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$this->client_id, $this->client_secret, $this->api_host, $this->redirect_url])
->setMethods(['makeHttpsCall'])
->getMock();
// Set up the mock to capture the user agent parameter
$captured_user_agent = null;
$client->method('makeHttpsCall')
->willReturnCallback(function ($endpoint, $request, $user_agent = null) use (&$captured_user_agent, $result) {
$captured_user_agent = $user_agent;
return $result;
});
// Append whitespace-only user agent extension
$client->appendToUserAgent(" ");
// Make a call
$client->exchangeAuthorizationCodeFor2FAResult($this->code, $this->username);
// Verify the user agent contains default information but no extra whitespace
$this->assertNotNull($captured_user_agent);
$expected_base = Client::USER_AGENT . " php/" . phpversion() . " " . php_uname();
$this->assertEquals($expected_base, $captured_user_agent);
}
}

View file

@ -2,6 +2,17 @@
Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
## 7.10.0 - 2025-08-23
### Added
- Support for PHP 8.5
### Changed
- Adjusted `guzzlehttp/promises` version constraint to `^2.3`
- Adjusted `guzzlehttp/psr7` version constraint to `^2.8`
## 7.9.3 - 2025-03-27

View file

@ -81,8 +81,8 @@
"require": {
"php": "^7.2.5 || ^8.0",
"ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.3",
"guzzlehttp/psr7": "^2.7.0",
"guzzlehttp/promises": "^2.3",
"guzzlehttp/psr7": "^2.8",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},

View file

@ -0,0 +1,6 @@
{
"name": "guzzle",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -125,7 +125,9 @@ class CurlFactory implements CurlFactoryInterface
unset($easy->handle);
if (\count($this->handles) >= $this->maxHandles) {
if (PHP_VERSION_ID < 80000) {
\curl_close($resource);
}
} else {
// Remove all callback functions as they can hold onto references
// and are not cleaned up by curl_reset. Using curl_setopt_array
@ -729,7 +731,10 @@ class CurlFactory implements CurlFactoryInterface
public function __destruct()
{
foreach ($this->handles as $id => $handle) {
if (PHP_VERSION_ID < 80000) {
\curl_close($handle);
}
unset($this->handles[$id]);
}
}

View file

@ -240,7 +240,10 @@ class CurlMultiHandler
$handle = $this->handles[$id]['easy']->handle;
unset($this->delays[$id], $this->handles[$id]);
\curl_multi_remove_handle($this->_mh, $handle);
if (PHP_VERSION_ID < 80000) {
\curl_close($handle);
}
return true;
}

View file

@ -333,8 +333,15 @@ class StreamHandler
);
return $this->createResource(
function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) {
function () use ($uri, $contextResource, $context, $options, $request) {
$resource = @\fopen((string) $uri, 'r', false, $contextResource);
// See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
if (function_exists('http_get_last_response_headers')) {
/** @var array|null */
$http_response_header = \http_get_last_response_headers();
}
$this->lastHeaders = $http_response_header ?? [];
if (false === $resource) {

View file

@ -187,12 +187,12 @@ final class Middleware
* Middleware that logs requests, responses, and errors using a message
* formatter.
*
* @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests.
*
* @param LoggerInterface $logger Logs messages.
* @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings.
* @param string $logLevel Level at which to log requests.
*
* @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests.
*
* @return callable Returns a function that accepts the next handler.
*/
public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable

View file

@ -1,6 +1,13 @@
# CHANGELOG
## 2.3.0 - 2025-08-22
### Added
- PHP 8.5 support
## 2.2.0 - 2025-03-27
### Fixed

View file

@ -41,7 +41,7 @@ composer require guzzlehttp/promises
| Version | Status | PHP Version |
|---------|---------------------|--------------|
| 1.x | Security fixes only | >=5.5,<8.3 |
| 2.x | Latest | >=7.2.5,<8.5 |
| 2.x | Latest | >=7.2.5,<8.6 |
## Quick Start

View file

@ -30,7 +30,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"autoload": {
"psr-4": {

View file

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.8.0 - 2025-08-23
### Added
- Allow empty lists as header values
### Changed
- PHP 8.5 support
## 2.7.1 - 2025-03-27
### Fixed

View file

@ -25,7 +25,7 @@ composer require guzzlehttp/psr7
| Version | Status | PHP Version |
|---------|---------------------|--------------|
| 1.x | EOL (2024-06-30) | >=5.4,<8.2 |
| 2.x | Latest | >=7.2.5,<8.5 |
| 2.x | Latest | >=7.2.5,<8.6 |
## AppendStream

View file

@ -62,7 +62,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"

View file

@ -174,10 +174,6 @@ trait MessageTrait
return $this->trimAndValidateHeaderValues([$value]);
}
if (count($value) === 0) {
throw new \InvalidArgumentException('Header value can not be an empty array.');
}
return $this->trimAndValidateHeaderValues($value);
}

View file

@ -397,7 +397,7 @@ final class Utils
restore_error_handler();
if ($ex) {
/** @var $ex \RuntimeException */
/** @var \RuntimeException $ex */
throw $ex;
}
@ -444,7 +444,7 @@ final class Utils
restore_error_handler();
if ($ex) {
/** @var $ex \RuntimeException */
/** @var \RuntimeException $ex */
throw $ex;
}

View file

@ -44,21 +44,20 @@
"ext-json": "*",
"carbonphp/carbon-doctrine-types": "<100.0",
"psr/clock": "^1.0",
"symfony/clock": "^6.3 || ^7.0",
"symfony/clock": "^6.3.12 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
},
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.57.2",
"friendsofphp/php-cs-fixer": "^3.75.0",
"kylekatarnls/multi-tester": "^2.5.3",
"ondrejmirtes/better-reflection": "^6.25.0.4",
"phpmd/phpmd": "^2.15.0",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.11.2",
"phpunit/phpunit": "^10.5.20",
"squizlabs/php_codesniffer": "^3.9.0"
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.17",
"phpunit/phpunit": "^10.5.46",
"squizlabs/php_codesniffer": "^3.13.0"
},
"provide": {
"psr/clock-implementation": "1.0"

View file

@ -127,53 +127,50 @@ This project exists thanks to all the people who contribute.
Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
<!-- <open-collective-sponsors> -->
<a title="Ставки на спорт, БК в Україні" href="https://betking.com.ua/sports-book/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Букмекер" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/065e61d2-f890-42db-b06c-8d40b39b2f0e/bk.jpg" width="96" height="96"></a>
<a title="Porównanie kasyn online w Polsce. Darmowe automaty online." href="https://onlinekasyno-polis.pl/" target="_blank"><img alt="Online Kasyno Polis" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/12fe53d4-b2e4-4601-b9ea-7b652c414a38/274px%20274px-2.png" width="96" height="96"></a>
<a title="Онлайн казино 777 Україна" href="https://777.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Онлайн казино 777" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/7e572d50-1ce8-4d69-ae12-86cc80371373/ok-ua-777.png" width="96" height="96"></a>
<a title="Best non Gamstop sites in the UK" href="https://www.pieria.co.uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Best non Gamstop sites in the UK" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/34e340b8-e1de-4932-8a76-1b3ce2ec7ee8/logo_white%20bg%20(8).png" width="96" height="96"></a>
<a title="Real Money Pokies" href="https://onlinecasinoskiwi.co.nz/real-money-pokies/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Real Money Pokies" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/d0f7382e-32ea-4425-a8c4-3019f9ed501c/NZ_logo%20(6)%20(2).jpg" width="96" height="96"></a>
<a title="Non GamStop Bookies UK" href="https://netto.co.uk/betting-sites-not-on-gamstop/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Non GamStop Bookies UK" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/43c5561c-8907-4ef7-a4ee-c6da054788b8/logo-site%20(3).jpg" width="96" height="96"></a>
<a title="#1 Guide To Online Gambling In Canada" href="https://casinohex.org/canada/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="CasinoHex Canada" src="https://opencollective-production.s3.us-west-1.amazonaws.com/79fdbcc0-a997-11eb-abbc-25e48b63c6dc.jpg" width="127.5" height="96"></a>
<a title="Non GamStop Bookies UK" href="https://netto.co.uk/betting-sites-not-on-gamstop/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Non GamStop Bookies UK" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/51bfaa05-02b3-4cd9-b1a4-9d0d8f34cbae/%D0%97%D0%BD%D1%96%D0%BC%D0%BE%D0%BA%20%D0%B5%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202025-07-04%20%D0%BE%2015.21.16%20(1)%20(1)%20(1).jpg" width="126" height="96"></a>
<a title="Trusted last mile route planning and route optimization" href="https://route4me.com/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Route4Me Route Planner" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/237386c3-48a2-47c6-97ac-5f888cdb4cda/Route4MeIconLogo.png" width="96" height="96"></a>
<a title="Onlinecasinosgr.com" href="https://onlinecasinosgr.com/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Onlinecasinosgr.com" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/a9b971ee-db5f-4400-8c4b-76cf9bc35015/IMAGE%202024-06-14%2013%3A54%3A14.jpg" width="96" height="96"></a>
<a title="Онлайн казино та БК (ставки на спорт) в Україні" href="https://betking.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Betking казино" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/08587758-582c-4136-aba5-2519230960d3/betking.jpg" width="96" height="96"></a>
<a title="WestNews проект Александра Победы о гемблинге и онлайн-казино в Украине, предлагающий новости, обзоры, рейтинги и гиды по игорным заведениям." href="https://westnews.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="WestNews онлайн казино Украины" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/7fae83dd-0d53-42f7-b63c-d7062a86ccb1/3502ab17-a150-40e1-8f01-c26ff60c4cf8.png" width="96" height="96"></a>
<a title="gaia-wines.gr" href="https://www.gaia-wines.gr/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="gaia-wines.gr" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/a9b971ee-db5f-4400-8c4b-76cf9bc35015/IMAGE%202024-06-14%2013%3A54%3A14.jpg" width="96" height="96"></a>
<a title="Ставки на спорт, БК в Україні" href="https://betking.com.ua/sports-book/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Букмекер" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/065e61d2-f890-42db-b06c-8d40b39b2f0e/bk.jpg" width="96" height="96"></a>
<a title="Best Casinos not on Gamstop in the UK 2025" href="https://www.vso.org.uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="best non Gamstop casinos" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/3f48874e-f2f6-4062-a2a2-1500677ee3d9/125%D1%85125%20(1).jpg" width="96" height="96"></a>
<a title="Проект с обзорами легальных онлайн казино Украины. Мы помогаем выбрать лучше казино онлайн игрокам." href="https://sportarena.com/casino/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Лучшие онлайн казино Украины на Sportarena" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/765475f7-3fea-4867-8f83-7b6f91b06128/sportarena%20(1).png" width="60" height="64"></a>
<a title="Проєкт з оглядами онлайн казино та їхніх бонусів. На сайті можна знайти актуальні промокоди та інші бонуси онлайн казино України." href="https://y-k.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Онлайн казино та їхні бонуси y-k.com.ua" src="https://logo.clearbit.com/y-k.com.ua" width="64" height="64"></a>
<a title="#1 Guide To Online Gambling In Canada" href="https://casinohex.org/canada/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="CasinoHex Canada" src="https://opencollective-production.s3.us-west-1.amazonaws.com/79fdbcc0-a997-11eb-abbc-25e48b63c6dc.jpg" width="127.5" height="96"></a>
<a title="Real Money Pokies" href="https://onlinecasinoskiwi.co.nz/real-money-pokies/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Real Money Pokies" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/d0f7382e-32ea-4425-a8c4-3019f9ed501c/NZ_logo%20(6)%20(2).jpg" width="96" height="96"></a>
<a title="Онлайн казино та БК (ставки на спорт) в Україні" href="https://betking.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Betking казино" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/08587758-582c-4136-aba5-2519230960d3/betking.jpg" width="64" height="64"></a>
<a title="WestNews проект Александра Победы о гемблинге и онлайн-казино в Украине, предлагающий новости, обзоры, рейтинги и гиды по игорным заведениям." href="https://westnews.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="WestNews онлайн казино Украины" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/7fae83dd-0d53-42f7-b63c-d7062a86ccb1/3502ab17-a150-40e1-8f01-c26ff60c4cf8.png" width="64" height="64"></a>
<a title="UK casinos not on GamStop" href="https://www.stjames-theatre.co.uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="UK casinos not on GamStop" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/34e5e82e-2121-4082-a321-050dca381d6c/%D0%97%D0%BD%D1%96%D0%BC%D0%BE%D0%BA%20%D0%B5%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202025-01-10%20%D0%BE%2015.29.42%20(1)%20(1).jpg" width="64" height="64"></a><details><summary>See more</summary>
<a title="OnlineCasinosSpelen" href="https://onlinecasinosspelen.com?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="OnlineCasinosSpelen" src="https://logo.clearbit.com/onlinecasinosspelen.com" width="64" height="64"></a>
<a title="Betwinner is an online bookmaker offering sports betting, casino games, and more." href="https://guidebook.betwinner.com/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Guidebook.BetWinner" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/82cab29a-7002-4924-83bf-2eecb03d07c4/0x0.png" width="64" height="64"></a>
<a title="Онлайн казино casino.ua" href="https://casino.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Онлайн казино casino.ua" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/32790ee6-245b-45bd-acf7-7a661fe2cf9f/logo.png" width="64" height="64"></a>
<a title="Best PayID Pokies in Australia" href="https://payid-gambler.net/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="PayIDGambler" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/b120ff74-a4cc-4e25-a96f-2b040d60de14/payidgambler.png" width="64" height="64"></a>
<a title="Legal-casino.net незалежний інтернет-портал, присвячений ліцензійним онлайн казино України та азартним іграм в інтернеті. На якому не проводяться ігри на реальні чи віртуальні гроші." href="https://legal-casino.net/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Legal Casino" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/79978436-a1cb-42f1-8269-d495b232934a/legal-casino.jpg" width="64" height="64"></a>
<a title="WildWinz online casino" href="https://wildwinz.com?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="WildWinz Casino" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/ccfcee7c-775c-4d43-ba23-3f0d2969497b/wildwinz.jpg" width="64" height="64"></a>
<a title="The Betwinner program allows individuals and businesses to earn commissions." href="https://betwinnerpartner.com/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Betwinner Partner" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/46a67975-2b70-4b91-9106-0e224c664b21/images%20(12).jpg" width="64" height="64"></a>
<a title="Top Casinos Canada" href="https://topcasino.net/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Top Casinos Canada" src="https://topcasino.net/img/topcasino-logo-cover.png" width="64" height="64"></a>
<a title="Playfortune.net.br" href="https://playfortune.net.br/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Playfortune.net.br" src="https://logo.clearbit.com/playfortune.net.br" width="64" height="64"></a>
<a title="https://play-fortune.pl/kasyno/z-minimalnym-depozytem/" href="https://play-fortune.pl/kasyno/z-minimalnym-depozytem/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="https://play-fortune.pl/kasyno/z-minimalnym-depozytem/" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/cbeea308-5148-4f6c-ac6e-dbfa029aadd1/PL.png" width="64" height="64"></a>
<a title="Best-betting.net is an Indian website where you can always find interesting, useful, and up-to-date information about cricket and other sports. Additionally, on our portal, you can explore predictions and betting opportunities for the most exciting sports" href="https://best-betting.net/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Best Betting" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/4b437e94-747c-4cf5-be67-d11bf8472d76/bestbetting-logo-cover.png" width="64" height="64"></a>
<a title="inkedin" href="https://inkedin.com?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="inkedin" src="https://logo.clearbit.com/inkedin.com" width="64" height="64"></a>
<a title="Актуальний та повносправний рейтинг онлайн казино України, ґрунтований на відгуках реальних гравців." href="https://uk.onlinecasino.in.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Онлайн казино України" src="https://opencollective-production.s3.us-west-1.amazonaws.com/c0b4b090-eef8-11ec-9cb7-0527a205b226.png" width="64" height="64"></a>
<a title="Buy TikTok Followers is a leading provider of social media growth solutions for TikTok.com." href="https://buytiktokfollowers.co/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="BuyTikTokFollowers.co" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/8626c295-9414-4e0c-b228-38ca2704cd68/btf-favicon.png" width="64" height="64"></a>
<a title="Slots not on GamStop" href="https://nogamstopcasinos.uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Slots not on GamStop" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/3b5fedc2-f3e5-41f5-84a9-869e2cbeb632/%D0%97%D0%BD%D1%96%D0%BC%D0%BE%D0%BA%20%D0%B5%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202025-05-01%20%D0%BE%2019.38.02%20(1)%20(1)%20(1).jpg" width="64" height="64"></a>
<a title="Offshore bookmakers review site." href="https://www.sportsbookreviewsonline.com/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Sportsbook Reviews Online" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/6d499f24-d669-4fc6-bb5f-b87184aa7963/sportsbookreviewsonline_com.png" width="64" height="64"></a>
<a title="Ставки на спорт Favbet" href="https://www.favbet.ua/uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Ставки на спорт Favbet" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/d86d313e-7b17-42fa-8b76-3f17fbf681a2/favbet-logo.jpg" width="64" height="64"></a>
<a title="Znajdź najlepsze zakłady bukmacherskie w Polsce w 2023 roku. Probukmacher.pl to Twoje kompendium wiedzy na temat bukmacherów!" href="https://www.probukmacher.pl?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Probukmacher" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/caf50271-4560-4ffe-a434-ea15239168db/Screenshot_1.png" width="89" height="64"></a>
<a title="Casino-portugal.pt" href="https://casino-portugal.pt/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Casino-portugal.pt" src="https://logo.clearbit.com/casino-portugal.pt" width="64" height="64"></a>
<a title="inkedin" href="https://inkedin.com?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="inkedin" src="https://logo.clearbit.com/inkedin.com" width="42" height="42"></a>
<a title="Casino-portugal.pt" href="https://casino-portugal.pt/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Casino-portugal.pt" src="https://logo.clearbit.com/casino-portugal.pt" width="42" height="42"></a>
<a title="Znajdź najlepsze zakłady bukmacherskie w Polsce w 2023 roku. Probukmacher.pl to Twoje kompendium wiedzy na temat bukmacherów!" href="https://www.probukmacher.pl?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Probukmacher" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/caf50271-4560-4ffe-a434-ea15239168db/Screenshot_1.png" width="58" height="42"></a>
<a title="Get professional support for Carbon" href="https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&amp;utm_medium=referral&amp;utm_campaign=docs" target="_blank"><img alt="Tidelift" src="https://carbon.nesbot.com/docs/sponsors/tidelift-brand.png" width="84" height="42"></a>
<a title="Playfortune.net.br" href="https://playfortune.net.br/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Playfortune.net.br" src="https://logo.clearbit.com/playfortune.net.br" width="42" height="42"></a>
<a title="https://play-fortune.pl/kasyno/z-minimalnym-depozytem/" href="https://play-fortune.pl/kasyno/z-minimalnym-depozytem/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="https://play-fortune.pl/kasyno/z-minimalnym-depozytem/" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/cbeea308-5148-4f6c-ac6e-dbfa029aadd1/PL.png" width="42" height="42"></a>
<a title="UK casinos not on GamStop" href="https://www.stjamestheatre.co.uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="UK casinos not on GamStop" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/34e5e82e-2121-4082-a321-050dca381d6c/%D0%97%D0%BD%D1%96%D0%BC%D0%BE%D0%BA%20%D0%B5%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202025-01-10%20%D0%BE%2015.29.42%20(1)%20(1).jpg" width="42" height="42"></a>
<a title="Актуальний та повносправний рейтинг онлайн казино України, ґрунтований на відгуках реальних гравців." href="https://uk.onlinecasino.in.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Онлайн казино України" src="https://opencollective-production.s3.us-west-1.amazonaws.com/c0b4b090-eef8-11ec-9cb7-0527a205b226.png" width="42" height="42"></a>
<a title="Sites not on GamStop" href="https://casinonotongamstop.uk/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Sites not on GamStop" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/5c5977b8-1e94-43d6-b2d7-4af25bb85dbd/%D0%97%D0%BD%D1%96%D0%BC%D0%BE%D0%BA%20%D0%B5%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202025-05-01%20%D0%BE%2015.08.38%20(1)%20(2).jpg" width="68" height="42"></a>
<a title="Проект с обзорами легальных онлайн казино Украины. Мы помогаем выбрать лучше казино онлайн игрокам." href="https://sportarena.com/casino/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Лучшие онлайн казино Украины на Sportarena" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/765475f7-3fea-4867-8f83-7b6f91b06128/sportarena%20(1).png" width="40" height="42"></a>
<a title="Проєкт з оглядами онлайн казино та їхніх бонусів. На сайті можна знайти актуальні промокоди та інші бонуси онлайн казино України." href="https://y-k.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Онлайн казино та їхні бонуси y-k.com.ua" src="https://logo.clearbit.com/y-k.com.ua" width="42" height="42"></a>
<a title="Slots City® ➢ Лучшее лицензионно казино онлайн и оффлайн на гривны в Украине. 【 Более1500 игровых автоматов и слотов】✅ Официально и Безопасно" href="https://slotscity.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Slots City" src="https://opencollective-production.s3.us-west-1.amazonaws.com/d7e298c0-7abe-11ed-8553-230872f5e54d.png" width="59" height="42"></a>
<a title="Entertainment" href="https://www.nongamstopbets.com/casinos-not-on-gamstop/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Non-GamStop Bets UK" src="https://logo.clearbit.com/nongamstopbets.com" width="42" height="42"></a>
<a title="WildWinz online casino" href="https://wildwinz.com?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="WildWinz Casino" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/ccfcee7c-775c-4d43-ba23-3f0d2969497b/wildwinz.jpg" width="42" height="42"></a>
<a title="ігрові автомати беткінг" href="https://betking.com.ua/games/all-slots/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Ігрові автомати" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/94601d07-3205-4c60-9c2d-9b8194dbefb7/skg-blue.png" width="42" height="42"></a>
<a title="Casinos not on Gamstop" href="https://lgcnews.com/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Non Gamstop Casinos" src="https://lgcnews.com/wp-content/uploads/2018/01/LGC-logo-v8-temp.png" width="84" height="42"></a>
<a title="Slotozilla website" href="https://www.slotozilla.com/nz/free-spins" target="_blank"><img alt="Slotozilla" src="https://carbon.nesbot.com/docs/sponsors/slotozilla.png" width="42" height="42"></a>
<a title="Per tutte le ultime notizie sul gioco d&#039;azzardo Non AAMS, le recensioni e i bonus di iscrizione." href="https://casinononaams.online?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="casino non aams" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/c60b92d1-590c-48a5-9527-fb0909431a86/casino%20non%20aams%20icon.jpg" width="42" height="42"></a>
<a title="Credit Zaim" href="https://creditzaim.com.ua/?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Credit Zaim" src="https://opencollective-production.s3.us-west-1.amazonaws.com/account-avatar/a856ed4e-651d-47c9-aa7a-98059423b3a6/creditzaim_logo.png" width="42" height="42"></a>
<a title="Incognito" href="https://opencollective.com/user-7146c9f8?utm_source=opencollective&amp;utm_medium=github&amp;utm_campaign=Carbon" target="_blank"><img alt="Incognito" src="https://images.opencollective.com/user-7146c9f8/avatar/256.png" width="42" height="42"></a><!-- </open-collective-sponsors> -->
<a title="ssddanbrown" href="https://github.com/ssddanbrown" target="_blank"><img alt="ssddanbrown" src="https://avatars.githubusercontent.com/u/8343178?s=128&v=4" width="42" height="42"></a></details><!-- </open-collective-sponsors> -->
[[See all](https://carbon.nesbot.com/#sponsors)]
[[Become a sponsor via OpenCollective*](https://opencollective.com/Carbon#sponsor)]
<a href="https://github.com/ssddanbrown" target="_blank"><img src="https://avatars.githubusercontent.com/u/8343178?s=128&v=4" width="42" height="42"></a>
<a href="https://github.com/BallymaloeCookerySchool" target="_blank"><img src="https://avatars.githubusercontent.com/u/123261043?s=128&v=4" width="42" height="42"></a>
[[Become a sponsor via OpenCollective*](https://opencollective.com/Carbon#sponsor)]
[[Become a sponsor via GitHub*](https://github.com/sponsors/kylekatarnls)]

View file

@ -158,7 +158,10 @@ function getOpenCollectiveSponsors(): string
$status = null;
$rank = 0;
if ($monthlyContribution > 29 || $yearlyContribution > 700) {
if ($monthlyContribution > 50 || $yearlyContribution > 900) {
$status = 'sponsor';
$rank = 5;
} elseif ($monthlyContribution > 29 || $yearlyContribution > 700) {
$status = 'sponsor';
$rank = 4;
} elseif ($monthlyContribution > 14.5 || $yearlyContribution > 500) {
@ -190,6 +193,7 @@ function getOpenCollectiveSponsors(): string
$membersByUrl = [];
$output = '';
$extra = '';
foreach ($list as $member) {
$url = $member['website'] ?? $member['profile'];
@ -229,12 +233,30 @@ function getOpenCollectiveSponsors(): string
$height *= 1.5;
}
$output .= "\n".'<a title="'.$title.'" href="'.$href.'" target="_blank"'.$rel.'>'.
$link = "\n".'<a title="'.$title.'" href="'.$href.'" target="_blank"'.$rel.'>'.
'<img alt="'.$alt.'" src="'.$src.'" width="'.$width.'" height="'.$height.'">'.
'</a>';
if ($member['rank'] >= 5) {
$output .= $link;
continue;
}
$extra .= $link;
}
$github = [
8343178 => 'ssddanbrown',
];
foreach ($github as $avatar => $user) {
$extra .= "\n".'<a title="'.$user.'" href="https://github.com/'.$user.'" target="_blank">'.
'<img alt="'.$user.'" src="https://avatars.githubusercontent.com/u/'.$avatar.'?s=128&v=4" width="42" height="42">'.
'</a>';
}
return $output;
return $output.'<details><summary>See more</summary>'.$extra.'</details>';
}
file_put_contents('readme.md', preg_replace_callback(

View file

@ -533,7 +533,7 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
($totalDays - $this->d).' days '.
($hours - $this->h).' hours '.
($minutes - $this->i).' minutes '.
($intervalSeconds - $this->s).' seconds '.
number_format($intervalSeconds - $this->s, 6, '.', '').' seconds '.
($microseconds - $intervalMicroseconds).' microseconds ',
));
}
@ -1082,7 +1082,7 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
default:
throw new InvalidIntervalException(
\sprintf('Invalid part %s in definition %s', $part, $intervalDefinition),
"Invalid part $part in definition $intervalDefinition",
);
}
}
@ -3088,16 +3088,50 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
// PHP <= 8.1
// @codeCoverageIgnoreStart
foreach ($properties as $property => $value) {
$name = preg_replace('/^\0.+\0/', '', $property);
$properties = array_combine(
array_map(
static fn (string $property) => preg_replace('/^\0.+\0/', '', $property),
array_keys($data),
),
$data,
);
$localStrictMode = $this->localStrictModeEnabled;
$this->localStrictModeEnabled = false;
$this->$name = $value;
$days = $properties['days'] ?? false;
$this->days = $days === false ? false : (int) $days;
$this->y = (int) ($properties['y'] ?? 0);
$this->m = (int) ($properties['m'] ?? 0);
$this->d = (int) ($properties['d'] ?? 0);
$this->h = (int) ($properties['h'] ?? 0);
$this->i = (int) ($properties['i'] ?? 0);
$this->s = (int) ($properties['s'] ?? 0);
$this->f = (float) ($properties['f'] ?? 0.0);
// @phpstan-ignore-next-line
$this->weekday = (int) ($properties['weekday'] ?? 0);
// @phpstan-ignore-next-line
$this->weekday_behavior = (int) ($properties['weekday_behavior'] ?? 0);
// @phpstan-ignore-next-line
$this->first_last_day_of = (int) ($properties['first_last_day_of'] ?? 0);
$this->invert = (int) ($properties['invert'] ?? 0);
// @phpstan-ignore-next-line
$this->special_type = (int) ($properties['special_type'] ?? 0);
// @phpstan-ignore-next-line
$this->special_amount = (int) ($properties['special_amount'] ?? 0);
// @phpstan-ignore-next-line
$this->have_weekday_relative = (int) ($properties['have_weekday_relative'] ?? 0);
// @phpstan-ignore-next-line
$this->have_special_relative = (int) ($properties['have_special_relative'] ?? 0);
parent::__construct(self::getDateIntervalSpec($this));
if ($name !== 'localStrictModeEnabled') {
$this->localStrictModeEnabled = $localStrictMode;
foreach ($properties as $property => $value) {
if ($property === 'localStrictModeEnabled') {
continue;
}
$this->$property = $value;
}
$this->localStrictModeEnabled = $properties['localStrictModeEnabled'] ?? $localStrictMode;
// @codeCoverageIgnoreEnd
}
@ -3156,7 +3190,7 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
}
$microseconds = $interval->f;
$instance = new $className(static::getDateIntervalSpec($interval, false, $skip));
$instance = self::buildInstance($interval, $className, $skip);
if ($instance instanceof self) {
$instance->originalInput = $interval;
@ -3175,6 +3209,83 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
return self::withOriginal($instance, $interval);
}
/**
* @template T of DateInterval
*
* @param DateInterval $interval
*
* @psalm-param class-string<T> $className
*
* @return T
*/
private static function buildInstance(
DateInterval $interval,
string $className,
array $skip = [],
): object {
$serialization = self::buildSerializationString($interval, $className, $skip);
return match ($serialization) {
null => new $className(static::getDateIntervalSpec($interval, false, $skip)),
default => unserialize($serialization),
};
}
/**
* As demonstrated by rlanvin (https://github.com/rlanvin) in
* https://github.com/briannesbitt/Carbon/issues/3018#issuecomment-2888538438
*
* Modifying the output of serialize() to change the class name and unserializing
* the tweaked string allows creating new interval instances where the ->days
* property can be set. It's not possible neither with `new` nto with `__set_state`.
*
* It has a non-negligible performance cost, so we'll use this method only if
* $interval->days !== false.
*/
private static function buildSerializationString(
DateInterval $interval,
string $className,
array $skip = [],
): ?string {
if ($interval->days === false || PHP_VERSION_ID < 8_02_00 || $skip !== []) {
return null;
}
// De-enhance CarbonInterval objects to be serializable back to DateInterval
if ($interval instanceof self && !is_a($className, self::class, true)) {
$interval = clone $interval;
unset($interval->timezoneSetting);
unset($interval->originalInput);
unset($interval->startDate);
unset($interval->endDate);
unset($interval->rawInterval);
unset($interval->absolute);
unset($interval->initialValues);
unset($interval->clock);
unset($interval->step);
unset($interval->localMonthsOverflow);
unset($interval->localYearsOverflow);
unset($interval->localStrictModeEnabled);
unset($interval->localHumanDiffOptions);
unset($interval->localToStringFormat);
unset($interval->localSerializer);
unset($interval->localMacros);
unset($interval->localGenericMacros);
unset($interval->localFormatFunction);
unset($interval->localTranslator);
}
$serialization = serialize($interval);
$inputClass = $interval::class;
$expectedStart = 'O:'.\strlen($inputClass).':"'.$inputClass.'":';
if (!str_starts_with($serialization, $expectedStart)) {
return null; // @codeCoverageIgnore
}
return 'O:'.\strlen($className).':"'.$className.'":'.substr($serialization, \strlen($expectedStart));
}
private static function copyStep(self $from, self $to): void
{
$to->setStep($from->getStep());
@ -3408,7 +3519,8 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
return;
}
if (PHP_VERSION_ID !== 80320) {
// @codeCoverageIgnoreStart
if (PHP_VERSION_ID !== 8_03_20) {
$instance->$unit += $value;
return;
@ -3416,8 +3528,10 @@ class CarbonInterval extends DateInterval implements CarbonConverterInterface
// Cannot use +=, nor set to a negative value directly as it segfaults in PHP 8.3.20
self::setIntervalUnit($instance, $unit, ($instance->$unit ?? 0) + $value);
// @codeCoverageIgnoreEnd
}
/** @codeCoverageIgnore */
private static function setIntervalUnit(DateInterval $instance, string $unit, mixed $value): void
{
switch ($unit) {

View file

@ -176,9 +176,9 @@ require PHP_VERSION < 8.2
*
* @mixin DeprecatedPeriodProperties
*
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CamelCasePropertyName)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(TooManyFields)
* @SuppressWarnings(CamelCasePropertyName)
* @SuppressWarnings(CouplingBetweenObjects)
*/
class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
{
@ -414,16 +414,19 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
$instance = static::createFromArray($params);
if ($options !== null) {
$instance->options = $options;
$instance->options = ($instance instanceof CarbonPeriodImmutable ? static::IMMUTABLE : 0) | $options;
$instance->handleChangedParameters();
}
return $instance;
}
public static function createFromISO8601String(string $iso, ?int $options = null): static
{
return self::createFromIso($iso, $options);
}
/**
* Return whether given interval contains non zero value of any time unit.
* Return whether the given interval contains non-zero value of any time unit.
*/
protected static function intervalHasTime(DateInterval $interval): bool
{
@ -453,7 +456,7 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
/**
* Parse given ISO 8601 string into an array of arguments.
*
* @SuppressWarnings(PHPMD.ElseExpression)
* @SuppressWarnings(ElseExpression)
*/
protected static function parseIso8601(string $iso): array
{
@ -597,7 +600,7 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
/**
* CarbonPeriod constructor.
*
* @SuppressWarnings(PHPMD.ElseExpression)
* @SuppressWarnings(ElseExpression)
*
* @throws InvalidArgumentException
*/
@ -725,9 +728,10 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
parent::__construct(
$this->startDate,
$this->dateInterval,
$this->endDate ?? $this->recurrences ?? 1,
$this->endDate ?? max(1, min(2147483639, $this->recurrences ?? 1)),
$this->options,
);
$this->constructed = true;
}
@ -1115,7 +1119,7 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
/**
* Add a filter to the stack.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(UnusedFormalParameter)
*/
public function addFilter(callable|string $callback, ?string $name = null): static
{
@ -1132,7 +1136,7 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
/**
* Prepend a filter to the stack.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(UnusedFormalParameter)
*/
public function prependFilter(callable|string $callback, ?string $name = null): static
{
@ -2245,11 +2249,100 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
public function __debugInfo(): array
{
$info = $this->baseDebugInfo();
unset($info['start'], $info['end'], $info['interval'], $info['include_start_date'], $info['include_end_date']);
unset(
$info['start'],
$info['end'],
$info['interval'],
$info['include_start_date'],
$info['include_end_date'],
$info['constructed'],
$info["\0*\0constructed"],
);
return $info;
}
public function __unserialize(array $data): void
{
try {
$values = array_combine(
array_map(
static fn (string $key): string => preg_replace('/^\0\*\0/', '', $key),
array_keys($data),
),
$data,
);
$this->initializeSerialization($values);
foreach ($values as $key => $value) {
if ($value === null) {
continue;
}
$property = match ($key) {
'tzName' => $this->setTimezone(...),
'options' => $this->setOptions(...),
'recurrences' => $this->setRecurrences(...),
'current' => function (mixed $current): void {
if (!($current instanceof CarbonInterface)) {
$current = $this->resolveCarbon($current);
}
$this->carbonCurrent = $current;
},
'start' => 'startDate',
'interval' => $this->setDateInterval(...),
'end' => 'endDate',
'key' => null,
'include_start_date' => function (bool $included): void {
$this->excludeStartDate(!$included);
},
'include_end_date' => function (bool $included): void {
$this->excludeEndDate(!$included);
},
default => $key,
};
if ($property === null) {
continue;
}
if (\is_callable($property)) {
$property($value);
continue;
}
if ($value instanceof DateTimeInterface && !($value instanceof CarbonInterface)) {
$value = ($value instanceof DateTime)
? Carbon::instance($value)
: CarbonImmutable::instance($value);
}
try {
$this->$property = $value;
} catch (Throwable) {
// Must be ignored for backward-compatibility
}
}
if (\array_key_exists('carbonRecurrences', $values)) {
$this->carbonRecurrences = $values['carbonRecurrences'];
} elseif (((int) ($values['recurrences'] ?? 0)) <= 1 && $this->endDate !== null) {
$this->carbonRecurrences = null;
}
} catch (Throwable $e) {
// @codeCoverageIgnoreStart
if (!method_exists(parent::class, '__unserialize')) {
throw $e;
}
parent::__unserialize($data);
// @codeCoverageIgnoreEnd
}
}
/**
* Update properties after removing built-in filters.
*/
@ -2293,7 +2386,7 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
/**
* Recurrences filter callback (limits number of recurrences).
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(UnusedFormalParameter)
*/
protected function filterRecurrences(CarbonInterface $current, int $key): bool|callable
{
@ -2578,4 +2671,47 @@ class CarbonPeriod extends DatePeriodBase implements Countable, JsonSerializable
return $sortedArguments;
}
private function initializeSerialization(array $values): void
{
$serializationBase = [
'start' => $values['start'] ?? $values['startDate'] ?? null,
'current' => $values['current'] ?? $values['carbonCurrent'] ?? null,
'end' => $values['end'] ?? $values['endDate'] ?? null,
'interval' => $values['interval'] ?? $values['dateInterval'] ?? null,
'recurrences' => max(1, (int) ($values['recurrences'] ?? $values['carbonRecurrences'] ?? 1)),
'include_start_date' => $values['include_start_date'] ?? true,
'include_end_date' => $values['include_end_date'] ?? false,
];
foreach (['start', 'current', 'end'] as $dateProperty) {
if ($serializationBase[$dateProperty] instanceof Carbon) {
$serializationBase[$dateProperty] = $serializationBase[$dateProperty]->toDateTime();
} elseif ($serializationBase[$dateProperty] instanceof CarbonInterface) {
$serializationBase[$dateProperty] = $serializationBase[$dateProperty]->toDateTimeImmutable();
}
}
if ($serializationBase['interval'] instanceof CarbonInterval) {
$serializationBase['interval'] = $serializationBase['interval']->toDateInterval();
}
// @codeCoverageIgnoreStart
if (method_exists(parent::class, '__unserialize')) {
parent::__unserialize($serializationBase);
return;
}
$excludeStart = !($values['include_start_date'] ?? true);
$includeEnd = $values['include_end_date'] ?? true;
parent::__construct(
$serializationBase['start'],
$serializationBase['interval'],
$serializationBase['end'] ?? $serializationBase['recurrences'],
($excludeStart ? self::EXCLUDE_START_DATE : 0) | ($includeEnd && \defined('DatePeriod::INCLUDE_END_DATE') ? self::INCLUDE_END_DATE : 0),
);
// @codeCoverageIgnoreEnd
}
}

View file

@ -16,6 +16,7 @@ namespace Carbon;
use Carbon\Exceptions\InvalidCastException;
use Carbon\Exceptions\InvalidTimeZoneException;
use Carbon\Traits\LocalFactory;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Exception;
@ -129,10 +130,23 @@ class CarbonTimeZone extends DateTimeZone
{
$name = $this->getName();
foreach ($this->listAbbreviations() as $abbreviation => $zones) {
$date = new DateTimeImmutable($dst ? 'July 1' : 'January 1', $this);
$timezone = $date->format('T');
$abbreviations = $this->listAbbreviations();
$matchingZones = array_merge($abbreviations[$timezone] ?? [], $abbreviations[strtolower($timezone)] ?? []);
if ($matchingZones !== []) {
foreach ($matchingZones as $zone) {
if ($zone['timezone_id'] === $name && $zone['dst'] == $dst) {
return $timezone;
}
}
}
foreach ($abbreviations as $abbreviation => $zones) {
foreach ($zones as $zone) {
if ($zone['timezone_id'] === $name && $zone['dst'] == $dst) {
return $abbreviation;
return strtoupper($abbreviation);
}
}
}

View file

@ -22,25 +22,25 @@
*/
return [
'year' => ':count il',
'a_year' => '{1}bir il|]1,Inf[:count il',
'a_year' => '{1}bir il|[-Inf,Inf]:count il',
'y' => ':count il',
'month' => ':count ay',
'a_month' => '{1}bir ay|]1,Inf[:count ay',
'a_month' => '{1}bir ay|[-Inf,Inf]:count ay',
'm' => ':count ay',
'week' => ':count həftə',
'a_week' => '{1}bir həftə|]1,Inf[:count həftə',
'a_week' => '{1}bir həftə|[-Inf,Inf]:count həftə',
'w' => ':count h.',
'day' => ':count gün',
'a_day' => '{1}bir gün|]1,Inf[:count gün',
'a_day' => '{1}bir gün|[-Inf,Inf]:count gün',
'd' => ':count g.',
'hour' => ':count saat',
'a_hour' => '{1}bir saat|]1,Inf[:count saat',
'a_hour' => '{1}bir saat|[-Inf,Inf]:count saat',
'h' => ':count s.',
'minute' => ':count dəqiqə',
'a_minute' => '{1}bir dəqiqə|]1,Inf[:count dəqiqə',
'a_minute' => '{1}bir dəqiqə|[-Inf,Inf]:count dəqiqə',
'min' => ':count d.',
'second' => ':count saniyə',
'a_second' => '{1}birneçə saniyə|]1,Inf[:count saniyə',
'a_second' => '{1}birneçə saniyə|[-Inf,Inf]:count saniyə',
's' => ':count san.',
'ago' => ':time əvvəl',
'from_now' => ':time sonra',

View file

@ -15,13 +15,20 @@
* - JD Isaacks
*/
return [
'year' => '{1}ལོ་གཅིག|]1,Inf[:count ལོ',
'month' => '{1}ཟླ་བ་གཅིག|]1,Inf[:count ཟླ་བ',
'week' => ':count བདུན་ཕྲག',
'day' => '{1}ཉིན་གཅིག|]1,Inf[:count ཉིན་',
'hour' => '{1}ཆུ་ཚོད་གཅིག|]1,Inf[:count ཆུ་ཚོད',
'minute' => '{1}སྐར་མ་གཅིག|]1,Inf[:count སྐར་མ',
'second' => '{1}ལམ་སང|]1,Inf[:count སྐར་ཆ།',
'year' => 'ལོ:count',
'a_year' => '{1}ལོ་གཅིག|[-Inf,Inf]ལོ:count',
'month' => 'ཟླ་བ:count',
'a_month' => '{1}ཟླ་བ་གཅིག|[-Inf,Inf]ཟླ་བ:count',
'week' => 'གཟའ་འཁོར་:count',
'a_week' => 'གཟའ་འཁོར་གཅིག',
'day' => 'ཉིན:count་',
'a_day' => '{1}ཉིན་གཅིག|[-Inf,Inf]ཉིན:count',
'hour' => 'ཆུ་ཚོད:count',
'a_hour' => '{1}ཆུ་ཚོད་གཅིག|[-Inf,Inf]ཆུ་ཚོད:count',
'minute' => 'སྐར་མ་:count',
'a_minute' => '{1}སྐར་མ་གཅིག|[-Inf,Inf]སྐར་མ་:count',
'second' => 'སྐར་ཆ:count',
'a_second' => '{01}ལམ་སང|[-Inf,Inf]སྐར་ཆ:count',
'ago' => ':time སྔན་ལ',
'from_now' => ':time ལ་',
'diff_yesterday' => 'ཁ་སང',

View file

@ -16,19 +16,26 @@
* - Daniel Monaghan
*/
return [
'year' => '{1}blwyddyn|]1,Inf[:count flynedd',
'year' => '{1}:count flwyddyn|[-Inf,Inf]:count flynedd',
'a_year' => '{1}blwyddyn|[-Inf,Inf]:count flynedd',
'y' => ':countbl',
'month' => '{1}mis|]1,Inf[:count mis',
'month' => ':count mis',
'a_month' => '{1}mis|[-Inf,Inf]:count mis',
'm' => ':countmi',
'week' => ':count wythnos',
'a_week' => '{1}wythnos|[-Inf,Inf]:count wythnos',
'w' => ':countw',
'day' => '{1}diwrnod|]1,Inf[:count diwrnod',
'day' => ':count diwrnod',
'a_day' => '{1}diwrnod|[-Inf,Inf]:count diwrnod',
'd' => ':countd',
'hour' => '{1}awr|]1,Inf[:count awr',
'hour' => ':count awr',
'a_hour' => '{1}awr|[-Inf,Inf]:count awr',
'h' => ':counth',
'minute' => '{1}munud|]1,Inf[:count munud',
'minute' => ':count munud',
'a_minute' => '{1}munud|[-Inf,Inf]:count munud',
'min' => ':countm',
'second' => '{1}ychydig eiliadau|]1,Inf[:count eiliad',
'second' => ':count eiliad',
'a_second' => '{0,1}ychydig eiliadau|[-Inf,Inf]:count eiliad',
's' => ':counts',
'ago' => ':time yn ôl',
'from_now' => 'mewn :time',

View file

@ -10,18 +10,18 @@
*/
$months = [
ެނުއަރީ',
'ފެބްރުއަރީ',
ަނަވަރީ',
'ފެބުރުވަރީ',
'މާރިޗު',
ޭޕްރީލު',
ެޕްރީލް',
'މޭ',
'ޖޫން',
'ޖުލައި',
ޯގަސްޓު',
'ސެޕްޓެމްބަރު',
'އޮކްޓޯބަރު',
'ނޮވެމްބަރު',
'ޑިސެމްބަރު',
ޮގަސްޓު',
'ސެޕްޓެންބަރު',
'އޮކްޓޫބަރު',
'ނޮވެންބަރު',
'ޑިސެންބަރު',
];
$weekdays = [
@ -38,6 +38,7 @@ $weekdays = [
* Authors:
* - Josh Soref
* - Jawish Hameed
* - Saiph Muhammad
*/
return [
'year' => ':count '.'އަހަރު',

View file

@ -17,37 +17,37 @@
*/
return [
/*
* {1}, {0} and ]1,Inf[ are not needed as it's the default for English pluralization.
* {1}, {0} and [-Inf,Inf] are not needed as it's the default for English pluralization.
* But as some languages are using en.php as a fallback, it's better to specify it
* explicitly so those languages also fallback to English pluralization when a unit
* is missing.
*/
'year' => '{1}:count year|{0}:count years|]1,Inf[:count years',
'a_year' => '{1}a year|{0}:count years|]1,Inf[:count years',
'y' => '{1}:countyr|{0}:countyrs|]1,Inf[:countyrs',
'month' => '{1}:count month|{0}:count months|]1,Inf[:count months',
'a_month' => '{1}a month|{0}:count months|]1,Inf[:count months',
'm' => '{1}:countmo|{0}:countmos|]1,Inf[:countmos',
'week' => '{1}:count week|{0}:count weeks|]1,Inf[:count weeks',
'a_week' => '{1}a week|{0}:count weeks|]1,Inf[:count weeks',
'year' => '{1}:count year|{0}:count years|[-Inf,Inf]:count years',
'a_year' => '{1}a year|{0}:count years|[-Inf,Inf]:count years',
'y' => '{1}:countyr|{0}:countyrs|[-Inf,Inf]:countyrs',
'month' => '{1}:count month|{0}:count months|[-Inf,Inf]:count months',
'a_month' => '{1}a month|{0}:count months|[-Inf,Inf]:count months',
'm' => '{1}:countmo|{0}:countmos|[-Inf,Inf]:countmos',
'week' => '{1}:count week|{0}:count weeks|[-Inf,Inf]:count weeks',
'a_week' => '{1}a week|{0}:count weeks|[-Inf,Inf]:count weeks',
'w' => ':countw',
'day' => '{1}:count day|{0}:count days|]1,Inf[:count days',
'a_day' => '{1}a day|{0}:count days|]1,Inf[:count days',
'day' => '{1}:count day|{0}:count days|[-Inf,Inf]:count days',
'a_day' => '{1}a day|{0}:count days|[-Inf,Inf]:count days',
'd' => ':countd',
'hour' => '{1}:count hour|{0}:count hours|]1,Inf[:count hours',
'a_hour' => '{1}an hour|{0}:count hours|]1,Inf[:count hours',
'hour' => '{1}:count hour|{0}:count hours|[-Inf,Inf]:count hours',
'a_hour' => '{1}an hour|{0}:count hours|[-Inf,Inf]:count hours',
'h' => ':counth',
'minute' => '{1}:count minute|{0}:count minutes|]1,Inf[:count minutes',
'a_minute' => '{1}a minute|{0}:count minutes|]1,Inf[:count minutes',
'minute' => '{1}:count minute|{0}:count minutes|[-Inf,Inf]:count minutes',
'a_minute' => '{1}a minute|{0}:count minutes|[-Inf,Inf]:count minutes',
'min' => ':countm',
'second' => '{1}:count second|{0}:count seconds|]1,Inf[:count seconds',
'a_second' => '{1}a few seconds|{0}:count seconds|]1,Inf[:count seconds',
'second' => '{1}:count second|{0}:count seconds|[-Inf,Inf]:count seconds',
'a_second' => '{0,1}a few seconds|[-Inf,Inf]:count seconds',
's' => ':counts',
'millisecond' => '{1}:count millisecond|{0}:count milliseconds|]1,Inf[:count milliseconds',
'a_millisecond' => '{1}a millisecond|{0}:count milliseconds|]1,Inf[:count milliseconds',
'millisecond' => '{1}:count millisecond|{0}:count milliseconds|[-Inf,Inf]:count milliseconds',
'a_millisecond' => '{1}a millisecond|{0}:count milliseconds|[-Inf,Inf]:count milliseconds',
'ms' => ':countms',
'microsecond' => '{1}:count microsecond|{0}:count microseconds|]1,Inf[:count microseconds',
'a_microsecond' => '{1}a microsecond|{0}:count microseconds|]1,Inf[:count microseconds',
'microsecond' => '{1}:count microsecond|{0}:count microseconds|[-Inf,Inf]:count microseconds',
'a_microsecond' => '{1}a microsecond|{0}:count microseconds|[-Inf,Inf]:count microseconds',
'µs' => ':countµs',
'ago' => ':time ago',
'from_now' => ':time from now',
@ -59,7 +59,7 @@ return [
'diff_tomorrow' => 'tomorrow',
'diff_before_yesterday' => 'before yesterday',
'diff_after_tomorrow' => 'after tomorrow',
'period_recurrences' => '{1}once|{0}:count times|]1,Inf[:count times',
'period_recurrences' => '{1}once|{0}:count times|[-Inf,Inf]:count times',
'period_interval' => 'every :interval',
'period_start_date' => 'from :date',
'period_end_date' => 'to :date',

View file

@ -21,25 +21,25 @@
*/
return [
'year' => ':count tahun',
'a_year' => '{1}setahun|]1,Inf[:count tahun',
'a_year' => '{1}setahun|[-Inf,Inf]:count tahun',
'y' => ':countthn',
'month' => ':count bulan',
'a_month' => '{1}sebulan|]1,Inf[:count bulan',
'a_month' => '{1}sebulan|[-Inf,Inf]:count bulan',
'm' => ':countbln',
'week' => ':count minggu',
'a_week' => '{1}seminggu|]1,Inf[:count minggu',
'a_week' => '{1}seminggu|[-Inf,Inf]:count minggu',
'w' => ':countmgg',
'day' => ':count hari',
'a_day' => '{1}sehari|]1,Inf[:count hari',
'a_day' => '{1}sehari|[-Inf,Inf]:count hari',
'd' => ':counthr',
'hour' => ':count jam',
'a_hour' => '{1}sejam|]1,Inf[:count jam',
'a_hour' => '{1}sejam|[-Inf,Inf]:count jam',
'h' => ':countj',
'minute' => ':count menit',
'a_minute' => '{1}semenit|]1,Inf[:count menit',
'a_minute' => '{1}semenit|[-Inf,Inf]:count menit',
'min' => ':countmnt',
'second' => ':count detik',
'a_second' => '{1}beberapa detik|]1,Inf[:count detik',
'a_second' => '{1}beberapa detik|[-Inf,Inf]:count detik',
's' => ':countdt',
'ago' => ':time yang lalu',
'from_now' => ':time dari sekarang',

View file

@ -38,7 +38,7 @@ return [
'minute' => ':count分',
'min' => ':count分',
'second' => ':count秒',
'a_second' => '{1}数秒|]1,Inf[:count秒',
'a_second' => '{1}数秒|[-Inf,Inf]:count秒',
's' => ':count秒',
'ago' => ':time前',
'from_now' => ':time後',

View file

@ -16,13 +16,20 @@
* - JD Isaacks
*/
return [
'year' => '{1}setaun|]1,Inf[:count taun',
'month' => '{1}sewulan|]1,Inf[:count wulan',
'week' => '{1}sakminggu|]1,Inf[:count minggu',
'day' => '{1}sedinten|]1,Inf[:count dinten',
'hour' => '{1}setunggal jam|]1,Inf[:count jam',
'minute' => '{1}setunggal menit|]1,Inf[:count menit',
'second' => '{1}sawetawis detik|]1,Inf[:count detik',
'year' => ':count taun',
'a_year' => '{1}setaun|[-Inf,Inf]:count taun',
'month' => ':count wulan',
'a_month' => '{1}sewulan|[-Inf,Inf]:count wulan',
'week' => ':count minggu',
'a_week' => '{1}sakminggu|[-Inf,Inf]:count minggu',
'day' => ':count dina',
'a_day' => '{1}sedina|[-Inf,Inf]:count dina',
'hour' => ':count jam',
'a_hour' => '{1}setunggal jam|[-Inf,Inf]:count jam',
'minute' => ':count menit',
'a_minute' => '{1}setunggal menit|[-Inf,Inf]:count menit',
'second' => ':count detik',
'a_second' => '{0,1}sawetawis detik|[-Inf,Inf]:count detik',
'ago' => ':time ingkang kepengker',
'from_now' => 'wonten ing :time',
'diff_today' => 'Dinten',

View file

@ -30,25 +30,25 @@ use Carbon\CarbonInterface;
return [
'year' => ':count წელი',
'y' => ':count წელი',
'a_year' => '{1}წელი|]1,Inf[:count წელი',
'a_year' => '{1}წელი|[-Inf,Inf]:count წელი',
'month' => ':count თვე',
'm' => ':count თვე',
'a_month' => '{1}თვე|]1,Inf[:count თვე',
'a_month' => '{1}თვე|[-Inf,Inf]:count თვე',
'week' => ':count კვირა',
'w' => ':count კვირა',
'a_week' => '{1}კვირა|]1,Inf[:count კვირა',
'a_week' => '{1}კვირა|[-Inf,Inf]:count კვირა',
'day' => ':count დღე',
'd' => ':count დღე',
'a_day' => '{1}დღე|]1,Inf[:count დღე',
'a_day' => '{1}დღე|[-Inf,Inf]:count დღე',
'hour' => ':count საათი',
'h' => ':count საათი',
'a_hour' => '{1}საათი|]1,Inf[:count საათი',
'a_hour' => '{1}საათი|[-Inf,Inf]:count საათი',
'minute' => ':count წუთი',
'min' => ':count წუთი',
'a_minute' => '{1}წუთი|]1,Inf[:count წუთი',
'a_minute' => '{1}წუთი|[-Inf,Inf]:count წუთი',
'second' => ':count წამი',
's' => ':count წამი',
'a_second' => '{1}რამდენიმე წამი|]1,Inf[:count წამი',
'a_second' => '{1}რამდენიმე წამი|[-Inf,Inf]:count წამი',
'ago' => static function ($time) {
$replacements = [
// year

View file

@ -31,32 +31,32 @@ return array_replace_recursive(require __DIR__.'/en.php', [
'first_day_of_week' => 1,
'day_of_first_week_of_year' => 1,
'year' => '{1}ukioq :count|{0}:count ukiut|]1,Inf[ukiut :count',
'a_year' => '{1}ukioq|{0}:count ukiut|]1,Inf[ukiut :count',
'y' => '{1}:countyr|{0}:countyrs|]1,Inf[:countyrs',
'year' => '{1}ukioq :count|{0}:count ukiut|[-Inf,Inf]ukiut :count',
'a_year' => '{1}ukioq|{0}:count ukiut|[-Inf,Inf]ukiut :count',
'y' => '{1}:countyr|{0}:countyrs|[-Inf,Inf]:countyrs',
'month' => '{1}qaammat :count|{0}:count qaammatit|]1,Inf[qaammatit :count',
'a_month' => '{1}qaammat|{0}:count qaammatit|]1,Inf[qaammatit :count',
'm' => '{1}:countmo|{0}:countmos|]1,Inf[:countmos',
'month' => '{1}qaammat :count|{0}:count qaammatit|[-Inf,Inf]qaammatit :count',
'a_month' => '{1}qaammat|{0}:count qaammatit|[-Inf,Inf]qaammatit :count',
'm' => '{1}:countmo|{0}:countmos|[-Inf,Inf]:countmos',
'week' => '{1}:count sap. ak.|{0}:count sap. ak.|]1,Inf[:count sap. ak.',
'a_week' => '{1}a sap. ak.|{0}:count sap. ak.|]1,Inf[:count sap. ak.',
'week' => '{1}:count sap. ak.|{0}:count sap. ak.|[-Inf,Inf]:count sap. ak.',
'a_week' => '{1}a sap. ak.|{0}:count sap. ak.|[-Inf,Inf]:count sap. ak.',
'w' => ':countw',
'day' => '{1}:count ulloq|{0}:count ullut|]1,Inf[:count ullut',
'a_day' => '{1}a ulloq|{0}:count ullut|]1,Inf[:count ullut',
'day' => '{1}:count ulloq|{0}:count ullut|[-Inf,Inf]:count ullut',
'a_day' => '{1}a ulloq|{0}:count ullut|[-Inf,Inf]:count ullut',
'd' => ':countd',
'hour' => '{1}:count tiimi|{0}:count tiimit|]1,Inf[:count tiimit',
'a_hour' => '{1}tiimi|{0}:count tiimit|]1,Inf[:count tiimit',
'hour' => '{1}:count tiimi|{0}:count tiimit|[-Inf,Inf]:count tiimit',
'a_hour' => '{1}tiimi|{0}:count tiimit|[-Inf,Inf]:count tiimit',
'h' => ':counth',
'minute' => '{1}:count minutsi|{0}:count minutsit|]1,Inf[:count minutsit',
'a_minute' => '{1}a minutsi|{0}:count minutsit|]1,Inf[:count minutsit',
'minute' => '{1}:count minutsi|{0}:count minutsit|[-Inf,Inf]:count minutsit',
'a_minute' => '{1}a minutsi|{0}:count minutsit|[-Inf,Inf]:count minutsit',
'min' => ':countm',
'second' => '{1}:count sikunti|{0}:count sikuntit|]1,Inf[:count sikuntit',
'a_second' => '{1}sikunti|{0}:count sikuntit|]1,Inf[:count sikuntit',
'second' => '{1}:count sikunti|{0}:count sikuntit|[-Inf,Inf]:count sikuntit',
'a_second' => '{1}sikunti|{0}:count sikuntit|[-Inf,Inf]:count sikuntit',
's' => ':counts',
'ago' => ':time matuma siorna',

View file

@ -17,19 +17,25 @@
* - Sovichet Tep
*/
return [
'year' => '{1}មួយឆ្នាំ|]1,Inf[:count ឆ្នាំ',
'year' => ':count ឆ្នាំ',
'a_year' => '{1}មួយឆ្នាំ|[-Inf,Inf]:count ឆ្នាំ',
'y' => ':count ឆ្នាំ',
'month' => '{1}មួយខែ|]1,Inf[:count ខែ',
'month' => ':count ខែ',
'a_month' => '{1}មួយខែ|[-Inf,Inf]:count ខែ',
'm' => ':count ខែ',
'week' => ':count សប្ដាហ៍',
'w' => ':count សប្ដាហ៍',
'day' => '{1}មួយថ្ងៃ|]1,Inf[:count ថ្ងៃ',
'week' => ':count សប្តាហ៍',
'w' => ':count សប្តាហ៍',
'day' => ':count ថ្ងៃ',
'a_day' => '{1}មួយថ្ងៃ|[-Inf,Inf]:count ថ្ងៃ',
'd' => ':count ថ្ងៃ',
'hour' => '{1}មួយម៉ោង|]1,Inf[:count ម៉ោង',
'hour' => ':count ម៉ោង',
'a_hour' => '{1}មួយម៉ោង|[-Inf,Inf]:count ម៉ោង',
'h' => ':count ម៉ោង',
'minute' => '{1}មួយនាទី|]1,Inf[:count នាទី',
'minute' => ':count នាទី',
'a_minute' => '{1}មួយនាទី|[-Inf,Inf]:count នាទី',
'min' => ':count នាទី',
'second' => '{1}ប៉ុន្មានវិនាទី|]1,Inf[:count វិនាទី',
'second' => ':count វិនាទី',
'a_second' => '{0,1}ប៉ុន្មានវិនាទី|[-Inf,Inf]:count វិនាទី',
's' => ':count វិនាទី',
'ago' => ':timeមុន',
'from_now' => ':timeទៀត',

View file

@ -17,13 +17,20 @@
* - rajeevnaikte
*/
return [
'year' => '{1}ಒಂದು ವರ್ಷ|]1,Inf[:count ವರ್ಷ',
'month' => '{1}ಒಂದು ತಿಂಗಳು|]1,Inf[:count ತಿಂಗಳು',
'week' => '{1}ಒಂದು ವಾರ|]1,Inf[:count ವಾರಗಳು',
'day' => '{1}ಒಂದು ದಿನ|]1,Inf[:count ದಿನ',
'hour' => '{1}ಒಂದು ಗಂಟೆ|]1,Inf[:count ಗಂಟೆ',
'minute' => '{1}ಒಂದು ನಿಮಿಷ|]1,Inf[:count ನಿಮಿಷ',
'second' => '{1}ಕೆಲವು ಕ್ಷಣಗಳು|]1,Inf[:count ಸೆಕೆಂಡುಗಳು',
'year' => '{1}:count ವರ್ಷ|[-Inf,Inf]:count ವರ್ಷಗಳು',
'a_year' => '{1}ಒಂದು ವರ್ಷ|[-Inf,Inf]:count ವರ್ಷಗಳು',
'month' => ':count ತಿಂಗಳು',
'a_month' => '{1}ಒಂದು ತಿಂಗಳು|[-Inf,Inf]:count ತಿಂಗಳು',
'week' => '{1}:count ವಾರ|[-Inf,Inf]:count ವಾರಗಳು',
'a_week' => '{1}ಒಂದು ವಾರ|[-Inf,Inf]:count ವಾರಗಳು',
'day' => '{1}:count ದಿನ|[-Inf,Inf]:count ದಿನಗಳು',
'a_day' => '{1}ಒಂದು ದಿನ|[-Inf,Inf]:count ದಿನಗಳು',
'hour' => '{1}:count ಗಂಟೆ|[-Inf,Inf]:count ಗಂಟೆಗಳು',
'a_hour' => '{1}ಒಂದು ಗಂಟೆ|[-Inf,Inf]:count ಗಂಟೆಗಳು',
'minute' => '{1}:count ನಿಮಿಷ|[-Inf,Inf]:count ನಿಮಿಷಗಳು',
'a_minute' => '{1}ಒಂದು ನಿಮಿಷ|[-Inf,Inf]:count ನಿಮಿಷಗಳು',
'second' => '{0,1}:count ಸೆಕೆಂಡ್|[-Inf,Inf]:count ಸೆಕೆಂಡುಗಳು',
'a_second' => '{0,1}ಕೆಲವು ಕ್ಷಣಗಳು|[-Inf,Inf]:count ಸೆಕೆಂಡುಗಳು',
'ago' => ':time ಹಿಂದೆ',
'from_now' => ':time ನಂತರ',
'diff_now' => 'ಈಗ',

View file

@ -22,25 +22,25 @@
*/
return [
'year' => ':count년',
'a_year' => '{1}일년|]1,Inf[:count년',
'a_year' => '{1}일년|[-Inf,Inf]:count년',
'y' => ':count년',
'month' => ':count개월',
'a_month' => '{1}한달|]1,Inf[:count개월',
'a_month' => '{1}한달|[-Inf,Inf]:count개월',
'm' => ':count개월',
'week' => ':count주',
'a_week' => '{1}일주일|]1,Inf[:count 주',
'a_week' => '{1}일주일|[-Inf,Inf]:count 주',
'w' => ':count주일',
'day' => ':count일',
'a_day' => '{1}하루|]1,Inf[:count일',
'a_day' => '{1}하루|[-Inf,Inf]:count일',
'd' => ':count일',
'hour' => ':count시간',
'a_hour' => '{1}한시간|]1,Inf[:count시간',
'a_hour' => '{1}한시간|[-Inf,Inf]:count시간',
'h' => ':count시간',
'minute' => ':count분',
'a_minute' => '{1}일분|]1,Inf[:count분',
'a_minute' => '{1}일분|[-Inf,Inf]:count분',
'min' => ':count분',
'second' => ':count초',
'a_second' => '{1}몇초|]1,Inf[:count초',
'a_second' => '{1}몇초|[-Inf,Inf]:count초',
's' => ':count초',
'ago' => ':time 전',
'from_now' => ':time 후',

View file

@ -27,7 +27,8 @@ return [
'h' => ':count ຊມ. ',
'minute' => ':count ນາທີ',
'min' => ':count ນທ. ',
'second' => '{1}ບໍ່ເທົ່າໃດວິນາທີ|]1,Inf[:count ວິນາທີ',
'second' => ':count ວິນາທີ',
'a_second' => '{0,1}ບໍ່ເທົ່າໃດວິນາທີ|[-Inf,Inf]:count ວິນາທີ',
's' => ':count ວິ. ',
'ago' => ':timeຜ່ານມາ',
'from_now' => 'ອີກ :time',

View file

@ -21,29 +21,29 @@
*/
return [
'year' => ':count tahun',
'a_year' => '{1}setahun|]1,Inf[:count tahun',
'a_year' => '{1}setahun|[-Inf,Inf]:count tahun',
'y' => ':count tahun',
'month' => ':count bulan',
'a_month' => '{1}sebulan|]1,Inf[:count bulan',
'a_month' => '{1}sebulan|[-Inf,Inf]:count bulan',
'm' => ':count bulan',
'week' => ':count minggu',
'a_week' => '{1}seminggu|]1,Inf[:count minggu',
'a_week' => '{1}seminggu|[-Inf,Inf]:count minggu',
'w' => ':count minggu',
'day' => ':count hari',
'a_day' => '{1}sehari|]1,Inf[:count hari',
'a_day' => '{1}sehari|[-Inf,Inf]:count hari',
'd' => ':count hari',
'hour' => ':count jam',
'a_hour' => '{1}sejam|]1,Inf[:count jam',
'a_hour' => '{1}sejam|[-Inf,Inf]:count jam',
'h' => ':count jam',
'minute' => ':count minit',
'a_minute' => '{1}seminit|]1,Inf[:count minit',
'a_minute' => '{1}seminit|[-Inf,Inf]:count minit',
'min' => ':count minit',
'second' => ':count saat',
'a_second' => '{1}beberapa saat|]1,Inf[:count saat',
'a_second' => '{1}beberapa saat|[-Inf,Inf]:count saat',
'millisecond' => ':count milisaat',
'a_millisecond' => '{1}semilisaat|]1,Inf[:count milliseconds',
'a_millisecond' => '{1}semilisaat|[-Inf,Inf]:count milliseconds',
'microsecond' => ':count mikrodetik',
'a_microsecond' => '{1}semikrodetik|]1,Inf[:count mikrodetik',
'a_microsecond' => '{1}semikrodetik|[-Inf,Inf]:count mikrodetik',
's' => ':count saat',
'ago' => ':time yang lepas',
'from_now' => ':time dari sekarang',

View file

@ -16,19 +16,25 @@
* - Nay Lin Aung
*/
return [
'year' => '{1}တစ်နှစ်|]1,Inf[:count နှစ်',
'year' => ':count နှစ်',
'a_year' => '{1}တစ်နှစ်|[-Inf,Inf]:count နှစ်',
'y' => ':count နှစ်',
'month' => '{1}တစ်လ|]1,Inf[:count လ',
'month' => ':count လ',
'a_month' => '{1}တစ်လ|[-Inf,Inf]:count လ',
'm' => ':count လ',
'week' => ':count ပတ်',
'w' => ':count ပတ်',
'day' => '{1}တစ်ရက်|]1,Inf[:count ရက်',
'day' => ':count ရက်',
'a_day' => '{1}တစ်ရက်|[-Inf,Inf]:count ရက်',
'd' => ':count ရက်',
'hour' => '{1}တစ်နာရီ|]1,Inf[:count နာရီ',
'hour' => ':count နာရီ',
'a_hour' => '{1}တစ်နာရီ|[-Inf,Inf]:count နာရီ',
'h' => ':count နာရီ',
'minute' => '{1}တစ်မိနစ်|]1,Inf[:count မိနစ်',
'minute' => ':count မိနစ်',
'a_minute' => '{1}တစ်မိနစ်|[-Inf,Inf]:count မိနစ်',
'min' => ':count မိနစ်',
'second' => '{1}စက္ကန်.အနည်းငယ်|]1,Inf[:count စက္ကန့်',
'second' => ':count စက္ကန့်',
'a_second' => '{0,1}စက္ကန်.အနည်းငယ်|[-Inf,Inf]:count စက္ကန့်',
's' => ':count စက္ကန့်',
'ago' => 'လွန်ခဲ့သော :time က',
'from_now' => 'လာမည့် :time မှာ',

View file

@ -38,16 +38,22 @@ $weekdays = [
* Authors:
* - Narain Sagar
* - Sawood Alam
* - Narain Sagar
*/
return [
'year' => '{1}'.'هڪ سال'.'|:count '.'سال',
'month' => '{1}'.'هڪ مهينو'.'|:count '.'مهينا',
'week' => '{1}'.'ھڪ ھفتو'.'|:count '.'هفتا',
'day' => '{1}'.'هڪ ڏينهن'.'|:count '.'ڏينهن',
'hour' => '{1}'.'هڪ ڪلاڪ'.'|:count '.'ڪلاڪ',
'minute' => '{1}'.'هڪ منٽ'.'|:count '.'منٽ',
'second' => '{1}'.'چند سيڪنڊ'.'|:count '.'سيڪنڊ',
'year' => ':count '.'سال',
'a_year' => '{1}'.'هڪ سال'.'|:count '.'سال',
'month' => ':count '.'مهينا',
'a_month' => '{1}'.'هڪ مهينو'.'|:count '.'مهينا',
'week' => ':count '.'هفتا',
'a_week' => '{1}'.'ھڪ ھفتو'.'|:count '.'هفتا',
'day' => ':count '.'ڏينهن',
'a_day' => '{1}'.'هڪ ڏينهن'.'|:count '.'ڏينهن',
'hour' => ':count '.'ڪلاڪ',
'a_hour' => '{1}'.'هڪ ڪلاڪ'.'|:count '.'ڪلاڪ',
'minute' => ':count '.'منٽ',
'a_minute' => '{1}'.'هڪ منٽ'.'|:count '.'منٽ',
'second' => ':count '.'سيڪنڊ',
'a_second' => '{1}'.'چند سيڪنڊ'.'|:count '.'سيڪنڊ',
'ago' => ':time اڳ',
'from_now' => ':time پوء',
'diff_yesterday' => 'ڪالهه',

View file

@ -16,7 +16,7 @@
return array_replace_recursive(require __DIR__.'/en.php', [
'year' => ':count sanad|:count sanadood',
'a_year' => 'sanad|:count sanadood',
'y' => '{1}:countsn|{0}:countsns|]1,Inf[:countsn',
'y' => '{1}:countsn|{0}:countsns|[-Inf,Inf]:countsn',
'month' => ':count bil|:count bilood',
'a_month' => 'bil|:count bilood',
'm' => ':countbil',

View file

@ -26,7 +26,7 @@ return [
'year' => ':count godina|:count godine|:count godina',
'y' => ':count g.',
'month' => ':count mesec|:count meseca|:count meseci',
'm' => ':count mj.',
'm' => ':count mes.',
'week' => ':count nedelja|:count nedelje|:count nedelja',
'w' => ':count ned.',
'day' => ':count dan|:count dana|:count dana',

View file

@ -34,7 +34,7 @@ return [
'minute' => ':count นาที',
'min' => ':count นาที',
'second' => ':count วินาที',
'a_second' => '{1}ไม่กี่วินาที|]1,Inf[:count วินาที',
'a_second' => '{1}ไม่กี่วินาที|[-Inf,Inf]:count วินาที',
's' => ':count วินาที',
'ago' => ':timeที่แล้ว',
'from_now' => 'อีก :time',

View file

@ -23,25 +23,25 @@
*/
return [
'year' => ':count yıl',
'a_year' => '{1}bir yıl|]1,Inf[:count yıl',
'a_year' => '{1}bir yıl|[-Inf,Inf]:count yıl',
'y' => ':county',
'month' => ':count ay',
'a_month' => '{1}bir ay|]1,Inf[:count ay',
'a_month' => '{1}bir ay|[-Inf,Inf]:count ay',
'm' => ':countay',
'week' => ':count hafta',
'a_week' => '{1}bir hafta|]1,Inf[:count hafta',
'a_week' => '{1}bir hafta|[-Inf,Inf]:count hafta',
'w' => ':counth',
'day' => ':count gün',
'a_day' => '{1}bir gün|]1,Inf[:count gün',
'a_day' => '{1}bir gün|[-Inf,Inf]:count gün',
'd' => ':countg',
'hour' => ':count saat',
'a_hour' => '{1}bir saat|]1,Inf[:count saat',
'a_hour' => '{1}bir saat|[-Inf,Inf]:count saat',
'h' => ':countsa',
'minute' => ':count dakika',
'a_minute' => '{1}bir dakika|]1,Inf[:count dakika',
'a_minute' => '{1}bir dakika|[-Inf,Inf]:count dakika',
'min' => ':countdk',
'second' => ':count saniye',
'a_second' => '{1}birkaç saniye|]1,Inf[:count saniye',
'a_second' => '{1}birkaç saniye|[-Inf,Inf]:count saniye',
's' => ':countsn',
'ago' => ':time önce',
'from_now' => ':time sonra',

View file

@ -46,15 +46,22 @@ $weekdays = [
* - hafezdivandari
* - Hossein Jabbari
* - nimamo
* - Usman Zahid
*/
return [
'year' => 'ایک سال|:count سال',
'month' => 'ایک ماہ|:count ماہ',
'week' => ':count ہفتے',
'day' => 'ایک دن|:count دن',
'hour' => 'ایک گھنٹہ|:count گھنٹے',
'minute' => 'ایک منٹ|:count منٹ',
'second' => 'چند سیکنڈ|:count سیکنڈ',
'year' => ':count '.'سال',
'a_year' => 'ایک سال|:count سال',
'month' => ':count '.'ماہ',
'a_month' => 'ایک ماہ|:count ماہ',
'week' => ':count '.'ہفتے',
'day' => ':count '.'دن',
'a_day' => 'ایک دن|:count دن',
'hour' => ':count '.'گھنٹے',
'a_hour' => 'ایک گھنٹہ|:count گھنٹے',
'minute' => ':count '.'منٹ',
'a_minute' => 'ایک منٹ|:count منٹ',
'second' => ':count '.'سیکنڈ',
'a_second' => 'چند سیکنڈ|:count سیکنڈ',
'ago' => ':time قبل',
'from_now' => ':time بعد',
'after' => ':time بعد',

View file

@ -37,7 +37,7 @@ return [
'minute' => ':count:optional-space分钟',
'min' => ':count:optional-space分钟',
'second' => ':count:optional-space秒',
'a_second' => '{1}几秒|]1,Inf[:count:optional-space秒',
'a_second' => '{1}几秒|[-Inf,Inf]:count:optional-space秒',
's' => ':count:optional-space秒',
'ago' => ':time前',
'from_now' => ':time后',

View file

@ -39,7 +39,7 @@ return [
'minute' => ':count:optional-space分鐘',
'min' => ':count:optional-space分鐘',
'second' => ':count:optional-space秒',
'a_second' => '{1}幾秒|]1,Inf[:count:optional-space秒',
'a_second' => '{1}幾秒|[-Inf,Inf]:count:optional-space秒',
's' => ':count:optional-space秒',
'ago' => ':time前',
'from_now' => ':time後',

View file

@ -319,7 +319,7 @@ trait Creator
*
* @return static|null
*/
public static function create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = null): ?self
public static function create($year = 0, $month = 1, $day = 1, $hour = 0, $minute = 0, $second = 0, $timezone = null): ?static
{
$month = self::monthToInt($month);
@ -405,7 +405,7 @@ trait Creator
*
* @return static|null
*/
public static function createSafe($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $timezone = null): ?self
public static function createSafe($year = null, $month = null, $day = null, $hour = null, $minute = null, $second = null, $timezone = null): ?static
{
$month = self::monthToInt($month);
$fields = static::getRangesByUnit();
@ -563,7 +563,7 @@ trait Creator
*
* @return static|null
*/
public static function rawCreateFromFormat(string $format, string $time, $timezone = null): ?self
public static function rawCreateFromFormat(string $format, string $time, $timezone = null): ?static
{
// Work-around for https://bugs.php.net/bug.php?id=80141
$format = preg_replace('/(?<!\\\\)((?:\\\\{2})*)c/', '$1Y-m-d\TH:i:sP', $format);
@ -640,7 +640,7 @@ trait Creator
* @return static|null
*/
#[ReturnTypeWillChange]
public static function createFromFormat($format, $time, $timezone = null): ?self
public static function createFromFormat($format, $time, $timezone = null): ?static
{
$function = static::$createFromFormatFunction;
@ -685,9 +685,9 @@ trait Creator
string $format,
string $time,
$timezone = null,
?string $locale = self::DEFAULT_LOCALE,
?string $locale = CarbonInterface::DEFAULT_LOCALE,
?TranslatorInterface $translator = null
): ?self {
): ?static {
$format = preg_replace_callback('/(?<!\\\\)(\\\\{2})*(LTS|LT|[Ll]{1,4})/', function ($match) use ($locale, $translator) {
[$code] = $match;
@ -825,7 +825,7 @@ trait Creator
*
* @return static|null
*/
public static function createFromLocaleFormat(string $format, string $locale, string $time, $timezone = null): ?self
public static function createFromLocaleFormat(string $format, string $locale, string $time, $timezone = null): ?static
{
$format = preg_replace_callback(
'/(?:\\\\[a-zA-Z]|[bfkqCEJKQRV]){2,}/',
@ -855,7 +855,7 @@ trait Creator
*
* @return static|null
*/
public static function createFromLocaleIsoFormat(string $format, string $locale, string $time, $timezone = null): ?self
public static function createFromLocaleIsoFormat(string $format, string $locale, string $time, $timezone = null): ?static
{
$time = static::translateTimeString($time, $locale, static::DEFAULT_LOCALE, CarbonInterface::TRANSLATE_MONTHS | CarbonInterface::TRANSLATE_DAYS | CarbonInterface::TRANSLATE_MERIDIEM);
@ -874,7 +874,7 @@ trait Creator
*
* @return static|null
*/
public static function make($var, DateTimeZone|string|null $timezone = null): ?self
public static function make($var, DateTimeZone|string|null $timezone = null): ?static
{
if ($var instanceof DateTimeInterface) {
return static::instance($var);

View file

@ -1836,6 +1836,8 @@ trait Date
/**
* Set the timezone or returns the timezone name if no arguments passed.
*
* @return ($value is null ? string : static)
*/
public function tz(DateTimeZone|string|int|null $value = null): static|string
{

View file

@ -327,7 +327,7 @@ trait Localization
}
/**
* Translate a time string from the current locale (`$date->locale()`) to an other.
* Translate a time string from the current locale (`$date->locale()`) to another one.
*
* @param string $timeString time string to translate
* @param string|null $to output locale of the result returned ("en" by default)
@ -659,7 +659,11 @@ trait Localization
{
$word = str_replace([':count', '%count', ':time'], '', $word);
$word = strtr($word, ['' => "'"]);
$word = preg_replace('/({\d+(,(\d+|Inf))?}|[\[\]]\d+(,(\d+|Inf))?[\[\]])/', '', $word);
$word = preg_replace(
'/\{(?:-?\d+(?:\.\d+)?|-?Inf)(?:,(?:-?\d+|-?Inf))?}|[\[\]](?:-?\d+(?:\.\d+)?|-?Inf)(?:,(?:-?\d+|-?Inf))?[\[\]]/',
'',
$word,
);
return trim($word);
}
@ -688,7 +692,7 @@ trait Localization
return $key === 'to'
? self::cleanWordFromTranslationString(end($parts))
: '(?:'.implode('|', array_map([static::class, 'cleanWordFromTranslationString'], $parts)).')';
: '(?:'.implode('|', array_map(static::cleanWordFromTranslationString(...), $parts)).')';
}, $keys);
}

Some files were not shown because too many files have changed in this diff Show more