PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / FileSystem / Drivers / S3 / S3FileList.php

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

265 lines 8.1 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\App\App;
6 use FluentCart\App\Modules\StorageDrivers\S3\S3;
7 use FluentCart\Framework\Support\Arr;
8
9 class S3FileList
10 {
11 private string $date;
12 private string $timeStamp;
13 private string $accessKey;
14 private string $bucket;
15 private string $hashAlgorithm = 'sha256';
16 private string $httpMethod;
17 private string $region;
18 private string $secretKey;
19 private string $signature;
20 private string $requestUrl;
21
22 private int $maxKeys = 10;
23
24 public static function get(string $secret, string $accessKey, string $bucket, string $region, $search = '')
25 {
26 $self = new static($secret, $accessKey, $bucket, $region);
27 return $self->getList($search);
28 }
29
30 public function __construct(string $secret, string $accessKey, string $bucket = '', string $region = '')
31 {
32 $this->secretKey = $secret;
33 $this->accessKey = $accessKey;
34 $this->region = S3::getBucketRegion($bucket);
35 $this->bucket = $bucket;
36
37 $this->httpMethod = 'GET';
38 $this->timeStamp = gmdate('Ymd\THis\Z');
39 $this->date = substr($this->timeStamp, 0, 8);
40 $this->maxKeys = (int)App::request()->get('per_page', 10);
41
42 // �
43 Corrected region-based S3 endpoint
44 // $baseUrl = $this->bucket === ''
45 // ? "https://s3.{$this->region}.amazonaws.com"
46 // : "https://{$this->bucket}.s3.{$this->region}.amazonaws.com";
47
48 $hasDot = strpos($this->bucket, '.') !== false;
49
50 // Base S3 endpoint
51 if ($this->bucket === '') {
52 $baseUrl = "https://s3.{$this->region}.amazonaws.com";
53 } elseif ($hasDot) {
54 // Path-style (required for dotted buckets)
55 $baseUrl = "https://s3.{$this->region}.amazonaws.com/{$this->bucket}";
56 } else {
57 // Virtual-hosted style
58 $baseUrl = "https://{$this->bucket}.s3.{$this->region}.amazonaws.com";
59 }
60
61 $this->requestUrl = "{$baseUrl}/?encoding-type=url&list-type=2&max-keys={$this->maxKeys}";
62 $this->signature = $this->generateSignature();
63
64 }
65
66 public function getList($search = '')
67 {
68 add_filter('http_request_timeout', fn() => 30);
69
70 $url = $this->requestUrl;
71
72 if (!empty($search)) {
73 $url .= '&prefix=' . rawurlencode($search);
74 }
75
76 $this->signature = $this->generateSignature(['prefix' => $search]);
77
78 $response = wp_remote_request($url, [
79 'method' => $this->httpMethod,
80 'headers' => $this->getHeaders(),
81 ]);
82
83 $responseCode = wp_remote_retrieve_response_code($response);
84
85 if ($responseCode === 200) {
86 return $this->parseResponseXml($response);
87 }
88
89 return new \WP_Error(
90 $responseCode,
91 __('Invalid Credential', 'fluent-cart')
92 );
93 }
94
95 public function verifyAuth()
96 {
97 add_filter('http_request_timeout', fn() => 30);
98
99 $response = wp_remote_request($this->requestUrl, [
100 'method' => $this->httpMethod,
101 'headers' => $this->getHeaders(),
102 ]);
103
104 $responseCode = wp_remote_retrieve_response_code($response);
105
106 if ($responseCode === 200) {
107 return [
108 'message' => __('Successfully tested S3!', 'fluent-cart'),
109 'code' => $responseCode,
110 ];
111 }
112
113 $error_message = __('Invalid Credential', 'fluent-cart');
114 $responseBody = wp_remote_retrieve_body($response);
115
116 if (!empty($responseBody)) {
117 $xml = @simplexml_load_string($responseBody);
118 if ($xml) {
119 $code = (string)$xml->Code;
120 $message = (string)$xml->Message;
121 // Uncomment if you want to include AWS error message
122 // $error_message .= " ({$code}: {$message})";
123 }
124 }
125
126 return new \WP_Error($responseCode, $error_message);
127 }
128
129 private function parseResponseXml($response): array
130 {
131 $xml = simplexml_load_string(wp_remote_retrieve_body($response));
132 $array = json_decode(json_encode($xml), true);
133
134 $files = [];
135 $contents = Arr::get($array, 'Contents', []);
136
137 $decodeFileKey = function ($key) {
138 // convert + back to space first
139 $key = str_replace('+', ' ', $key);
140
141 // Then decode %XX safely
142 return rawurldecode($key);
143 };
144
145 if (!Arr::has($contents, 'Key')) {
146 foreach ($contents as $file) {
147 $files[] = [
148 'name' => $decodeFileKey($file['Key']),
149 'size' => $file['Size'],
150 'driver' => 's3',
151 'bucket' => $this->bucket,
152 ];
153 }
154 } else {
155 $files[] = [
156 'name' => $decodeFileKey($contents['Key']),
157 'size' => $contents['Size'],
158 'driver' => 's3',
159 'bucket' => $this->bucket,
160 ];
161 }
162
163 return $files;
164 }
165
166 private function generateSignature(array $params = []): string
167 {
168 return hash_hmac(
169 $this->hashAlgorithm,
170 $this->createStringToSign($params),
171 $this->getSigningKey()
172 );
173 }
174
175 private function createStringToSign(array $params = []): string
176 {
177 $hash = hash($this->hashAlgorithm, $this->createCanonicalUrl($params));
178 return "AWS4-HMAC-SHA256\n{$this->timeStamp}\n{$this->getScope()}\n{$hash}";
179 }
180
181 private function createCanonicalUrl(array $params = []): string
182 {
183 $prefix = Arr::get($params, 'prefix', '');
184 $canonicalQuery = "encoding-type=url&list-type=2&max-keys={$this->maxKeys}";
185
186 if ($prefix !== '') {
187 $canonicalQuery .= '&prefix=' . rawurlencode($prefix);
188 }
189
190 $contentHash = $this->getContentHash();
191 $host = $this->getHost();
192
193 $canonicalUri = '/';
194
195 if ($this->bucket !== '' && strpos($this->bucket, '.') !== false) {
196 // Path-style: include bucket in URI
197 $canonicalUri = '/' . $this->bucket . '/';
198 }
199
200 return "{$this->httpMethod}\n" .
201 "{$canonicalUri}\n" .
202 "{$canonicalQuery}\n" .
203 "host:{$host}\n" .
204 "x-amz-content-sha256:{$contentHash}\n" .
205 "x-amz-date:{$this->timeStamp}\n\n" .
206 "host;x-amz-content-sha256;x-amz-date\n" .
207 "{$contentHash}";
208 }
209
210
211 private function getHostOld(): string
212 {
213 return $this->bucket === ''
214 ? "s3.{$this->region}.amazonaws.com"
215 : "{$this->bucket}.s3.{$this->region}.amazonaws.com";
216 }
217
218 private function getHost(): string
219 {
220 if ($this->bucket === '') {
221 return "s3.{$this->region}.amazonaws.com";
222 }
223
224 // If bucket contains dot, use path-style host
225 if (strpos($this->bucket, '.') !== false) {
226 return "s3.{$this->region}.amazonaws.com";
227 }
228
229 return "{$this->bucket}.s3.{$this->region}.amazonaws.com";
230 }
231
232 private function getContentHash(): string
233 {
234 return hash($this->hashAlgorithm, '');
235 }
236
237 private function getScope(): string
238 {
239 return "{$this->date}/{$this->region}/s3/aws4_request";
240 }
241
242 private function getSigningKey()
243 {
244 $dateKey = hash_hmac($this->hashAlgorithm, $this->date, "AWS4{$this->secretKey}", true);
245 $regionKey = hash_hmac($this->hashAlgorithm, $this->region, $dateKey, true);
246 $serviceKey = hash_hmac($this->hashAlgorithm, 's3', $regionKey, true);
247 return hash_hmac($this->hashAlgorithm, 'aws4_request', $serviceKey, true);
248 }
249
250 private function getHeaders(): array
251 {
252 return [
253 'x-amz-content-sha256' => $this->getContentHash(),
254 'x-amz-date' => $this->timeStamp,
255 'Authorization' => sprintf(
256 'AWS4-HMAC-SHA256 Credential=%s/%s/%s/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=%s',
257 $this->accessKey,
258 $this->date,
259 $this->region,
260 $this->signature
261 ),
262 ];
263 }
264 }
265