PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
← All changes | includes/sdk/s3/Aws/S3/S3Client.php +289 -26 1.2.7 → 1.4.2 View file →
@@ -8,23 +8,27 @@
8 8 use Dudlewebs\WPMCS\s3\Aws\AwsClient;
9 9 use Dudlewebs\WPMCS\s3\Aws\CacheInterface;
10 10 use Dudlewebs\WPMCS\s3\Aws\ClientResolver;
11 11 use Dudlewebs\WPMCS\s3\Aws\Command;
12 +use Dudlewebs\WPMCS\s3\Aws\CommandInterface;
13 +use Dudlewebs\WPMCS\s3\Aws\Configuration\ConfigurationResolver;
12 14 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
13 15 use Dudlewebs\WPMCS\s3\Aws\HandlerList;
16 +use Dudlewebs\WPMCS\s3\Aws\Identity\S3\S3ExpressIdentityProvider;
14 17 use Dudlewebs\WPMCS\s3\Aws\InputValidationMiddleware;
15 18 use Dudlewebs\WPMCS\s3\Aws\Middleware;
19 +use Dudlewebs\WPMCS\s3\Aws\ResultInterface;
16 20 use Dudlewebs\WPMCS\s3\Aws\Retry\QuotaManager;
17 21 use Dudlewebs\WPMCS\s3\Aws\RetryMiddleware;
18 -use Dudlewebs\WPMCS\s3\Aws\ResultInterface;
19 -use Dudlewebs\WPMCS\s3\Aws\CommandInterface;
20 22 use Dudlewebs\WPMCS\s3\Aws\RetryMiddlewareV2;
23 +use Dudlewebs\WPMCS\s3\Aws\S3\Parser\GetBucketLocationResultMutator;
24 +use Dudlewebs\WPMCS\s3\Aws\S3\Parser\S3Parser;
25 +use Dudlewebs\WPMCS\s3\Aws\S3\Parser\ValidateResponseChecksumResultMutator;
26 +use Dudlewebs\WPMCS\s3\Aws\S3\RegionalEndpoint\ConfigurationProvider;
21 27 use Dudlewebs\WPMCS\s3\Aws\S3\UseArnRegion\Configuration;
22 28 use Dudlewebs\WPMCS\s3\Aws\S3\UseArnRegion\ConfigurationInterface;
23 29 use Dudlewebs\WPMCS\s3\Aws\S3\UseArnRegion\ConfigurationProvider as UseArnRegionConfigurationProvider;
24 -use Dudlewebs\WPMCS\s3\Aws\S3\RegionalEndpoint\ConfigurationProvider;
25 30 use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\RequestException;
26 -use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\Promise;
27 31 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\PromiseInterface;
28 32 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
29 33 /**
30 34 * Client used to interact with **Amazon Simple Storage Service (Amazon S3)**.
@@ -36,10 +40,16 @@
36 40 * @method \Aws\Result copyObject(array $args = [])
37 41 * @method \GuzzleHttp\Promise\Promise copyObjectAsync(array $args = [])
38 42 * @method \Aws\Result createBucket(array $args = [])
39 43 * @method \GuzzleHttp\Promise\Promise createBucketAsync(array $args = [])
44 + * @method \Aws\Result createBucketMetadataConfiguration(array $args = [])
45 + * @method \GuzzleHttp\Promise\Promise createBucketMetadataConfigurationAsync(array $args = [])
46 + * @method \Aws\Result createBucketMetadataTableConfiguration(array $args = [])
47 + * @method \GuzzleHttp\Promise\Promise createBucketMetadataTableConfigurationAsync(array $args = [])
40 48 * @method \Aws\Result createMultipartUpload(array $args = [])
41 49 * @method \GuzzleHttp\Promise\Promise createMultipartUploadAsync(array $args = [])
50 + * @method \Aws\Result createSession(array $args = [])
51 + * @method \GuzzleHttp\Promise\Promise createSessionAsync(array $args = [])
42 52 * @method \Aws\Result deleteBucket(array $args = [])
43 53 * @method \GuzzleHttp\Promise\Promise deleteBucketAsync(array $args = [])
44 54 * @method \Aws\Result deleteBucketAnalyticsConfiguration(array $args = [])
45 55 * @method \GuzzleHttp\Promise\Promise deleteBucketAnalyticsConfigurationAsync(array $args = [])
@@ -52,8 +62,12 @@
52 62 * @method \Aws\Result deleteBucketInventoryConfiguration(array $args = [])
53 63 * @method \GuzzleHttp\Promise\Promise deleteBucketInventoryConfigurationAsync(array $args = [])
54 64 * @method \Aws\Result deleteBucketLifecycle(array $args = [])
55 65 * @method \GuzzleHttp\Promise\Promise deleteBucketLifecycleAsync(array $args = [])
66 + * @method \Aws\Result deleteBucketMetadataConfiguration(array $args = [])
67 + * @method \GuzzleHttp\Promise\Promise deleteBucketMetadataConfigurationAsync(array $args = [])
68 + * @method \Aws\Result deleteBucketMetadataTableConfiguration(array $args = [])
69 + * @method \GuzzleHttp\Promise\Promise deleteBucketMetadataTableConfigurationAsync(array $args = [])
56 70 * @method \Aws\Result deleteBucketMetricsConfiguration(array $args = [])
57 71 * @method \GuzzleHttp\Promise\Promise deleteBucketMetricsConfigurationAsync(array $args = [])
58 72 * @method \Aws\Result deleteBucketOwnershipControls(array $args = [])
59 73 * @method \GuzzleHttp\Promise\Promise deleteBucketOwnershipControlsAsync(array $args = [])
@@ -94,8 +108,12 @@
94 108 * @method \Aws\Result getBucketLocation(array $args = [])
95 109 * @method \GuzzleHttp\Promise\Promise getBucketLocationAsync(array $args = [])
96 110 * @method \Aws\Result getBucketLogging(array $args = [])
97 111 * @method \GuzzleHttp\Promise\Promise getBucketLoggingAsync(array $args = [])
112 + * @method \Aws\Result getBucketMetadataConfiguration(array $args = [])
113 + * @method \GuzzleHttp\Promise\Promise getBucketMetadataConfigurationAsync(array $args = [])
114 + * @method \Aws\Result getBucketMetadataTableConfiguration(array $args = [])
115 + * @method \GuzzleHttp\Promise\Promise getBucketMetadataTableConfigurationAsync(array $args = [])
98 116 * @method \Aws\Result getBucketMetricsConfiguration(array $args = [])
99 117 * @method \GuzzleHttp\Promise\Promise getBucketMetricsConfigurationAsync(array $args = [])
100 118 * @method \Aws\Result getBucketNotification(array $args = [])
101 119 * @method \GuzzleHttp\Promise\Promise getBucketNotificationAsync(array $args = [])
@@ -148,8 +166,10 @@
148 166 * @method \Aws\Result listBucketMetricsConfigurations(array $args = [])
149 167 * @method \GuzzleHttp\Promise\Promise listBucketMetricsConfigurationsAsync(array $args = [])
150 168 * @method \Aws\Result listBuckets(array $args = [])
151 169 * @method \GuzzleHttp\Promise\Promise listBucketsAsync(array $args = [])
170 + * @method \Aws\Result listDirectoryBuckets(array $args = [])
171 + * @method \GuzzleHttp\Promise\Promise listDirectoryBucketsAsync(array $args = [])
152 172 * @method \Aws\Result listMultipartUploads(array $args = [])
153 173 * @method \GuzzleHttp\Promise\Promise listMultipartUploadsAsync(array $args = [])
154 174 * @method \Aws\Result listObjectVersions(array $args = [])
155 175 * @method \GuzzleHttp\Promise\Promise listObjectVersionsAsync(array $args = [])
@@ -212,12 +232,18 @@
212 232 * @method \Aws\Result putObjectTagging(array $args = [])
213 233 * @method \GuzzleHttp\Promise\Promise putObjectTaggingAsync(array $args = [])
214 234 * @method \Aws\Result putPublicAccessBlock(array $args = [])
215 235 * @method \GuzzleHttp\Promise\Promise putPublicAccessBlockAsync(array $args = [])
236 + * @method \Aws\Result renameObject(array $args = [])
237 + * @method \GuzzleHttp\Promise\Promise renameObjectAsync(array $args = [])
216 238 * @method \Aws\Result restoreObject(array $args = [])
217 239 * @method \GuzzleHttp\Promise\Promise restoreObjectAsync(array $args = [])
218 240 * @method \Aws\Result selectObjectContent(array $args = [])
219 241 * @method \GuzzleHttp\Promise\Promise selectObjectContentAsync(array $args = [])
242 + * @method \Aws\Result updateBucketMetadataInventoryTableConfiguration(array $args = [])
243 + * @method \GuzzleHttp\Promise\Promise updateBucketMetadataInventoryTableConfigurationAsync(array $args = [])
244 + * @method \Aws\Result updateBucketMetadataJournalTableConfiguration(array $args = [])
245 + * @method \GuzzleHttp\Promise\Promise updateBucketMetadataJournalTableConfigurationAsync(array $args = [])
220 246 * @method \Aws\Result uploadPart(array $args = [])
221 247 * @method \GuzzleHttp\Promise\Promise uploadPartAsync(array $args = [])
222 248 * @method \Aws\Result uploadPartCopy(array $args = [])
223 249 * @method \GuzzleHttp\Promise\Promise uploadPartCopyAsync(array $args = [])
@@ -225,17 +251,20 @@
225 251 * @method \GuzzleHttp\Promise\Promise writeGetObjectResponseAsync(array $args = [])
226 252 */
227 253 class S3Client extends AwsClient implements S3ClientInterface
228 254 {
255 + private const DIRECTORY_BUCKET_REGEX = '/^[a-zA-Z0-9_-]+--[a-z0-9]+-az\\d+--x-s3' . '(?!.*(?:-s3alias|--ol-s3|\\.mrap))$/';
229 256 use S3ClientTrait;
230 257 /** @var array */
231 258 private static $mandatoryAttributes = ['Bucket', 'Key'];
259 + /** @var array */
260 + private static $checksumOptionEnum = ['when_supported' => \true, 'when_required' => \true];
232 261 public static function getArguments()
233 262 {
234 263 $args = parent::getArguments();
235 264 $args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
236 265 $args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider'];
237 - return $args + ['bucket_endpoint' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to send requests to a hardcoded ' . 'bucket endpoint rather than create an endpoint as a ' . 'result of injecting the bucket into the URL. This ' . 'option is useful for interacting with CNAME endpoints.'], 'use_arn_region' => ['type' => 'config', 'valid' => ['bool', Configuration::class, CacheInterface::class, 'callable'], 'doc' => 'Set to true to allow passed in ARNs to override' . ' client region. Accepts...', 'fn' => [__CLASS__, '_apply_use_arn_region'], 'default' => [UseArnRegionConfigurationProvider::class, 'defaultProvider']], 'use_accelerate_endpoint' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to send requests to an S3 Accelerate' . ' endpoint by default. Can be enabled or disabled on' . ' individual operations by setting' . ' \'@use_accelerate_endpoint\' to true or false. Note:' . ' you must enable S3 Accelerate on a bucket before it can' . ' be accessed via an Accelerate endpoint.', 'default' => \false], 'use_path_style_endpoint' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to send requests to an S3 path style' . ' endpoint by default.' . ' Can be enabled or disabled on individual operations by setting' . ' \'@use_path_style_endpoint\' to true or false.', 'default' => \false], 'disable_multiregion_access_points' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to disable the usage of' . ' multi region access points. These are enabled by default.' . ' Can be enabled or disabled on individual operations by setting' . ' \'@disable_multiregion_access_points\' to true or false.', 'default' => \false]];
266 + return ['request_checksum_calculation' => ['type' => 'config', 'valid' => ['string'], 'doc' => 'Valid values are `when_supported` and `when_required`. Default is `when_supported`.' . ' `when_supported` results in checksum calculation when an operation has modeled checksum support.' . ' `when_required` results in checksum calculation when an operation has modeled checksum support and' . ' request checksums are modeled as required.', 'fn' => [__CLASS__, '_apply_request_checksum_calculation'], 'default' => [__CLASS__, '_default_request_checksum_calculation']], 'response_checksum_validation' => ['type' => 'config', 'valid' => ['string'], 'doc' => 'Valid values are `when_supported` and `when_required`. Default is `when_supported`.' . ' `when_supported` results in checksum validation when an operation has modeled checksum support.' . ' `when_required` results in checksum validation when an operation has modeled checksum support and' . ' `CheckSumMode` is set to `enabled`.', 'fn' => [__CLASS__, '_apply_response_checksum_validation'], 'default' => [__CLASS__, '_default_response_checksum_validation']]] + $args + ['bucket_endpoint' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to send requests to a hardcoded ' . 'bucket endpoint rather than create an endpoint as a ' . 'result of injecting the bucket into the URL. This ' . 'option is useful for interacting with CNAME endpoints.'], 'use_arn_region' => ['type' => 'config', 'valid' => ['bool', Configuration::class, CacheInterface::class, 'callable'], 'doc' => 'Set to true to allow passed in ARNs to override' . ' client region. Accepts...', 'fn' => [__CLASS__, '_apply_use_arn_region'], 'default' => [UseArnRegionConfigurationProvider::class, 'defaultProvider']], 'use_accelerate_endpoint' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to send requests to an S3 Accelerate' . ' endpoint by default. Can be enabled or disabled on' . ' individual operations by setting' . ' \'@use_accelerate_endpoint\' to true or false. Note:' . ' you must enable S3 Accelerate on a bucket before it can' . ' be accessed via an Accelerate endpoint.', 'default' => \false], 'use_path_style_endpoint' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to send requests to an S3 path style' . ' endpoint by default.' . ' Can be enabled or disabled on individual operations by setting' . ' \'@use_path_style_endpoint\' to true or false.', 'default' => \false], 'disable_multiregion_access_points' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to disable the usage of' . ' multi region access points. These are enabled by default.' . ' Can be enabled or disabled on individual operations by setting' . ' \'@disable_multiregion_access_points\' to true or false.', 'default' => \false], 'disable_express_session_auth' => ['type' => 'config', 'valid' => ['bool'], 'doc' => 'Set to true to disable the usage of' . ' s3 express session authentication. This is enabled by default.', 'default' => [__CLASS__, '_default_disable_express_session_auth']], 's3_express_identity_provider' => ['type' => 'config', 'valid' => ['bool', 'callable'], 'doc' => 'Specifies the provider used to generate identities to sign s3 express requests. ' . 'Set to `false` to disable s3 express auth, or a callable provider used to create s3 express ' . 'identities or return null.', 'default' => [__CLASS__, '_default_s3_express_identity_provider']]];
238 267 }
239 268 /**
240 269 * {@inheritdoc}
241 270 *
@@ -245,9 +274,11 @@
245 274 *
246 275 * - bucket_endpoint: (bool) Set to true to send requests to a
247 276 * hardcoded bucket endpoint rather than create an endpoint as a result
248 277 * of injecting the bucket into the URL. This option is useful for
249 - * interacting with CNAME endpoints.
278 + * interacting with CNAME endpoints. Note: if you are using version 2.243.0
279 + * and above and do not expect the bucket name to appear in the host, you will
280 + * also need to set `use_path_style_endpoint` to `true`.
250 281 * - calculate_md5: (bool) Set to false to disable calculating an MD5
251 282 * for all Amazon S3 signed uploads.
252 283 * - s3_us_east_1_regional_endpoint:
253 284 * (Aws\S3\RegionalEndpoint\ConfigurationInterface|Aws\CacheInterface\|callable|string|array)
@@ -293,21 +324,26 @@
293 324 {
294 325 if (!isset($args['s3_us_east_1_regional_endpoint']) || $args['s3_us_east_1_regional_endpoint'] instanceof CacheInterface) {
295 326 $args['s3_us_east_1_regional_endpoint'] = ConfigurationProvider::defaultProvider($args);
296 327 }
328 + $this->addBuiltIns($args);
297 329 parent::__construct($args);
298 330 $stack = $this->getHandlerList();
331 + $config = $this->getConfig();
299 332 $stack->appendInit(SSECMiddleware::wrap($this->getEndpoint()->getScheme()), 's3.ssec');
300 - $stack->appendBuild(ApplyChecksumMiddleware::wrap($this->getApi()), 's3.checksum');
333 + $stack->appendBuild(ApplyChecksumMiddleware::wrap($this->getApi(), $this->getConfig()), 's3.checksum');
301 334 $stack->appendBuild(Middleware::contentType(['PutObject', 'UploadPart']), 's3.content_type');
302 - // Use the bucket style middleware when using a "bucket_endpoint" (for cnames)
303 335 if ($this->getConfig('bucket_endpoint')) {
304 - $stack->appendBuild(BucketEndpointMiddleware::wrap(), 's3.bucket_endpoint');
305 - } else {
336 + $stack->appendBuild(BucketEndpointMiddleware::wrap($this->isUseEndpointV2(), $args['endpoint'] ?? null), 's3.bucket_endpoint');
337 + } elseif (!$this->isUseEndpointV2()) {
306 338 $stack->appendBuild(S3EndpointMiddleware::wrap($this->getRegion(), $this->getConfig('endpoint_provider'), ['accelerate' => $this->getConfig('use_accelerate_endpoint'), 'path_style' => $this->getConfig('use_path_style_endpoint'), 'use_fips_endpoint' => $this->getConfig('use_fips_endpoint'), 'dual_stack' => $this->getConfig('use_dual_stack_endpoint')->isUseDualStackEndpoint()]), 's3.endpoint_middleware');
307 339 }
308 - $stack->appendBuild(BucketEndpointArnMiddleware::wrap($this->getApi(), $this->getRegion(), ['use_arn_region' => $this->getConfig('use_arn_region'), 'accelerate' => $this->getConfig('use_accelerate_endpoint'), 'path_style' => $this->getConfig('use_path_style_endpoint'), 'dual_stack' => $this->getConfig('use_dual_stack_endpoint')->isUseDualStackEndpoint(), 'use_fips_endpoint' => $this->getConfig('use_fips_endpoint'), 'disable_multiregion_access_points' => $this->getConfig('disable_multiregion_access_points'), 'endpoint' => isset($args['endpoint']) ? $args['endpoint'] : null]), 's3.bucket_endpoint_arn');
340 + $stack->appendBuild(BucketEndpointArnMiddleware::wrap($this->getApi(), $this->getRegion(), ['use_arn_region' => $this->getConfig('use_arn_region'), 'accelerate' => $this->getConfig('use_accelerate_endpoint'), 'path_style' => $this->getConfig('use_path_style_endpoint'), 'dual_stack' => $this->getConfig('use_dual_stack_endpoint')->isUseDualStackEndpoint(), 'use_fips_endpoint' => $this->getConfig('use_fips_endpoint'), 'disable_multiregion_access_points' => $this->getConfig('disable_multiregion_access_points'), 'endpoint' => $args['endpoint'] ?? null], $this->isUseEndpointV2()), 's3.bucket_endpoint_arn');
341 + if ($this->getConfig('disable_express_session_auth')) {
342 + $stack->prependSign($this->getDisableExpressSessionAuthMiddleware(), 's3.disable_express_session_auth');
343 + }
309 344 $stack->appendValidate(InputValidationMiddleware::wrap($this->getApi(), self::$mandatoryAttributes), 'input_validation_middleware');
345 + $stack->appendSign(ExpiresParsingMiddleware::wrap(), 's3.expires_parsing');
310 346 $stack->appendSign(PutObjectUrlMiddleware::wrap(), 's3.put_object_url');
311 347 $stack->appendSign(PermanentRedirectMiddleware::wrap(), 's3.permanent_redirect');
312 348 $stack->appendInit(Middleware::sourceFile($this->getApi()), 's3.source_file');
313 349 $stack->appendInit($this->getSaveAsParameter(), 's3.save_as');
@@ -313,8 +349,12 @@
313 349 $stack->appendInit($this->getSaveAsParameter(), 's3.save_as');
314 350 $stack->appendInit($this->getLocationConstraintMiddleware(), 's3.location');
315 351 $stack->appendInit($this->getEncodingTypeMiddleware(), 's3.auto_encode');
316 352 $stack->appendInit($this->getHeadObjectMiddleware(), 's3.head_object');
353 + $this->processModel($this->isUseEndpointV2());
354 + if ($this->isUseEndpointV2()) {
355 + $stack->after('builder', 's3.check_empty_path_with_query', $this->getEmptyPathWithQuery());
356 + }
317 357 }
318 358 /**
319 359 * Determine if a string is a valid name for a DNS compatible Amazon S3
320 360 * bucket.
@@ -351,17 +391,74 @@
351 391 // The Configuration class itself will validate other inputs
352 392 $args['use_arn_region'] = new Configuration($value);
353 393 }
354 394 }
395 + public static function _default_request_checksum_calculation(array $args) : string
396 + {
397 + return ConfigurationResolver::resolve('request_checksum_calculation', ApplyChecksumMiddleware::DEFAULT_CALCULATION_MODE, 'string', $args);
398 + }
399 + public static function _apply_request_checksum_calculation(string $value, array &$args) : void
400 + {
401 + $value = \strtolower($value);
402 + if (\array_key_exists($value, self::$checksumOptionEnum)) {
403 + $args['request_checksum_calculation'] = $value;
404 + } else {
405 + $validValues = \implode(' | ', \array_keys(self::$checksumOptionEnum));
406 + throw new \InvalidArgumentException('invalid value provided for `request_checksum_calculation`.' . ' valid values are: ' . $validValues . '.');
407 + }
408 + }
409 + public static function _default_response_checksum_validation(array $args) : string
410 + {
411 + return ConfigurationResolver::resolve('response_checksum_validation', ValidateResponseChecksumResultMutator::DEFAULT_VALIDATION_MODE, 'string', $args);
412 + }
413 + public static function _apply_response_checksum_validation($value, array &$args) : void
414 + {
415 + $value = \strtolower($value);
416 + if (\array_key_exists($value, self::$checksumOptionEnum)) {
417 + $args['response_checksum_validation'] = $value;
418 + } else {
419 + $validValues = \implode(' | ', \array_keys(self::$checksumOptionEnum));
420 + throw new \InvalidArgumentException('invalid value provided for `response_checksum_validation`.' . ' valid values are: ' . $validValues . '.');
421 + }
422 + }
423 + public static function _default_disable_express_session_auth(array &$args)
424 + {
425 + return ConfigurationResolver::resolve('s3_disable_express_session_auth', \false, 'bool', $args);
426 + }
427 + public static function _default_s3_express_identity_provider(array $args)
428 + {
429 + if ($args['config']['disable_express_session_auth']) {
430 + return \false;
431 + }
432 + return new S3ExpressIdentityProvider($args['region']);
433 + }
355 434 public function createPresignedRequest(CommandInterface $command, $expires, array $options = [])
356 435 {
357 436 $command = clone $command;
358 - $command->getHandlerList()->remove('signer');
437 + $list = $command->getHandlerList();
438 + $list->remove('signer');
439 + //Removes checksum calculation behavior by default
440 + if (empty($command['ChecksumAlgorithm']) && empty($command['AddContentMD5'])) {
441 + $list->remove('s3.checksum');
442 + }
359 443 $request = \Dudlewebs\WPMCS\s3\Aws\serialize($command);
360 - $signing_name = $this->getSigningName($request->getUri()->getHost());
444 + //Applies ContentSHA256 parameter, if provided and not applied
445 + // by middleware
446 + $commandName = $command->getName();
447 + if (!empty($command['ContentSHA256'] && isset(ApplyChecksumMiddleware::$sha256[$commandName]) && !$request->hasHeader('X-Amz-Content-Sha256'))) {
448 + $request = $request->withHeader('X-Amz-Content-Sha256', $command['ContentSHA256']);
449 + }
450 + $signing_name = $command['@context']['signing_service'] ?? $this->getSigningName($request->getUri()->getHost());
451 + $signature_version = $this->getSignatureVersionFromCommand($command);
361 452 /** @var \Aws\Signature\SignatureInterface $signer */
362 - $signer = \call_user_func($this->getSignatureProvider(), $this->getConfig('signature_version'), $signing_name, $this->getConfig('signing_region'));
363 - return $signer->presign($request, $this->getCredentials()->wait(), $expires, $options);
453 + $signer = \call_user_func($this->getSignatureProvider(), $signature_version, $signing_name, $this->getConfig('signing_region'));
454 + if ($signature_version == 'v4-s3express') {
455 + $provider = $this->getConfig('s3_express_identity_provider');
456 + $credentials = $provider($command)->wait();
457 + } else {
458 + $credentials = $this->getCredentials()->wait();
459 + }
460 + return $signer->presign($request, $credentials, $expires, $options);
364 461 }
365 462 /**
366 463 * Returns the URL to an object identified by its bucket and key.
367 464 *
@@ -400,14 +497,18 @@
400 497 {
401 498 $region = $this->getRegion();
402 499 return static function (callable $handler) use($region) {
403 500 return function (Command $command, $request = null) use($handler, $region) {
404 - if ($command->getName() === 'CreateBucket') {
405 - $locationConstraint = isset($command['CreateBucketConfiguration']['LocationConstraint']) ? $command['CreateBucketConfiguration']['LocationConstraint'] : null;
501 + if ($command->getName() === 'CreateBucket' && !self::isDirectoryBucket($command['Bucket'])) {
502 + $locationConstraint = $command['CreateBucketConfiguration']['LocationConstraint'] ?? null;
406 503 if ($locationConstraint === 'us-east-1') {
407 504 unset($command['CreateBucketConfiguration']);
408 505 } elseif ('us-east-1' !== $region && empty($locationConstraint)) {
409 - $command['CreateBucketConfiguration'] = ['LocationConstraint' => $region];
506 + if (isset($command['CreateBucketConfiguration'])) {
507 + $command['CreateBucketConfiguration']['LocationConstraint'] = $region;
508 + } else {
509 + $command['CreateBucketConfiguration'] = ['LocationConstraint' => $region];
510 + }
410 511 }
411 512 }
412 513 return $handler($command, $request);
413 514 };
@@ -438,9 +539,9 @@
438 539 */
439 540 private function getHeadObjectMiddleware()
440 541 {
441 542 return static function (callable $handler) {
442 - return function (CommandInterface $command, RequestInterface $request = null) use($handler) {
543 + return function (CommandInterface $command, ?RequestInterface $request = null) use($handler) {
443 544 if ($command->getName() === 'HeadObject' && !isset($command['@http']['decode_content'])) {
444 545 $command['@http']['decode_content'] = \false;
445 546 }
446 547 return $handler($command, $request);
@@ -486,8 +587,44 @@
486 587 };
487 588 };
488 589 }
489 590 /**
591 + * Provides a middleware that checks for an empty path and a
592 + * non-empty query string.
593 + *
594 + * @return \Closure
595 + */
596 + private function getEmptyPathWithQuery()
597 + {
598 + return static function (callable $handler) {
599 + return function (Command $command, RequestInterface $request) use($handler) {
600 + $uri = $request->getUri();
601 + if (empty($uri->getPath()) && !empty($uri->getQuery())) {
602 + $uri = $uri->withPath('/');
603 + $request = $request->withUri($uri);
604 + }
605 + return $handler($command, $request);
606 + };
607 + };
608 + }
609 + /**
610 + * Provides a middleware that disables express session auth when
611 + * customers opt out of it.
612 + *
613 + * @return \Closure
614 + */
615 + private function getDisableExpressSessionAuthMiddleware()
616 + {
617 + return function (callable $handler) {
618 + return function (CommandInterface $command, ?RequestInterface $request = null) use($handler) {
619 + if (!empty($command['@context']['signature_version']) && $command['@context']['signature_version'] === 'v4-s3express') {
620 + $command['@context']['signature_version'] = 's3v4';
621 + }
622 + return $handler($command, $request);
623 + };
624 + };
625 + }
626 + /**
490 627 * Special handling for when the service name is s3-object-lambda.
491 628 * So, if the host contains s3-object-lambda, then the service name
492 629 * returned is s3-object-lambda, otherwise the default signing service is returned.
493 630 * @param string $host The host to validate if is a s3-object-lambda URL.
@@ -499,8 +636,98 @@
499 636 return 's3-object-lambda';
500 637 }
501 638 return $this->getConfig('signing_name');
502 639 }
640 + /**
641 + * If EndpointProviderV2 is used, removes `Bucket` from request URIs.
642 + * This is now handled by the endpoint ruleset.
643 + *
644 + * Additionally adds a synthetic shape `ExpiresString` and modifies
645 + * `Expires` type to ensure it remains set to `timestamp`.
646 + *
647 + * @param array $args
648 + * @return void
649 + *
650 + * @internal
651 + */
652 + private function processModel(bool $isUseEndpointV2) : void
653 + {
654 + $definition = $this->getApi()->getDefinition();
655 + if ($isUseEndpointV2) {
656 + foreach ($definition['operations'] as &$operation) {
657 + if (isset($operation['http']['requestUri'])) {
658 + $requestUri = $operation['http']['requestUri'];
659 + if ($requestUri === "/{Bucket}") {
660 + $requestUri = \str_replace('/{Bucket}', '/', $requestUri);
661 + } else {
662 + $requestUri = \str_replace('/{Bucket}', '', $requestUri);
663 + // If we're left with just a query string, prepend '/'
664 + if (\str_starts_with($requestUri, '?')) {
665 + $requestUri = '/' . $requestUri;
666 + }
667 + }
668 + $operation['http']['requestUri'] = $requestUri;
669 + }
670 + }
671 + }
672 + foreach ($definition['shapes'] as $key => &$value) {
673 + $suffix = 'Output';
674 + if (\str_ends_with($key, $suffix)) {
675 + if (isset($value['members']['Expires'])) {
676 + $value['members']['Expires']['deprecated'] = \true;
677 + $value['members']['ExpiresString'] = ['shape' => 'ExpiresString', 'location' => 'header', 'locationName' => 'Expires'];
678 + }
679 + }
680 + }
681 + $definition['shapes']['ExpiresString']['type'] = 'string';
682 + $definition['shapes']['Expires']['type'] = 'timestamp';
683 + $this->getApi()->setDefinition($definition);
684 + }
685 + /**
686 + * Adds service-specific client built-in values
687 + *
688 + * @return void
689 + */
690 + private function addBuiltIns($args)
691 + {
692 + if (isset($args['region']) && $args['region'] !== 'us-east-1') {
693 + return \false;
694 + }
695 + if (!isset($args['region']) && ConfigurationResolver::resolve('region', '', 'string') !== 'us-east-1') {
696 + return \false;
697 + }
698 + $key = 'AWS::S3::UseGlobalEndpoint';
699 + $result = $args['s3_us_east_1_regional_endpoint'] instanceof \Closure ? $args['s3_us_east_1_regional_endpoint']()->wait() : $args['s3_us_east_1_regional_endpoint'];
700 + if (\is_string($result)) {
701 + if ($result === 'regional') {
702 + $value = \false;
703 + } else {
704 + if ($result === 'legacy') {
705 + $value = \true;
706 + } else {
707 + return;
708 + }
709 + }
710 + } else {
711 + if ($result->isFallback() || $result->getEndpointsType() === 'legacy') {
712 + $value = \true;
713 + } else {
714 + $value = \false;
715 + }
716 + }
717 + $this->clientBuiltIns[$key] = $value;
718 + }
719 + /**
720 + * Determines whether a bucket is a directory bucket.
721 + * Only considers the availability zone/suffix format
722 + *
723 + * @param string $bucket
724 + * @return bool
725 + */
726 + public static function isDirectoryBucket(string $bucket) : bool
727 + {
728 + return \preg_match(self::DIRECTORY_BUCKET_REGEX, $bucket) === 1;
729 + }
503 730 /** @internal */
504 731 public static function _applyRetryConfig($value, $args, HandlerList $list)
505 732 {
506 733 if ($value) {
@@ -508,9 +735,9 @@
508 735 if ($config->getMode() === 'legacy') {
509 736 $maxRetries = $config->getMaxAttempts() - 1;
510 737 $decider = RetryMiddleware::createDefaultDecider($maxRetries);
511 738 $decider = function ($retries, $command, $request, $result, $error) use($decider, $maxRetries) {
512 - $maxRetries = null !== $command['@retries'] ? $command['@retries'] : $maxRetries;
739 + $maxRetries = $command['@retries'] ?? $maxRetries;
513 740 if ($decider($retries, $command, $request, $result, $error)) {
514 741 return \true;
515 742 }
516 743 if ($error instanceof AwsException && $retries < $maxRetries) {
@@ -532,10 +759,10 @@
532 759 $defaultDecider = RetryMiddlewareV2::createDefaultDecider(new QuotaManager(), $config->getMaxAttempts());
533 760 $list->appendSign(RetryMiddlewareV2::wrap($config, ['collect_stats' => $args['stats']['retries'], 'decider' => function ($attempts, CommandInterface $cmd, $result) use($defaultDecider, $config) {
534 761 $isRetryable = $defaultDecider($attempts, $cmd, $result);
535 762 if (!$isRetryable && $result instanceof AwsException && $attempts < $config->getMaxAttempts()) {
536 - if (!empty($result->getResponse()) && \strpos($result->getResponse()->getBody(), 'Your socket connection to the server') !== \false) {
537 - $isRetryable = \false;
763 + if (!empty($result->getResponse()) && $result->getResponse()->getStatusCode() >= 400) {
764 + return \strpos($result->getResponse()->getBody(), 'Your socket connection to the server') !== \false;
538 765 }
539 766 if ($result->getPrevious() instanceof RequestException && $cmd->getName() !== 'CompleteMultipartUpload') {
540 767 $isRetryable = \true;
541 768 }
@@ -548,9 +775,12 @@
548 775 /** @internal */
549 776 public static function _applyApiProvider($value, array &$args, HandlerList $list)
550 777 {
551 778 ClientResolver::_apply_api_provider($value, $args);
552 - $args['parser'] = new GetBucketLocationParser(new ValidateResponseChecksumParser(new AmbiguousSuccessParser(new RetryableMalformedResponseParser($args['parser'], $args['exception_class']), $args['error_parser'], $args['exception_class']), $args['api']));
779 + $s3Parser = new S3Parser($args['parser'], $args['error_parser'], $args['api'], $args['exception_class']);
780 + $s3Parser->addS3ResultMutator('get-bucket-location', new GetBucketLocationResultMutator());
781 + $s3Parser->addS3ResultMutator('validate-response-checksum', new ValidateResponseChecksumResultMutator($args['api'], ['response_checksum_validation' => $args['response_checksum_validation']]));
782 + $args['parser'] = $s3Parser;
553 783 }
554 784 /**
555 785 * @internal
556 786 * @codeCoverageIgnore
@@ -577,11 +807,14 @@
577 807 $docs['shapes']['ContentSHA256']['base'] = 'A SHA256 hash of the body content of the request.';
578 808 $api['shapes']['ContentSHA256'] = ['type' => 'string'];
579 809 $api['shapes']['PutObjectRequest']['members']['ContentSHA256'] = ['shape' => 'ContentSHA256'];
580 810 $api['shapes']['UploadPartRequest']['members']['ContentSHA256'] = ['shape' => 'ContentSHA256'];
581 - unset($api['shapes']['PutObjectRequest']['members']['ContentMD5']);
582 - unset($api['shapes']['UploadPartRequest']['members']['ContentMD5']);
583 811 $docs['shapes']['ContentSHA256']['append'] = $opt;
812 + // Add the AddContentMD5 parameter.
813 + $docs['shapes']['AddContentMD5']['base'] = 'Set to true to calculate the ContentMD5 for the upload.';
814 + $api['shapes']['AddContentMD5'] = ['type' => 'boolean'];
815 + $api['shapes']['PutObjectRequest']['members']['AddContentMD5'] = ['shape' => 'AddContentMD5'];
816 + $api['shapes']['UploadPartRequest']['members']['AddContentMD5'] = ['shape' => 'AddContentMD5'];
584 817 // Add the SaveAs parameter.
585 818 $docs['shapes']['SaveAs']['base'] = 'The path to a file on disk to save the object data.';
586 819 $api['shapes']['SaveAs'] = ['type' => 'string'];
587 820 $api['shapes']['GetObjectRequest']['members']['SaveAs'] = ['shape' => 'SaveAs'];
@@ -597,10 +830,32 @@
597 830 $api['shapes']['CompleteMultipartUploadOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL'];
598 831 // Fix references to Location Constraint.
599 832 unset($api['shapes']['CreateBucketRequest']['payload']);
600 833 $api['shapes']['BucketLocationConstraint']['enum'] = ["ap-northeast-1", "ap-southeast-2", "ap-southeast-1", "cn-north-1", "eu-central-1", "eu-west-1", "us-east-1", "us-west-1", "us-west-2", "sa-east-1"];
601 - // Add a note that the ContentMD5 is optional.
834 + // Add a note that the ContentMD5 is automatically computed, except for with PutObject and UploadPart
602 835 $docs['shapes']['ContentMD5']['append'] = '<div class="alert alert-info">The value will be computed on ' . 'your behalf.</div>';
836 + $docs['shapes']['ContentMD5']['excludeAppend'] = ['PutObjectRequest', 'UploadPartRequest'];
837 + //Add a note to ContentMD5 for PutObject and UploadPart that specifies the value is required
838 + // When uploading to a bucket with object lock enabled and that it is not computed automatically
839 + $objectLock = '<div class="alert alert-info">This value is required if uploading to a bucket ' . 'which has Object Lock enabled. It will not be calculated for you automatically. If you wish to have ' . 'the value calculated for you, use the `AddContentMD5` parameter.</div>';
840 + $docs['shapes']['ContentMD5']['appendOnly'] = ['message' => $objectLock, 'shapes' => ['PutObjectRequest', 'UploadPartRequest']];
841 + // Add `ExpiresString` shape to output structures which contain `Expires`
842 + // Deprecate existing `Expires` shapes in output structures
843 + // Add/Update documentation for both `ExpiresString` and `Expires`
844 + // Ensure `Expires` type remains timestamp
845 + foreach ($api['shapes'] as $key => &$value) {
846 + $suffix = 'Output';
847 + if (\substr($key, -\strlen($suffix)) === $suffix) {
848 + if (isset($value['members']['Expires'])) {
849 + $value['members']['Expires']['deprecated'] = \true;
850 + $value['members']['ExpiresString'] = ['shape' => 'ExpiresString', 'location' => 'header', 'locationName' => 'Expires'];
851 + $docs['shapes']['Expires']['refs'][$key . '$Expires'] .= '<p>This output shape has been deprecated. Please refer to <code>ExpiresString</code> instead.</p>.';
852 + }
853 + }
854 + }
855 + $api['shapes']['ExpiresString']['type'] = 'string';
856 + $docs['shapes']['ExpiresString']['base'] = 'The unparsed string value of the <code>Expires</code> output member.';
857 + $api['shapes']['Expires']['type'] = 'timestamp';
603 858 return [new Service($api, ApiProvider::defaultProvider()), new DocModel($docs)];
604 859 }
605 860 /**
606 861 * @internal
@@ -620,6 +875,14 @@
620 875 } else {
621 876 $examples['PutObject'] = [$putObjectExample];
622 877 }
623 878 return $examples;
879 + }
880 + /**
881 + * @param CommandInterface $command
882 + * @return array|mixed|null
883 + */
884 + private function getSignatureVersionFromCommand(CommandInterface $command)
885 + {
886 + return $command['@context']['signature_version'] ?? $this->getConfig('signature_version');
624 887 }
625 888 }