PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
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 1.2.9 All 35 releases
media-cloud-sync / includes / base / services / s3.php

s3.php in Media Cloud Sync 1.4.1, at includes/base/services/s3.php

1,462 lines 60.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 // Libraries
7 use Dudlewebs\WPMCS\s3\Aws\S3\S3Client;
8 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
9 use Dudlewebs\WPMCS\s3\Aws\S3\Exception\S3Exception;
10 use Dudlewebs\WPMCS\s3\Aws\S3\MultipartUploader;
11 use Dudlewebs\WPMCS\s3\Aws\Exception\MultipartUploadException;
12 use Dudlewebs\WPMCS\s3\Aws\S3\ObjectUploader;
13 use Dudlewebs\WPMCS\s3\Aws\Command;
14 use Exception;
15
16 class S3 {
17 private $assets_url;
18 private $version;
19 private $token;
20
21 protected $config;
22 protected $bucketConfig;
23 protected $settings;
24 protected $credentials;
25 protected $bucket_name;
26 protected $cdnConfig;
27
28 public $service = 's3';
29 public $s3Client = false;
30
31 /**
32 * Admin constructor.
33 * @since 1.0.0
34 */
35 public function __construct($credentials = null) {
36 $this->assets_url = WPMCS_ASSETS_URL;
37 $this->version = WPMCS_VERSION;
38 $this->token = WPMCS_TOKEN;
39
40 // Initialize setup
41 $this->init($credentials);
42 }
43
44 /**
45 * Initialise Client
46 *
47 * @param array|null $credentials Optional explicit credentials; falls back to
48 * Utils::get_credentials() when omitted.
49 */
50 public function init($credentials = null) {
51 $this->settings = Utils::get_settings();
52 $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
53 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
54 ? $this->credentials['config']
55 : [];
56 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
57 ? $this->credentials['bucketConfig']
58 : [];
59 $this->bucket_name = isset($this->bucketConfig['bucket_name']) && !empty($this->bucketConfig['bucket_name'])
60 ? $this->bucketConfig['bucket_name']
61 : '';
62 $this->cdnConfig = isset($this->credentials['cdn']) && !empty($this->credentials['cdn'])
63 ? $this->credentials['cdn']
64 : [];
65
66 if (
67 isset($this->config['region']) && !empty($this->config['region']) &&
68 isset($this->config['access_key']) && !empty($this->config['access_key']) &&
69 isset($this->config['secret_key']) && !empty($this->config['secret_key'])
70 ) {
71 $this->s3Client = new S3Client([
72 'version' => '2006-03-01',
73 'region' => $this->config['region'],
74 'use_accelerate_endpoint' => isset($this->bucketConfig['transfer_acceleration'])
75 ? $this->bucketConfig['transfer_acceleration'] : false,
76 'use_aws_shared_config_files' => false,
77 'credentials' => [
78 'key' => $this->config['access_key'],
79 'secret' => $this->config['secret_key'],
80 ],
81 ]);
82 }
83
84 }
85
86
87 /**
88 * Verify Credentials
89 * @since 1.0.0
90 * @return boolean
91 */
92 public function verifyCredentials($config = []) {
93 $region = isset($config['region']) ? $config['region'] : '';
94 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
95 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
96
97 if (!Service::has_missing_fields([$region, $access_key, $secret_key])) {
98 try {
99 $s3Client = new S3Client([
100 'version' => '2006-03-01',
101 'region' => $region,
102 'use_aws_shared_config_files' => false,
103 'credentials' => [
104 'key' => $access_key,
105 'secret' => $secret_key,
106 ],
107 ]);
108
109 $result = [
110 'success' => false,
111 'code' => 200,
112 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
113 ];
114
115 try {
116 $s3Client->listObjectsV2([
117 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
118 ]);
119
120 // If we reach here, the credentials are valid
121 $result = [
122 'success' => true,
123 'code' => 200,
124 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
125 ];
126 } catch (AwsException $e) {
127 $code = $e->getAwsErrorCode();
128
129 $validErrors = [
130 'AccessDenied',
131 'NoSuchBucket',
132 'AllAccessDisabled',
133 'AuthorizationHeaderMalformed',
134 'PermanentRedirect',
135 'InvalidBucketName',
136 ];
137
138 if (in_array($code, $validErrors)) {
139 // If we reach here, the credentials are valid
140 $result = [
141 'success' => true,
142 'code' => 200,
143 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
144 ];
145 }
146 }
147
148 if($result['success'] == false) {
149 return $result;
150 }
151
152 try {
153 $buckets = $s3Client->listBuckets();
154 $newBucketFormat = [];
155 if(isset($buckets['Buckets']) && !empty($buckets['Buckets'])){
156 foreach($buckets['Buckets'] as $bucket) {
157 if(isset($bucket['Name'])) {
158 $newBucketFormat[] = ['Name' => $bucket['Name'], 'CreationDate' => $bucket['CreationDate']];
159 }
160 }
161 }
162 $result['buckets_data']['buckets'] = $newBucketFormat;
163 $result['buckets_data']['message'] = esc_html__('Buckets listed successfully', 'media-cloud-sync');
164 $result['buckets_data']['status'] = true;
165 } catch (S3Exception $e) {
166 $result ['buckets_data']['buckets'] = [];
167 $result ['buckets_data']['message'] = esc_html__('Unable to list buckets, Please check the bucket listing permission', 'media-cloud-sync');
168 $result ['buckets_data']['status'] = false;
169 } catch (Exception $e) {
170 $result ['buckets_data']['buckets'] = [];
171 $result ['buckets_data']['message'] = esc_html__('Unable to list buckets, Please check the bucket listing permission', 'media-cloud-sync');
172 $result ['buckets_data']['status'] = false;
173 }
174 return $result;
175 }
176 catch (S3Exception $ex) {
177 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
178 } catch (Exception $ex) {
179 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
180 }
181 }
182 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
183 }
184
185 /**
186 * Verify Bucket Exist
187 * @since 1.0.0
188 * @return boolean
189 */
190 public function verifyBucketExist( $config = [], $bucketConfig = [] ) {
191 $region = isset($config['region']) ? $config['region'] : '';
192 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
193 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
194 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
195 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
196
197 if (!Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
198 try {
199 $s3Client = new S3Client([
200 'version' => '2006-03-01',
201 'region' => $region,
202 'use_accelerate_endpoint' => $transfer_acceleration,
203 'use_aws_shared_config_files' => false,
204 'credentials' => [
205 'key' => $access_key,
206 'secret' => $secret_key,
207 ],
208 ]);
209
210 //get S3 object
211 $bucket_found = false;
212 try {
213 $s3Client->getObject([
214 'Bucket' => $bucket_name,
215 'Key' => $this->token . '_dummy-object-for-bucket-exist-check'
216 ]);
217 $bucket_found = true;
218 } catch (AwsException $e) {
219 $code = $e->getAwsErrorCode();
220 if ($code === 'NoSuchKey') {
221 $bucket_found = true;
222 }
223 }
224
225 if($bucket_found) {
226 return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
227 } else {
228 return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
229 }
230 }
231 catch (S3Exception $ex) {
232 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
233 } catch (Exception $ex) {
234 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
235 }
236 }
237 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
238 }
239
240 /**
241 * Create Bucket
242 * @since 1.0.0
243 * @return boolean
244 */
245 public function createBucket( $config = [], $bucketConfig = [] ) {
246 $region = isset($config['region']) ? $config['region'] : '';
247 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
248 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
249 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
250 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
251
252 if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
253 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
254 }
255
256 try {
257 $s3ClientConfig = [
258 'version' => '2006-03-01',
259 'region' => $region,
260 'use_aws_shared_config_files' => false,
261 'credentials' => [
262 'key' => $access_key,
263 'secret' => $secret_key,
264 ],
265 ];
266
267 $s3Client = new S3Client($s3ClientConfig);
268
269 // Create Bucket
270 if ($region === 'us-east-1') {
271 $s3Client->createBucket([
272 'Bucket' => $bucket_name,
273 ]);
274 } else {
275 $s3Client->createBucket([
276 'Bucket' => $bucket_name,
277 'CreateBucketConfiguration' => [
278 'LocationConstraint' => $region,
279 ],
280 ]);
281 }
282
283 try {
284 $this->blockPublicAccess($bucket_name, false, $s3Client);
285 $this->putBucketPolicy($bucket_name, $s3Client);
286 $this->changeBucketOwnership($bucket_name, $s3Client);
287 try {
288
289 $this->changeTransferAccilaration($bucket_name, $s3Client, $transfer_acceleration);
290
291 return [
292 'message' => esc_html__('Bucket created successfully. Choose bucket from list to select the bucket.', 'media-cloud-sync'),
293 'data' => [
294 'Name' => $bucket_name,
295 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
296 ],
297 'code' => 200,
298 'success' => true,
299 ];
300 } catch (AwsException $ex) {
301 return ['message' => esc_html__('Bucket created and made public. But the following error happened while setting the transfer accilaration,', 'media-cloud-sync') . ' ' . $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
302 }
303 } catch (AwsException $ex) {
304 return ['message' => esc_html__('Bucket created. But the following error happened while setting the public access,', 'media-cloud-sync') . ' ' . $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
305 }
306 } catch (AwsException $ex) {
307 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
308 } catch (S3Exception $ex) {
309 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
310 } catch (Exception $ex) {
311 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
312 }
313 }
314
315 /**
316 * Check Bucket Write Permission
317 * @since 1.0.0
318 */
319 public function verifyObjectWritePermission( $config = [], $bucketConfig = [] ) {
320 $region = isset($config['region']) ? $config['region'] : '';
321 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
322 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
323 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
324 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
325
326 if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
327 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
328 }
329
330 try {
331 $s3ClientConfig = [
332 'version' => '2006-03-01',
333 'region' => $region,
334 'use_accelerate_endpoint' => $transfer_acceleration,
335 'use_aws_shared_config_files' => false,
336 'credentials' => [
337 'key' => $access_key,
338 'secret' => $secret_key,
339 ],
340 ];
341
342 $s3Client = new S3Client($s3ClientConfig);
343
344 $object_key = Utils::get_permission_check_object_key();
345
346
347 // Create a dummy object to check write permission
348 $s3Client->putObject([
349 'Bucket' => $bucket_name,
350 'Key' => $object_key,
351 'Body' => 'This is a test object to check write permission.',
352 ]);
353 // Check if the object was created successfully
354 if ($this->exists($object_key, $bucket_name, $s3Client)) {
355 return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
356 } else {
357 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
358 }
359
360 } catch (AwsException $ex) {
361 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
362 } catch (S3Exception $ex) {
363 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
364 } catch (Exception $ex) {
365 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
366 }
367 }
368
369
370
371 /**
372 * Check Bucket Delete Permission
373 * @since 1.0.0
374 */
375 public function verifyObjectDeletePermission( $config = [], $bucketConfig = [] ) {
376 $region = isset($config['region']) ? $config['region'] : '';
377 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
378 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
379 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
380 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
381
382 if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
383 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
384 }
385
386 try {
387 $s3ClientConfig = [
388 'version' => '2006-03-01',
389 'region' => $region,
390 'use_accelerate_endpoint' => $transfer_acceleration,
391 'use_aws_shared_config_files' => false,
392 'credentials' => [
393 'key' => $access_key,
394 'secret' => $secret_key,
395 ],
396 ];
397
398 $s3Client = new S3Client($s3ClientConfig);
399
400 $object_key = Utils::get_permission_check_object_key();
401
402 // Create a dummy object to check dlete permission
403 $s3Client->deleteObject([
404 'Bucket' => $bucket_name,
405 'Key' => $object_key,
406 ]);
407
408 // Check if the object was created successfully
409 if (!$this->exists($object_key, $bucket_name, $s3Client)) {
410 return ['message' => esc_html__('Bucket delete permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
411 } else {
412 return ['message' => esc_html__('Bucket delete permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
413 }
414
415 } catch (AwsException $ex) {
416 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
417 } catch (S3Exception $ex) {
418 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
419 } catch (Exception $ex) {
420 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
421 }
422 }
423
424
425 /**
426 * Check Bucket Read Permission
427 * @since 1.2.4
428 */
429 public function verifyObjectReadPermission() {
430 $result = [
431 'status' => false,
432 'message' => '',
433 'lastChecked' => time(),
434 ];
435
436 if (Service::has_missing_fields([$this->s3Client, $this->bucket_name])) {
437 $result['message'] = esc_html__('Invalid Request', 'media-cloud-sync');
438 return ['message' => esc_html__('Invalid Request', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
439 }
440
441 try {
442 $object_key = Utils::get_permission_check_object_key();
443
444 // Check if the object was created successfully
445 if (!$this->exists($object_key)) {
446 // Create a dummy object to check write permission
447 $this->s3Client->putObject([
448 'Bucket' => $this->bucket_name,
449 'Key' => $object_key,
450 'Body' => 'This is a test object to check permission.',
451 'ContentType' => 'text/plain',
452 'CacheControl' => 'no-cache, no-store, must-revalidate',
453 ]);
454 }
455
456
457 $url = $this->generate_file_url($object_key);
458 $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
459 // Never trust a cached response for this fixed, predictable URL — a stale cached
460 // error would otherwise keep failing the check long after real access is fine.
461 $no_cache_context = stream_context_create(['http' => ['header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"]]);
462 $headers = @get_headers($cdn_url, false, $no_cache_context);
463 $status_code = (is_array($headers) && !empty($headers[0]) && preg_match('/\s(\d{3})\s/', $headers[0], $matches))
464 ? (int) $matches[1]
465 : 0;
466
467 if ($status_code === 200) {
468 $result['status'] = true;
469 $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
470 } else if ($status_code === 403) {
471 $result['status'] = false;
472 if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
473 $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
474 } else {
475 $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
476 }
477 } else if ($status_code === 404) {
478 $result['status'] = false;
479 $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
480 } else if ($status_code === 500) {
481 $result['status'] = false;
482 $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
483 } else {
484 $result['status'] = false;
485 $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
486 }
487
488 $this->deleteSingle($object_key);
489 return [
490 'message' => $result['message'],
491 'code' => 200,
492 'success' => $result['status'],
493 'lastChecked' => $result['lastChecked'],
494 ];
495 } catch (AwsException $ex) {
496 $result['message'] = $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
497 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
498 } catch (S3Exception $ex) {
499 $result['message'] = $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
500 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
501 } catch (Exception $ex) {
502 $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
503 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
504 }
505 }
506
507
508 /**
509 * get Bucket Security Settings
510 */
511 public function getBucketSecuritySettings( $config = [], $bucketConfig = [] ) {
512 $region = isset($config['region']) ? $config['region'] : '';
513 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
514 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
515 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
516 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
517
518 if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
519 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
520 }
521
522 $errors = [];
523 try {
524 $s3ClientConfig = [
525 'version' => '2006-03-01',
526 'region' => $region,
527 'use_accelerate_endpoint' => $transfer_acceleration,
528 'use_aws_shared_config_files' => false,
529 'credentials' => [
530 'key' => $access_key,
531 'secret' => $secret_key,
532 ],
533 ];
534
535 $s3Client = new S3Client($s3ClientConfig);
536
537 // Check public access block configuration
538 $security['block_public_access'] = false;
539 try {
540 $publicAccessBlock = $s3Client->getPublicAccessBlock([
541 'Bucket' => $bucket_name,
542 ]);
543
544 $publicAccessBlockConfig = $publicAccessBlock['PublicAccessBlockConfiguration'];
545 if (
546 $publicAccessBlockConfig['BlockPublicAcls'] &&
547 $publicAccessBlockConfig['IgnorePublicAcls'] &&
548 $publicAccessBlockConfig['BlockPublicPolicy'] &&
549 $publicAccessBlockConfig['RestrictPublicBuckets']
550 ) {
551 $security['block_public_access'] = true;
552 }
553 } catch (S3Exception $ex) {
554 // If the bucket does not have public access block configuration, we assume it is not blocked
555 $errors['block_public_access'] = $ex->getMessage();
556 } catch (Exception $ex) {
557 $errors['block_public_access'] = $ex->getMessage();
558 }
559
560
561 $security['object_ownership_enforced'] = false;
562 try {
563 $ownershipControls = $s3Client->getBucketOwnershipControls([
564 'Bucket' => $bucket_name,
565 ]);
566
567 $ownershipRule = $ownershipControls['OwnershipControls']['Rules'][0]['ObjectOwnership'];
568
569 if ($ownershipRule === 'BucketOwnerEnforced') {
570 $security['object_ownership_enforced'] = true;
571 }
572 } catch (S3Exception $ex) {
573 $errors['object_ownership_enforced'] = $ex->getMessage();
574 } catch (Exception $ex) {
575 $errors['object_ownership_enforced'] = $ex->getMessage();
576 }
577
578 return ['message' => '', 'code' => 200, 'success' => empty($errors), 'security' => $security, 'errors' => $errors];
579 } catch (AwsException $ex) {
580 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
581 } catch (S3Exception $ex) {
582 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
583 } catch (Exception $ex) {
584 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
585 }
586 }
587
588
589
590 /**
591 * Change Bucket Public Access
592 */
593
594 public function changePublicAccess( $config = [], $bucketConfig = [], $value = false ) {
595 $region = isset($config['region']) ? $config['region'] : '';
596 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
597 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
598 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
599 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
600
601 if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
602 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
603 }
604
605 try {
606 $s3ClientConfig = [
607 'version' => '2006-03-01',
608 'region' => $region,
609 'use_accelerate_endpoint' => $transfer_acceleration,
610 'use_aws_shared_config_files' => false,
611 'credentials' => [
612 'key' => $access_key,
613 'secret' => $secret_key,
614 ],
615 ];
616
617 $s3Client = new S3Client($s3ClientConfig);
618
619 try {
620 $result = $this->blockPublicAccess($bucket_name, $value, $s3Client);
621 return ['message' => '', 'code' => 200, 'success' => true, 'result' => $result];
622 } catch (AwsException $ex) {
623 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
624 } catch (S3Exception $ex) {
625 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
626 } catch (Exception $ex) {
627 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
628 }
629 } catch (AwsException $ex) {
630 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
631 } catch (S3Exception $ex) {
632 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
633 } catch (Exception $ex) {
634 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
635 }
636 }
637
638
639
640 /**
641 * Change Bucket Ownership
642 */
643
644 public function changeObjectOwnership( $config = [], $bucketConfig = [], $value = false ) {
645 $region = isset($config['region']) ? $config['region'] : '';
646 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
647 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
648 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
649 $transfer_acceleration = isset($bucketConfig['transfer_acceleration']) ? $bucketConfig['transfer_acceleration'] : false;
650
651 if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
652 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
653 }
654
655 try {
656 $s3ClientConfig = [
657 'version' => '2006-03-01',
658 'region' => $region,
659 'use_accelerate_endpoint' => $transfer_acceleration,
660 'use_aws_shared_config_files' => false,
661 'credentials' => [
662 'key' => $access_key,
663 'secret' => $secret_key,
664 ],
665 ];
666
667 $s3Client = new S3Client($s3ClientConfig);
668
669 $ownership = $value ? 'BucketOwnerEnforced' : 'BucketOwnerPreferred';
670
671 try {
672 $result = $this->changeBucketOwnership( $bucket_name, $s3Client, $ownership );
673 return ['message' => '', 'code' => 200, 'success' => true, 'result' => $result];
674 } catch (AwsException $ex) {
675 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
676 } catch (S3Exception $ex) {
677 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
678 } catch (Exception $ex) {
679 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
680 }
681
682 }
683 catch (AwsException $ex) {
684 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
685 } catch (S3Exception $ex) {
686 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
687 } catch (Exception $ex) {
688 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
689 }
690 }
691
692
693
694
695 /**
696 * Block Public Access
697 * @since 1.0.0
698 */
699 private function blockPublicAccess($bucket, $block = true, $s3Client = false) {
700 if($s3Client == false) {
701 $s3Client = $this->s3Client;
702 }
703
704 if(empty($bucket)) return false;
705
706 try {
707 $s3Client->putPublicAccessBlock([
708 'Bucket' => $bucket,
709 'PublicAccessBlockConfiguration' => [
710 'BlockPublicPolicy' => $block,
711 'BlockPublicAcls' => $block,
712 'IgnorePublicAcls' => $block,
713 'RestrictPublicBuckets' => $block,
714 ]
715 ]);
716
717 return true;
718 } catch (AwsException $ex) {
719 return false;
720 }
721 }
722
723 /**
724 * Add Bucket Policy
725 *
726 * $private_prefix, when non-empty, carves that path out of the public
727 * grant entirely — every action in the list, not just reads, so an
728 * anonymous caller can't read, write, or delete anything under it. Kept
729 * as one statement with NotResource rather than split into "reads
730 * excluded, everything else still public" — that split would still let
731 * anonymous PutObject/DeleteObject reach a "private" file.
732 * @since 1.4.1 $private_prefix param added.
733 */
734 private function putBucketPolicy($bucket, $s3Client = false, $private_prefix = '') {
735 if($s3Client == false) {
736 $s3Client = $this->s3Client;
737 }
738
739 if(empty($bucket)) return false;
740
741 $actions = [
742 "s3:DeleteObjectTagging",
743 "s3:ListBucketMultipartUploads",
744 "s3:DeleteObjectVersion",
745 "s3:ListBucket",
746 "s3:DeleteObjectVersionTagging",
747 "s3:GetBucketAcl",
748 "s3:ListMultipartUploadParts",
749 "s3:PutObject",
750 "s3:GetObjectAcl",
751 "s3:GetObject",
752 "s3:AbortMultipartUpload",
753 "s3:DeleteObject",
754 "s3:GetBucketLocation",
755 "s3:PutObjectAcl",
756 "s3:putBucketOwnershipControls",
757 "s3:putBucketPolicy"
758 ];
759
760 $statement = [
761 "Effect" => "Allow",
762 "Principal" => "*",
763 "Action" => $actions,
764 ];
765
766 if (!empty($private_prefix)) {
767 $statement["NotResource"] = ["arn:aws:s3:::$bucket/$private_prefix/*"];
768 } else {
769 $statement["Resource"] = [
770 "arn:aws:s3:::$bucket/*",
771 "arn:aws:s3:::$bucket"
772 ];
773 }
774
775 $policy = json_encode([
776 "Version" => "2012-10-17",
777 "Statement" => [$statement]
778 ]);
779
780 try {
781 // Add bucket policy
782 $s3Client->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
783
784 return true;
785 } catch (AwsException $ex) {
786 return false;
787 } catch (S3Exception $ex) {
788 return false;
789 } catch (Exception $ex) {
790 return false;
791 }
792 }
793
794 /**
795 * Apply (or, with an empty $private_prefix, un-apply) the private-path
796 * bucket policy carve-out.
797 * @since 1.4.1
798 */
799 public function applyPrivatePathPolicy($private_prefix) {
800 if (!$this->s3Client || empty($this->bucket_name)) {
801 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
802 }
803
804 $ok = $this->putBucketPolicy($this->bucket_name, $this->s3Client, $private_prefix);
805
806 return $ok
807 ? ['success' => true, 'code' => 200, 'message' => esc_html__('Policy applied successfully', 'media-cloud-sync')]
808 : ['success' => false, 'code' => 200, 'message' => esc_html__('Failed to apply bucket policy', 'media-cloud-sync')];
809 }
810
811 /**
812 * Add Bucket Ownership
813 */
814 private function changeBucketOwnership($bucket, $s3Client = false, $ownership = 'BucketOwnerPreferred') {
815 if($s3Client == false) {
816 $s3Client = $this->s3Client;
817 }
818
819 if(empty($bucket)) return false;
820
821 try {
822 // Change object ownership ACL enabled
823 $s3Client->putBucketOwnershipControls([
824 'Bucket' => $bucket,
825 'OwnershipControls' => [
826 'Rules' => [['ObjectOwnership' => $ownership]],
827 ],
828 ]);
829
830 return true;
831 } catch (AwsException $ex) {
832 return false;
833 } catch (S3Exception $ex) {
834 return false;
835 } catch (Exception $ex) {
836 return false;
837 }
838 }
839
840 /**
841 * Change transfer accilaration
842 */
843 private function changeTransferAccilaration($bucket, $s3Client = false, $enable=false, $force = false) {
844 if($s3Client == false) {
845 $s3Client = $this->s3Client;
846 }
847
848 if(empty($bucket)) return false;
849 if(!$force && !$enable) return true;
850
851 // Check if the bucket already has transfer acceleration enabled
852 try {
853 $s3Client->putBucketAccelerateConfiguration([
854 'Bucket' => $bucket,
855 'AccelerateConfiguration' => [
856 'Status' => $enable ? 'Enabled' : 'Suspended'
857 ]
858 ]);
859 return true;
860 } catch (AwsException $ex) {
861 return false;
862 } catch (S3Exception $ex) {
863 return false;
864 } catch (Exception $ex) {
865 return false;
866 }
867 }
868
869 /**
870 * isConfigured Function To Identify the congfigurations are correct
871 * @since 1.0.0
872 */
873 public function isConfigured(){
874 if ($this->s3Client) {
875 try {
876 $this->s3Client->listObjectsV2([
877 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
878 ]);
879
880 // If we reach here, the credentials are valid
881 return true;
882 } catch (AwsException $ex) {
883 $code = $ex->getAwsErrorCode();
884
885 $validErrors = [
886 'AccessDenied',
887 'NoSuchBucket',
888 'AllAccessDisabled',
889 'AuthorizationHeaderMalformed',
890 'PermanentRedirect',
891 'InvalidBucketName'
892 ];
893
894 if (in_array($code, $validErrors)) {
895 // If we reach here, the credentials are valid
896 return true;
897 } else {
898 return false;
899 }
900 } catch (S3Exception $ex) {
901 return false;
902 } catch (Exception $ex) {
903 return false;
904 }
905 }
906 return false;
907 }
908
909 /**
910 * Make Object Private
911 * @since 1.0.0
912 *
913 */
914 public function toPrivate($key) {
915 if(!$key) return false;
916 if(!$this->s3Client) return false;
917 try {
918 $this->s3Client->putObjectAcl([
919 'Bucket' => $this->bucket_name,
920 'Key' => $key,
921 'ACL' => 'private'
922 ]);
923 return true;
924 } catch (AwsException $ex) {
925 return false;
926 }
927 }
928
929
930
931 /**
932 * Make Object Public
933 * @since 1.0.0
934 *
935 */
936 public function toPublic($key) {
937 if(!$key) return false;
938 if(!$this->s3Client) return false;
939 try {
940 $this->s3Client->putObjectAcl([
941 'Bucket' => $this->bucket_name,
942 'Key' => $key,
943 'ACL' => 'public-read'
944 ]);
945 return true;
946 } catch (AwsException $ex) {
947 return false;
948 }
949 }
950
951
952
953 /**
954 * Check the object exist
955 * @since 1.1.8
956 */
957 public function exists($key, $bucket_name = '', $client = null) {
958 if(!$key) return false;
959 try {
960 $bucket_name = $bucket_name ? $bucket_name : $this->bucket_name;
961 $client = $client ?? $this->s3Client;
962 if($client->doesObjectExistV2( $bucket_name, $key)) {
963 return true;
964 }
965 return false;
966 } catch (AwsException $ex) {
967 return false;
968 } catch (S3Exception $ex) {
969 return false;
970 } catch (Exception $ex) {
971 return false;
972 }
973 }
974
975 /**
976 * List Objects — $delimiter = null gives a flat/recursive listing instead of one folder level.
977 * @since 1.3.13
978 */
979 public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
980 if (!$this->s3Client) {
981 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
982 }
983 try {
984 $params = ['Bucket' => $this->bucket_name, 'MaxKeys' => $maxKeys];
985 if (!empty($delimiter)) {
986 $params['Delimiter'] = $delimiter;
987 }
988 if (!empty($prefix)) {
989 $params['Prefix'] = $prefix;
990 }
991 if (!empty($continuationToken)) {
992 $params['ContinuationToken'] = $continuationToken;
993 }
994
995 $result = $this->s3Client->listObjectsV2($params);
996 $folders = [];
997 foreach (($result['CommonPrefixes'] ?? []) as $common) {
998 $folders[] = $common['Prefix'];
999 }
1000 $objects = [];
1001 foreach (($result['Contents'] ?? []) as $object) {
1002 if ($object['Key'] === $prefix) {
1003 continue; // the folder placeholder object itself, not a file
1004 }
1005 $objects[] = [
1006 'key' => $object['Key'],
1007 'size' => (int) $object['Size'],
1008 'last_modified' => $object['LastModified'] ? $object['LastModified']->format(DATE_ATOM) : '',
1009 ];
1010 }
1011
1012 return [
1013 'success' => true,
1014 'code' => 200,
1015 'message' => '',
1016 'folders' => $folders,
1017 'objects' => $objects,
1018 'next_token' => !empty($result['IsTruncated']) ? ($result['NextContinuationToken'] ?? null) : null,
1019 ];
1020 } catch (AwsException $e) {
1021 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
1022 } catch (Exception $e) {
1023 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
1024 }
1025 }
1026
1027 /**
1028 * Upload Single
1029 * @since 1.0.0
1030 * @return boolean
1031 */
1032 public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false) {
1033 if (
1034 isset($absolute_source_path) && !empty($absolute_source_path) &&
1035 isset($relative_source_path) && !empty($relative_source_path)
1036 ) {
1037 $file_name = wp_basename( $relative_source_path );
1038 if ($file_name) {
1039 $upload_path = Utils::generate_object_key($relative_source_path, $prefix, $is_private);
1040 if ($upload_path === false) {
1041 // Only happens for a private reupload with no private-path provider
1042 // available (Pro inactive/unlicensed) — refuse rather than upload
1043 // an already-private file to an unprotected path.
1044 return [
1045 'success' => false,
1046 'code' => 200,
1047 'message' => esc_html__('This file is marked private, but the private-media add-on is not currently active — reupload skipped to avoid exposing it.', 'media-cloud-sync')
1048 ];
1049 }
1050 return $this->execute_upload($absolute_source_path, $upload_path);
1051 }
1052 return [
1053 'success' => false,
1054 'code' => 200,
1055 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
1056 ];
1057 }
1058 return [
1059 'success' => false,
1060 'code' => 200,
1061 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
1062 ];
1063 }
1064
1065 /**
1066 * Upload a local file to an exact destination key (no Utils::generate_object_key() derivation).
1067 * @since 1.4.0
1068 */
1069 public function uploadObjectAtKey($absolute_source_path, $key) {
1070 return $this->execute_upload($absolute_source_path, $key);
1071 }
1072
1073 /**
1074 * Build an unexecuted ObjectUploader (single PUT or multipart, decided internally by the
1075 * SDK, using this plugin's own multipart threshold rather than the SDK's 16MB default).
1076 * ACL is stripped via before_* hooks — this plugin's model is bucket-level, not per-object,
1077 * and an explicit `ACL: null` still serializes to an empty x-amz-acl header otherwise.
1078 * $options is threaded straight into the SDK (e.g. 'state' => UploadState to resume a
1079 * previously-failed multipart attempt).
1080 * @since 1.4.0
1081 */
1082 private function build_object_uploader($absolute_source_path, $key, $options = []) {
1083 $handle = fopen($absolute_source_path, 'rb');
1084 $params = [];
1085 $cache_control = Utils::get_cache_control_header();
1086 if ($cache_control) {
1087 $params['CacheControl'] = $cache_control;
1088 }
1089 $options += [
1090 'mup_threshold' => Schema::getConstant('S3_MULTIPART_MIN_FILE_SIZE'),
1091 'params' => $params,
1092 'before_initiate' => function ($params) { return $this->strip_acl($params); },
1093 'before_upload' => function ($params) { return $this->strip_acl($params); },
1094 'before_complete' => function ($params) { return $this->strip_acl($params); },
1095 ];
1096 return new ObjectUploader($this->s3Client, $this->bucket_name, $key, $handle, null, $options);
1097 }
1098
1099 // Mutate in place, not a clone — the SDK's before_* hooks call this and discard the
1100 // return value, relying on the same Command object being modified.
1101 private function strip_acl($params) {
1102 if ($params instanceof Command && $params->hasParam('ACL')) {
1103 unset($params['ACL']);
1104 } elseif (is_array($params) && isset($params['ACL'])) {
1105 unset($params['ACL']);
1106 }
1107 return $params;
1108 }
1109
1110 /**
1111 * Run an ObjectUploader synchronously and normalize the result shape. Retries up to
1112 * 3 attempts on MultipartUploadException, resuming from the failed attempt's saved
1113 * state rather than restarting the whole upload — same retry contract uploadSingle()
1114 * had before the ObjectUploader swap.
1115 * @since 1.4.0
1116 */
1117 private function execute_upload($absolute_source_path, $key) {
1118 $max_attempts = 3;
1119 $attempt = 0;
1120 $options = [];
1121
1122 while (true) {
1123 $attempt++;
1124 try {
1125 $this->build_object_uploader($absolute_source_path, $key, $options)->upload();
1126 return [
1127 'success' => true,
1128 'code' => 200,
1129 'file_url' => $this->generate_file_url($key),
1130 'key' => $key,
1131 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
1132 ];
1133 } catch (MultipartUploadException $e) {
1134 if ($attempt >= $max_attempts) {
1135 return [
1136 'success' => false,
1137 'code' => 200,
1138 'message' => $e->getMessage()
1139 ];
1140 }
1141 $options = ['state' => $e->getState()];
1142 } catch (AwsException $e) {
1143 return [
1144 'success' => false,
1145 'code' => 200,
1146 'message' => $e->getMessage()
1147 ];
1148 } catch (Exception $e) {
1149 return [
1150 'success' => false,
1151 'code' => 200,
1152 'message' => $e->getMessage()
1153 ];
1154 }
1155 }
1156 }
1157
1158 /**
1159 * Save object to server
1160 * @since 1.0.0
1161 */
1162 public function object_to_server($key, $save_path) {
1163 if(!$this->s3Client) return false;
1164 try {
1165 $getObject = $this->s3Client->GetObject([
1166 'Bucket' => $this->bucket_name,
1167 'Key' => $key,
1168 'SaveAs' => $save_path
1169 ]);
1170 if (file_exists($save_path)) {
1171 return true;
1172 }
1173 } catch (AwsException $e) {
1174 return false;
1175 }
1176 return false;
1177 }
1178
1179 /**
1180 * Object bytes in memory, no local file — for callers (e.g. zip download) that need
1181 * the content itself rather than a copy on the server's filesystem.
1182 * @since 1.3.13
1183 */
1184 public function get_object_content($key) {
1185 if(!$this->s3Client) return false;
1186 try {
1187 $result = $this->s3Client->GetObject([
1188 'Bucket' => $this->bucket_name,
1189 'Key' => $key,
1190 ]);
1191 return (string) $result['Body'];
1192 } catch (AwsException $e) {
1193 return false;
1194 }
1195 }
1196
1197 /**
1198 * Deletes the live object, then best-effort purges every historical version too — a
1199 * plain deleteSingle() on a versioned bucket only adds a delete marker, leaving prior
1200 * versions (and the storage they use) behind at the old key. The live delete happens
1201 * unconditionally first: not every S3-compatible endpoint supports ListObjectVersions
1202 * (confirmed missing on Cloudflare R2, a live 501 "NotImplemented"), and the object must
1203 * still end up gone either way.
1204 * @since 1.3.14
1205 */
1206 public function purge_all_versions($key) {
1207 if (!$this->s3Client) {
1208 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
1209 }
1210
1211 try {
1212 $this->s3Client->deleteObject([
1213 'Bucket' => $this->bucket_name,
1214 'Key' => $key,
1215 ]);
1216 } catch (AwsException $e) {
1217 return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
1218 }
1219
1220 // Best-effort only from here — providers that don't support version listing simply
1221 // skip this part; the live object above is already gone regardless.
1222 try {
1223 $objects = [];
1224 $marker = null;
1225 do {
1226 $args = ['Bucket' => $this->bucket_name, 'Prefix' => $key];
1227 if ($marker) {
1228 $args['KeyMarker'] = $marker['key'];
1229 $args['VersionIdMarker'] = $marker['version'];
1230 }
1231 $result = $this->s3Client->listObjectVersions($args);
1232 foreach (array_merge($result['Versions'] ?? [], $result['DeleteMarkers'] ?? []) as $version) {
1233 if (($version['Key'] ?? null) === $key) {
1234 $objects[] = ['Key' => $key, 'VersionId' => $version['VersionId']];
1235 }
1236 }
1237 $marker = !empty($result['IsTruncated'])
1238 ? ['key' => $result['NextKeyMarker'], 'version' => $result['NextVersionIdMarker']]
1239 : null;
1240 } while ($marker);
1241
1242 foreach (array_chunk($objects, 1000) as $chunk) {
1243 $this->s3Client->deleteObjects([
1244 'Bucket' => $this->bucket_name,
1245 'Delete' => ['Objects' => $chunk],
1246 ]);
1247 }
1248 } catch (AwsException $e) {
1249 // Version history cleanup unsupported/failed — not fatal, live object is gone.
1250 }
1251
1252 return ['success' => true, 'code' => 200, 'message' => esc_html__('Purged Successfully', 'media-cloud-sync')];
1253 }
1254
1255
1256 /**
1257 * Copy object to new path
1258 * @since 1.3.4
1259 */
1260 // Trusts copyObject()'s own success/failure rather than pre/post-verifying with extra
1261 // exists() HEAD requests — each one is a full network round-trip, and with move/copy
1262 // processing keys sequentially, three extra round-trips per file adds up fast on a
1263 // folder with many files. copyObject() itself throws (caught below) if the source is
1264 // missing or the copy otherwise fails, so nothing is lost by not checking first.
1265 public function copy_to_new_path($key, $new_path) {
1266 if (!$this->s3Client) {
1267 return [
1268 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
1269 'code' => 200,
1270 'success' => false
1271 ];
1272 }
1273 try {
1274 $this->s3Client->copyObject([
1275 'Bucket' => $this->bucket_name,
1276 'CopySource' => "{$this->bucket_name}/{$key}",
1277 'Key' => $new_path,
1278 'MetadataDirective' => 'COPY',
1279 ]);
1280 return [
1281 'success' => true,
1282 'code' => 200,
1283 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1284 ];
1285 } catch (AwsException $e) {
1286 return [
1287 'success' => false,
1288 'code' => 200,
1289 'message' => $e->getMessage()
1290 ];
1291 }
1292 }
1293
1294 // Like copy_to_new_path() but into an explicit (possibly different) bucket — needs write
1295 // access there too, so callers should fall back to download+upload on failure.
1296 public function copy_to_bucket($key, $new_key, $dest_bucket) {
1297 if (!$this->s3Client) {
1298 return [
1299 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
1300 'code' => 200,
1301 'success' => false
1302 ];
1303 }
1304 try {
1305 $this->s3Client->copyObject([
1306 'Bucket' => $dest_bucket,
1307 'CopySource' => "{$this->bucket_name}/{$key}",
1308 'Key' => $new_key,
1309 'MetadataDirective' => 'COPY',
1310 ]);
1311 return [
1312 'success' => true,
1313 'code' => 200,
1314 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1315 ];
1316 } catch (AwsException $e) {
1317 return [
1318 'success' => false,
1319 'code' => 200,
1320 'message' => $e->getMessage()
1321 ];
1322 }
1323 }
1324
1325
1326 /**
1327 * Delete Single
1328 * @since 1.0.0
1329 * @return boolean
1330 */
1331 public function deleteSingle($key) {
1332 $result = array();
1333 if (!$this->s3Client) {
1334 return array(
1335 'success' => false,
1336 'code' => 200,
1337 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1338 );
1339 }
1340 if (isset($key) && !empty($key)) {
1341 try {
1342 $this->s3Client->deleteObject([
1343 'Bucket' => $this->bucket_name,
1344 'Key' => $key
1345 ]);
1346
1347 if (!$this->exists($key)) {
1348 $result = array(
1349 'success' => true,
1350 'code' => 200,
1351 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync')
1352 );
1353 } else {
1354 $result = array(
1355 'success' => false,
1356 'code' => 200,
1357 'message' => esc_html__('File not deleted', 'media-cloud-sync')
1358 );
1359 }
1360 } catch (AwsException $e) {
1361 $result = array(
1362 'success' => false,
1363 'code' => 200,
1364 'message' => $e->getMessage()
1365 );
1366 }
1367 } else {
1368 $result = array(
1369 'success' => false,
1370 'code' => 200,
1371 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
1372 );
1373 }
1374 return $result;
1375 }
1376
1377 /**
1378 * get private URL
1379 * @since 1.0.0
1380 * @return boolean
1381 */
1382 public function get_private_url($key) {
1383 $result = array();
1384 if (!$this->s3Client) {
1385 return array(
1386 'success' => false,
1387 'code' => 200,
1388 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1389 );
1390 }
1391 if (isset($key) && !empty($key)) {
1392 try {
1393 $cmd = $this->s3Client->getCommand('GetObject', [
1394 'Bucket' => $this->bucket_name,
1395 'Key' => $key
1396 ]);
1397
1398 $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
1399
1400 $request = $this->s3Client->createPresignedRequest($cmd, sprintf('+%s minutes', $expires));
1401
1402 if ($privateUrl = (string)$request->getUri()) {
1403 $result = array(
1404 'success' => true,
1405 'code' => 200,
1406 'file_url' => $privateUrl,
1407 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync')
1408 );
1409 } else {
1410 $result = array(
1411 'success' => false,
1412 'code' => 200,
1413 'message' => esc_html__('Error getting private URL', 'media-cloud-sync')
1414 );
1415 }
1416 } catch (AwsException $e) {
1417 $result = array(
1418 'success' => false,
1419 'code' => 200,
1420 'message' => $e->getMessage()
1421 );
1422 }
1423 } else {
1424 $result = array(
1425 'success' => false,
1426 'code' => 200,
1427 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
1428 );
1429 }
1430 return $result;
1431 }
1432
1433 /**
1434 * Generate file URL
1435 */
1436 public function generate_file_url($key){
1437 $domain = $this->get_domain();
1438
1439 return apply_filters('wpmcs_generate_s3_file_url',
1440 $domain . '/' . $key,
1441 $domain, $key
1442 );
1443 }
1444
1445 /**
1446 * Is Provider URL
1447 * @since 1.3.6
1448 */
1449 public function is_provider_url($url) {
1450 $domain = $this->get_domain();
1451 return (strpos($url, $domain . '/') !== false);
1452 }
1453
1454 /**
1455 * Get domain URL
1456 */
1457 public function get_domain() {
1458 $region = isset($this->config['region']) ? $this->config['region'] : '';
1459 return "https://{$this->bucket_name}.s3.{$region}.amazonaws.com";
1460 }
1461
1462 }