| @@ -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 | } |