PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / FileSystem / Drivers / S3 / S3BucketCreator.php

S3BucketCreator.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.5, at app/Services/FileSystem/Drivers/S3/S3BucketCreator.php

221 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\FileSystem\Drivers\S3;
4
5 use FluentCart\Framework\Support\Arr;
6
7 class S3BucketCreator
8 {
9 private const DEFAULT_REGION = 'us-east-1';
10
11 private string $accessKey;
12 private string $secretKey;
13 private string $region;
14 private string $bucket;
15 private string $hashAlgorithm = 'sha256';
16 private string $httpMethod = 'PUT';
17 private string $signature;
18 private $timeStamp;
19 private $date;
20 private string $requestUrl;
21 private ?string $sessionToken = null;
22
23 public static function create(string $secret, string $accessKey, string $bucket, string $region, ?string $sessionToken = null)
24 {
25 $validation = S3InputValidator::validateBucketAndRegion($bucket, $region);
26 if (is_wp_error($validation)) {
27 return $validation;
28 }
29
30 return (new static($secret, $accessKey, $bucket, $region, $sessionToken))->createBucket();
31 }
32
33 public function __construct(string $secret, string $accessKey, string $bucket, string $region, ?string $sessionToken = null)
34 {
35 $this->secretKey = $secret;
36 $this->accessKey = $accessKey;
37 $this->bucket = $bucket;
38 $this->region = $region;
39 $this->sessionToken = $sessionToken;
40
41 $this->timeStamp = gmdate('Ymd\THis\Z');
42 $this->date = substr($this->timeStamp, 0, 8);
43
44 $this->requestUrl = $this->getBucketBaseUrl();
45
46 $this->generateSignature();
47 }
48
49 public function createBucket()
50 {
51 add_filter('http_request_timeout', function () {
52 return 30;
53 });
54
55 $body = '';
56 if ($this->region !== 'us-east-1') {
57 $body = '<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' .
58 '<LocationConstraint>' . $this->region . '</LocationConstraint>' .
59 '</CreateBucketConfiguration>';
60 }
61
62 $headers = $this->getHeaders($body);
63
64 $response = wp_remote_request($this->requestUrl, [
65 'method' => $this->httpMethod,
66 'headers' => $headers,
67 'body' => $body
68 ]);
69
70 $responseCode = wp_remote_retrieve_response_code($response);
71
72 if ($responseCode == '200') {
73 return [
74 'message' => __('Bucket created successfully', 'fluent-cart'),
75 'bucket' => $this->bucket,
76 'region' => $this->region
77 ];
78 } else {
79 $errorBody = wp_remote_retrieve_body($response);
80 $xml = simplexml_load_string($errorBody);
81 $message = $xml ? (string)$xml->Message : __('Unknown error occurred', 'fluent-cart');
82 $code = $xml ? (string)$xml->Code : $responseCode;
83
84 // Append raw body if XML parsing failed for better debugging
85 if (!$xml) {
86 $message .= ' (' . substr($errorBody, 0, 200) . ')';
87 }
88
89 return new \WP_Error(
90 'bucket_creation_failed',
91 sprintf(__('Failed to create bucket: %s (%s)', 'fluent-cart'), $message, $code)
92 );
93 }
94 }
95
96 public function getSignature(): string
97 {
98 return $this->signature;
99 }
100
101 public function generateSignature()
102 {
103 // For CreateBucket with XML body, we likely need to sign payload hash if not us-east-1
104 // But let's check basic signing first.
105
106 // Actually, payload signing is required for v4 auth if we are sending body
107 $this->signature = $this->generateSignatureKey();
108 }
109
110 private function createScope(): string
111 {
112 return "{$this->date}/{$this->region}/s3/aws4_request";
113 }
114
115 private function getContentHash($payload = ''): string
116 {
117 return hash($this->hashAlgorithm, $payload);
118 }
119
120 private function createCanonicalUrl($payloadHash): string
121 {
122 $payload = "$this->httpMethod\n" .
123 "/\n\n" .
124 "host:{$this->getHost()}\n" .
125 "x-amz-content-sha256:{$payloadHash}\n" .
126 "x-amz-date:{$this->timeStamp}\n";
127
128 if ($this->sessionToken) {
129 $payload .= "x-amz-security-token:{$this->sessionToken}\n";
130 }
131
132 $payload .= "\n";
133
134 $signedHeaders = "host;x-amz-content-sha256;x-amz-date";
135 if ($this->sessionToken) {
136 $signedHeaders .= ";x-amz-security-token";
137 }
138
139 $payload .= $signedHeaders . "\n" .
140 "{$payloadHash}";
141
142 return $payload;
143 }
144
145 private function getHost(): string
146 {
147 return parse_url($this->getBucketBaseUrl(), PHP_URL_HOST) ?: "{$this->bucket}.s3.amazonaws.com";
148 }
149
150 private function getBucketBaseUrl(): string
151 {
152 $host = $this->region === self::DEFAULT_REGION
153 ? "{$this->bucket}.s3.amazonaws.com"
154 : "{$this->bucket}.s3.{$this->region}.amazonaws.com";
155
156 return 'https://' . $host;
157 }
158
159 private function createStringToSign($payloadHash): string
160 {
161 $hash = hash($this->hashAlgorithm, $this->createCanonicalUrl($payloadHash));
162 return "AWS4-HMAC-SHA256\n{$this->timeStamp}\n{$this->createScope()}\n{$hash}";
163 }
164
165 private function getSigningKey()
166 {
167 $dateKey = hash_hmac($this->hashAlgorithm, $this->date, "AWS4{$this->secretKey}", true);
168 $regionKey = hash_hmac($this->hashAlgorithm, $this->region, $dateKey, true);
169 $serviceKey = hash_hmac($this->hashAlgorithm, 's3', $regionKey, true);
170 return hash_hmac($this->hashAlgorithm, 'aws4_request', $serviceKey, true);
171 }
172
173 // We need to re-generate signature dynamically based on body if needed,
174 // but constructor runs once. So usage of this class instance:
175
176 // Actually, let's just do it inside getHeaders since body is there
177
178 private function generateSignatureKey($body = '')
179 {
180 // wait, this method name in S3BucketList was 'generateSignatureKey' but it returned the Final signature?
181 // Let's re-verify S3BucketList implementation.
182 // Yes, S3BucketList::generateSignatureKey returned hash_hmac(..., stringToSign, signingKey)
183 // Which IS the signature.
184
185 $payloadHash = $this->getContentHash($body);
186 return hash_hmac($this->hashAlgorithm, $this->createStringToSign($payloadHash), $this->getSigningKey());
187 }
188
189
190 public function getHeaders($body = ''): array
191 {
192 $payloadHash = $this->getContentHash($body);
193
194 // Re-calculate signature with body
195 $signature = hash_hmac($this->hashAlgorithm, $this->createStringToSign($payloadHash), $this->getSigningKey());
196
197 $headers = [
198 "x-amz-content-sha256" => $payloadHash,
199 'x-amz-date' => $this->timeStamp,
200 ];
201
202 if ($this->region !== 'us-east-1') {
203 $headers['Content-Type'] = 'application/xml';
204 // AWS S3 CreateBucket doesn't strictly require Content-Type but good practice
205 }
206
207 if ($this->sessionToken) {
208 $headers['x-amz-security-token'] = $this->sessionToken;
209 }
210
211 $signedHeaders = "host;x-amz-content-sha256;x-amz-date";
212 if ($this->sessionToken) {
213 $signedHeaders .= ";x-amz-security-token";
214 }
215
216 $headers['Authorization'] = "AWS4-HMAC-SHA256 Credential={$this->accessKey}/{$this->date}/{$this->region}/s3/aws4_request, SignedHeaders={$signedHeaders}, Signature={$signature}";
217
218 return $headers;
219 }
220 }
221