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
← All changes | includes/base/services/s3.php +1032 -268 1.1.01.4.1 View file →
@@ -8,8 +8,10 @@
8 8 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
9 9 use Dudlewebs\WPMCS\s3\Aws\S3\Exception\S3Exception;
10 10 use Dudlewebs\WPMCS\s3\Aws\S3\MultipartUploader;
11 11 use Dudlewebs\WPMCS\s3\Aws\Exception\MultipartUploadException;
12 +use Dudlewebs\WPMCS\s3\Aws\S3\ObjectUploader;
13 +use Dudlewebs\WPMCS\s3\Aws\Command;
12 14 use Exception;
13 15
14 16 class S3 {
15 17 private $assets_url;
@@ -20,8 +22,9 @@
20 22 protected $bucketConfig;
21 23 protected $settings;
22 24 protected $credentials;
23 25 protected $bucket_name;
26 + protected $cdnConfig;
24 27
25 28 public $service = 's3';
26 29 public $s3Client = false;
27 30
@@ -28,23 +31,26 @@
28 31 /**
29 32 * Admin constructor.
30 33 * @since 1.0.0
31 34 */
32 - public function __construct() {
35 + public function __construct($credentials = null) {
33 36 $this->assets_url = WPMCS_ASSETS_URL;
34 37 $this->version = WPMCS_VERSION;
35 38 $this->token = WPMCS_TOKEN;
36 39
37 40 // Initialize setup
38 - $this->init();
41 + $this->init($credentials);
39 42 }
40 43
41 44 /**
42 45 * Initialise Client
46 + *
47 + * @param array|null $credentials Optional explicit credentials; falls back to
48 + * Utils::get_credentials() when omitted.
43 49 */
44 - public function init() {
50 + public function init($credentials = null) {
45 51 $this->settings = Utils::get_settings();
46 - $this->credentials = Utils::get_credentials();
52 + $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
47 53 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
48 54 ? $this->credentials['config']
49 55 : [];
50 56 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
@@ -52,8 +58,11 @@
52 58 : [];
53 59 $this->bucket_name = isset($this->bucketConfig['bucket_name']) && !empty($this->bucketConfig['bucket_name'])
54 60 ? $this->bucketConfig['bucket_name']
55 61 : '';
62 + $this->cdnConfig = isset($this->credentials['cdn']) && !empty($this->credentials['cdn'])
63 + ? $this->credentials['cdn']
64 + : [];
56 65
57 66 if (
58 67 isset($this->config['region']) && !empty($this->config['region']) &&
59 68 isset($this->config['access_key']) && !empty($this->config['access_key']) &&
@@ -79,14 +88,14 @@
79 88 * Verify Credentials
80 89 * @since 1.0.0
81 90 * @return boolean
82 91 */
83 - public function verifyCredentials($access_key, $secret_key, $region){
84 - if (
85 - isset($region) && !empty($region) &&
86 - isset($access_key) && !empty($access_key) &&
87 - isset($secret_key) && !empty($secret_key)
88 - ) {
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])) {
89 98 try {
90 99 $s3Client = new S3Client([
91 100 'version' => '2006-03-01',
92 101 'region' => $region,
@@ -91,26 +100,79 @@
91 100 'version' => '2006-03-01',
92 101 'region' => $region,
93 102 'use_aws_shared_config_files' => false,
94 103 'credentials' => [
95 - 'key' => $access_key,
96 - 'secret' => $secret_key,
97 - ],
104 + 'key' => $access_key,
105 + 'secret' => $secret_key,
106 + ],
98 107 ]);
99 108
100 - //Listing all S3 Bucket
101 - $buckets = $s3Client->listBuckets();
109 + $result = [
110 + 'success' => false,
111 + 'code' => 200,
112 + 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
113 + ];
102 114
103 - $newBucketFormat = [];
104 - if(isset($buckets['Buckets']) && !empty($buckets['Buckets'])){
105 - foreach($buckets['Buckets'] as $bucket) {
106 - if(isset($bucket['Name'])) {
107 - $newBucketFormat[] = ['Name' => $bucket['Name'], 'CreationDate' => $bucket['CreationDate']];
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 + }
108 160 }
109 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;
110 173 }
111 -
112 - return array( 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'), 'buckets' => $newBucketFormat, 'code' => 200, 'success' => true);
174 + return $result;
113 175 }
114 176 catch (S3Exception $ex) {
115 177 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
116 178 } catch (Exception $ex) {
@@ -120,19 +182,20 @@
120 182 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
121 183 }
122 184
123 185 /**
124 - * Verify Bucket
186 + * Verify Bucket Exist
125 187 * @since 1.0.0
126 188 * @return boolean
127 189 */
128 - public function verifyBucket($access_key, $secret_key, $region, $bucket_name, $transfer_acceleration=false){
129 - if (
130 - isset($region) && !empty($region) &&
131 - isset($access_key) && !empty($access_key) &&
132 - isset($secret_key) && !empty($secret_key) &&
133 - isset($bucket_name) && !empty($bucket_name)
134 - ) {
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])) {
135 198 try {
136 199 $s3Client = new S3Client([
137 200 'version' => '2006-03-01',
138 201 'region' => $region,
@@ -143,76 +206,27 @@
143 206 'secret' => $secret_key,
144 207 ],
145 208 ]);
146 209
147 - //Listing all S3 Bucket
148 - $buckets = $s3Client->listBuckets();
210 + //get S3 object
149 211 $bucket_found = false;
150 - $region_correct = false;
151 - if ($buckets) {
152 - foreach ($buckets['Buckets'] as $bucket) {
153 - if ($bucket['Name']==$bucket_name) {
154 - $bucket_found = true;
155 - }
156 - }
157 - } else {
158 - return array('message' => esc_html__('No matching bucket found', 'media-cloud-sync'), 'code' => 200, 'success' => false);
159 - }
160 - if ($bucket_found) {
161 - $upload_dir = wp_upload_dir();
162 - $file_dir = $upload_dir['basedir'] . '/' . Schema::getConstant('UPLOADS') . '/';
163 -
164 - if (!is_dir($file_dir)) {
165 - do_action( $this->token.'_create_plugin_dir' );
166 - }
167 -
168 - $fileName = $this->token."_verify.txt";
169 - $localFileName = $file_dir.$this->token."-local-verify.txt";
170 -
171 - $verify_file = fopen($file_dir.$fileName, "w");
172 - $txt = "We are verifying input/output operations in s3\n";
173 - fwrite($verify_file, $txt);
174 - fclose($verify_file);
175 -
176 - $upload = $s3Client->putObject([
212 + try {
213 + $s3Client->getObject([
177 214 'Bucket' => $bucket_name,
178 - 'Key' => $fileName,
179 - 'Body' => fopen($file_dir.$fileName, "r"),
180 - 'ACL' => 'public-read', // make file 'public'
215 + 'Key' => $this->token . '_dummy-object-for-bucket-exist-check'
181 216 ]);
182 -
183 - @unlink($file_dir.$fileName);
184 - if ($upload->get('ObjectURL')) {
185 - try {
186 - $getObject = $s3Client->GetObject([
187 - 'Bucket' => $bucket_name,
188 - 'Key' => $fileName,
189 - 'SaveAs' => $localFileName
190 - ]);
191 -
192 - if (file_exists($localFileName)) {
193 - @unlink($localFileName);
194 - $s3Client->deleteObject([
195 - 'Bucket' => $bucket_name,
196 - 'Key' => $fileName,
197 - ]);
217 + $bucket_found = true;
218 + } catch (AwsException $e) {
219 + $code = $e->getAwsErrorCode();
220 + if ($code === 'NoSuchKey') {
221 + $bucket_found = true;
222 + }
223 + }
198 224
199 - if (!$s3Client->doesObjectExist($bucket_name, $fileName)) {
200 - return array('message' => esc_html__('Configuration for AWS/S3 has verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true);
201 - } else {
202 - return array('message' => esc_html__('Bucket has permission issues on deleting the object from bucket, Please check ACL permission as well as policies', 'media-cloud-sync'), 'code' => 200, 'success' => false);
203 - }
204 - } else {
205 - return array('message' => esc_html__('Bucket has permission issues on getting the object from bucket, Please check ACL permission as well as policies', 'media-cloud-sync'), 'code' => 200, 'success' => false);
206 - }
207 - } catch (S3Exception $ex) {
208 - return array('message' => $ex->getAwsErrorMessage(), 'code' => $ex->getAwsErrorCode(), 'success' => false);
209 - }
210 - } else {
211 - return array('message' => esc_html__('Bucket has permission issues on putting object in to bucket, Please check ACL permission as well as policies', 'media-cloud-sync'), 'code' => 200, 'success' => false);
212 - }
225 + if($bucket_found) {
226 + return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
213 227 } else {
214 - return array('message' => esc_html__('Bucket Name is incorrect', 'media-cloud-sync'), 'code' => 200, 'success' => false);
228 + return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
215 229 }
216 230 }
217 231 catch (S3Exception $ex) {
218 232 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
@@ -227,10 +241,16 @@
227 241 * Create Bucket
228 242 * @since 1.0.0
229 243 * @return boolean
230 244 */
231 - public function createBucket($access_key, $secret_key, $region, $bucket_name, $transfer_acceleration = false){
232 - if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
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])) {
233 253 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
234 254 }
235 255
236 256 try {
@@ -246,11 +266,23 @@
246 266
247 267 $s3Client = new S3Client($s3ClientConfig);
248 268
249 269 // Create Bucket
250 - $s3Client->createBucket(['Bucket' => $bucket_name]);
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 +
251 283 try {
252 - $this->enablBucketPublicAccess($bucket_name, $s3Client);
284 + $this->blockPublicAccess($bucket_name, false, $s3Client);
253 285 $this->putBucketPolicy($bucket_name, $s3Client);
254 286 $this->changeBucketOwnership($bucket_name, $s3Client);
255 287 try {
256 288
@@ -278,37 +310,429 @@
278 310 } catch (Exception $ex) {
279 311 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
280 312 }
281 313 }
314 +
282 315 /**
283 - * Make Public Access Block Settings Enable
316 + * Check Bucket Write Permission
284 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
285 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 + }
286 440
287 - private function enablBucketPublicAccess($bucket, $s3Client = false) {
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) {
288 700 if($s3Client == false) {
289 701 $s3Client = $this->s3Client;
290 702 }
291 703
292 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 + ]);
293 716
294 - $s3Client->putPublicAccessBlock([
295 - 'Bucket' => $bucket,
296 - 'PublicAccessBlockConfiguration' => [
297 - 'BlockPublicPolicy' => false,
298 - 'BlockPublicAcls' => false,
299 - 'IgnorePublicAcls' => false,
300 - 'RestrictPublicBuckets' => false,
301 - ]
302 - ]);
303 -
304 - return true;
717 + return true;
718 + } catch (AwsException $ex) {
719 + return false;
720 + }
305 721 }
306 722
307 723 /**
308 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.
309 733 */
310 - private function putBucketPolicy($bucket, $s3Client = false) {
734 + private function putBucketPolicy($bucket, $s3Client = false, $private_prefix = '') {
311 735 if($s3Client == false) {
312 736 $s3Client = $this->s3Client;
313 737 }
314 738
@@ -313,47 +737,79 @@
313 737 }
314 738
315 739 if(empty($bucket)) return false;
316 740
317 - $policy = '{
318 - "Version": "2012-10-17",
319 - "Statement": [
320 - {
321 - "Effect": "Allow",
322 - "Principal": "*",
323 - "Action": [
324 - "s3:DeleteObjectTagging",
325 - "s3:ListBucketMultipartUploads",
326 - "s3:DeleteObjectVersion",
327 - "s3:ListBucket",
328 - "s3:DeleteObjectVersionTagging",
329 - "s3:GetBucketAcl",
330 - "s3:ListMultipartUploadParts",
331 - "s3:PutObject",
332 - "s3:GetObjectAcl",
333 - "s3:GetObject",
334 - "s3:AbortMultipartUpload",
335 - "s3:DeleteObject",
336 - "s3:GetBucketLocation",
337 - "s3:PutObjectAcl",
338 - "s3:putBucketOwnershipControls",
339 - "s3:putBucketPolicy"
340 - ],
341 - "Resource": [
342 - "arn:aws:s3:::' . $bucket . '/*",
343 - "arn:aws:s3:::' . $bucket . '"
344 - ]
345 - }
346 - ]
347 - }';
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 + ];
348 759
349 - // Add bucket policy
350 - $s3Client->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
760 + $statement = [
761 + "Effect" => "Allow",
762 + "Principal" => "*",
763 + "Action" => $actions,
764 + ];
351 765
352 - return true;
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 + }
353 792 }
354 793
355 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 + /**
356 812 * Add Bucket Ownership
357 813 */
358 814 private function changeBucketOwnership($bucket, $s3Client = false, $ownership = 'BucketOwnerPreferred') {
359 815 if($s3Client == false) {
@@ -361,17 +817,25 @@
361 817 }
362 818
363 819 if(empty($bucket)) return false;
364 820
365 - // Change object ownership ACL enabled
366 - $s3Client->putBucketOwnershipControls([
367 - 'Bucket' => $bucket,
368 - 'OwnershipControls' => [
369 - 'Rules' => [['ObjectOwnership' => $ownership]],
370 - ],
371 - ]);
821 + try {
822 + // Change object ownership ACL enabled
823 + $s3Client->putBucketOwnershipControls([
824 + 'Bucket' => $bucket,
825 + 'OwnershipControls' => [
826 + 'Rules' => [['ObjectOwnership' => $ownership]],
827 + ],
828 + ]);
372 829
373 - return true;
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 + }
374 838 }
375 839
376 840 /**
377 841 * Change transfer accilaration
@@ -383,16 +847,24 @@
383 847
384 848 if(empty($bucket)) return false;
385 849 if(!$force && !$enable) return true;
386 850
387 - $s3Client->putBucketAccelerateConfiguration([
388 - 'Bucket' => $bucket,
389 - 'AccelerateConfiguration' => [
390 - 'Status' => $enable ? 'Enabled' : 'Suspended'
391 - ]
392 - ]);
393 -
394 - return true;
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 + }
395 867 }
396 868
397 869 /**
398 870 * isConfigured Function To Identify the congfigurations are correct
@@ -400,18 +872,35 @@
400 872 */
401 873 public function isConfigured(){
402 874 if ($this->s3Client) {
403 875 try {
404 - $buckets = $s3Client->listBuckets();
405 - if(!empty($buckets)){
406 - foreach($buckets as $bucket) {
407 - if ($bucket['Name']==$this->bucket_name) {
408 - return true;
409 - }
410 - }
411 - }
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) {
412 901 return false;
413 - } catch (AwsException $ex) {
902 + } catch (Exception $ex) {
414 903 return false;
415 904 }
416 905 }
417 906 return false;
@@ -423,8 +912,9 @@
423 912 *
424 913 */
425 914 public function toPrivate($key) {
426 915 if(!$key) return false;
916 + if(!$this->s3Client) return false;
427 917 try {
428 918 $this->s3Client->putObjectAcl([
429 919 'Bucket' => $this->bucket_name,
430 920 'Key' => $key,
@@ -433,9 +923,8 @@
433 923 return true;
434 924 } catch (AwsException $ex) {
435 925 return false;
436 926 }
437 - return false;
438 927 }
439 928
440 929
441 930
@@ -441,23 +930,23 @@
441 930
442 931 /**
443 932 * Make Object Public
444 933 * @since 1.0.0
445 - *
934 + *
446 935 */
447 936 public function toPublic($key) {
448 937 if(!$key) return false;
938 + if(!$this->s3Client) return false;
449 939 try {
450 940 $this->s3Client->putObjectAcl([
451 941 'Bucket' => $this->bucket_name,
452 942 'Key' => $key,
453 943 'ACL' => 'public-read'
454 - ]);
944 + ]);
455 945 return true;
456 946 } catch (AwsException $ex) {
457 947 return false;
458 948 }
459 - return false;
460 949 }
461 950
462 951
463 952
@@ -464,16 +953,76 @@
464 953 /**
465 954 * Check the object exist
466 955 * @since 1.1.8
467 956 */
468 - public function exists($key) {
957 + public function exists($key, $bucket_name = '', $client = null) {
469 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 + }
470 974
471 - if($this->s3Client->doesObjectExist($this->bucket_name, $key)) {
472 - return true;
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];
473 982 }
474 -
475 - return false;
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 + }
476 1025 }
477 1026
478 1027 /**
479 1028 * Upload Single
@@ -479,99 +1028,132 @@
479 1028 * Upload Single
480 1029 * @since 1.0.0
481 1030 * @return boolean
482 1031 */
483 - public function uploadSingle($media_absolute_path, $media_path, $prefix='') {
484 - $result = array();
1032 + public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false) {
485 1033 if (
486 - isset($media_absolute_path) && !empty($media_absolute_path) &&
487 - isset($media_path) && !empty($media_path)
1034 + isset($absolute_source_path) && !empty($absolute_source_path) &&
1035 + isset($relative_source_path) && !empty($relative_source_path)
488 1036 ) {
489 - $file_name = wp_basename( $media_path );
1037 + $file_name = wp_basename( $relative_source_path );
490 1038 if ($file_name) {
491 - $upload_path = Utils::generate_object_key($file_name, $prefix);
492 -
493 - // Decide Multipart upload or normal put object
494 - if (filesize($media_absolute_path) <= Schema::getConstant('S3_MULTIPART_MIN_FILE_SIZE')) {
495 - // Upload a publicly accessible file. The file size and type are determined by the SDK.
496 - try {
497 - $upload = $this->s3Client->putObject([
498 - 'Bucket' => $this->bucket_name,
499 - 'Key' => $upload_path,
500 - 'Body' => fopen($media_absolute_path, 'r'),
501 - 'ACL' => 'public-read', // make file 'public'
502 - ]);
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 + }
503 1064
504 - $result = array(
505 - 'success' => true,
506 - 'code' => 200,
507 - 'file_url' => $upload->get('ObjectURL'),
508 - 'key' => $upload_path,
509 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
510 - );
511 - } catch (AwsException $e) {
512 - $result = array(
513 - 'success' => false,
514 - 'code' => 200,
515 - 'message' => $e->getMessage()
516 - );
517 - }
518 - } else {
519 - $multiUploader = new MultipartUploader($this->s3Client, $media_absolute_path, [
520 - 'bucket' => $this->bucket_name,
521 - 'key' => $upload_path,
522 - 'acl' => 'public-read', // make file 'public'
523 - ]);
524 -
525 - try {
526 - do {
527 - try {
528 - $uploaded = $multiUploader->upload();
529 - } catch (MultipartUploadException $e) {
530 - $multiUploader = new MultipartUploader($this->s3Client, $media_absolute_path, [
531 - 'state' => $e->getState(),
532 - ]);
533 - }
534 - } while (!isset($uploaded));
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 + }
535 1072
536 - if (isset($uploaded['ObjectURL']) && !empty($uploaded['ObjectURL'])) {
537 - $result = array(
538 - 'success' => true,
539 - 'code' => 200,
540 - 'file_url' => urldecode($uploaded['ObjectURL']),
541 - 'key' => $upload_path,
542 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
543 - );
544 - } else {
545 - $result = array(
546 - 'success' => false,
547 - 'code' => 200,
548 - 'message' => esc_html__('Something happened while uploading to server', 'media-cloud-sync')
549 - );
550 - }
551 - } catch (MultipartUploadException $e) {
552 - $result = array(
553 - 'success' => false,
554 - 'code' => 200,
555 - 'message' => $e->getMessage()
556 - );
557 - }
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 + ];
558 1140 }
559 - } else {
560 - $result = array(
1141 + $options = ['state' => $e->getState()];
1142 + } catch (AwsException $e) {
1143 + return [
561 1144 'success' => false,
562 1145 'code' => 200,
563 - 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
564 - );
1146 + 'message' => $e->getMessage()
1147 + ];
1148 + } catch (Exception $e) {
1149 + return [
1150 + 'success' => false,
1151 + 'code' => 200,
1152 + 'message' => $e->getMessage()
1153 + ];
565 1154 }
566 - } else {
567 - $result = array(
568 - 'success' => false,
569 - 'code' => 200,
570 - 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
571 - );
572 1155 }
573 - return $result;
574 1156 }
575 1157
576 1158 /**
577 1159 * Save object to server
@@ -577,8 +1159,9 @@
577 1159 * Save object to server
578 1160 * @since 1.0.0
579 1161 */
580 1162 public function object_to_server($key, $save_path) {
1163 + if(!$this->s3Client) return false;
581 1164 try {
582 1165 $getObject = $this->s3Client->GetObject([
583 1166 'Bucket' => $this->bucket_name,
584 1167 'Key' => $key,
@@ -592,10 +1175,156 @@
592 1175 }
593 1176 return false;
594 1177 }
595 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 + }
596 1196
597 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 + /**
598 1327 * Delete Single
599 1328 * @since 1.0.0
600 1329 * @return boolean
601 1330 */
@@ -600,8 +1329,15 @@
600 1329 * @return boolean
601 1330 */
602 1331 public function deleteSingle($key) {
603 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 + }
604 1340 if (isset($key) && !empty($key)) {
605 1341 try {
606 1342 $this->s3Client->deleteObject([
607 1343 'Bucket' => $this->bucket_name,
@@ -607,9 +1343,9 @@
607 1343 'Bucket' => $this->bucket_name,
608 1344 'Key' => $key
609 1345 ]);
610 1346
611 - if (!$this->s3Client->doesObjectExist($this->bucket_name, $key)) {
1347 + if (!$this->exists($key)) {
612 1348 $result = array(
613 1349 'success' => true,
614 1350 'code' => 200,
615 1351 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync')
@@ -638,14 +1374,21 @@
638 1374 return $result;
639 1375 }
640 1376
641 1377 /**
642 - * get presigned URL
1378 + * get private URL
643 1379 * @since 1.0.0
644 1380 * @return boolean
645 1381 */
646 - public function get_presigned_url($key) {
1382 + public function get_private_url($key) {
647 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 + }
648 1391 if (isset($key) && !empty($key)) {
649 1392 try {
650 1393 $cmd = $this->s3Client->getCommand('GetObject', [
651 1394 'Bucket' => $this->bucket_name,
@@ -651,24 +1394,24 @@
651 1394 'Bucket' => $this->bucket_name,
652 1395 'Key' => $key
653 1396 ]);
654 1397
655 - $expires = isset($this->settings['presigned_expire']) ? $this->settings['presigned_expire'] : 20;
1398 + $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
656 1399
657 1400 $request = $this->s3Client->createPresignedRequest($cmd, sprintf('+%s minutes', $expires));
658 1401
659 - if ($presignedUrl = (string)$request->getUri()) {
1402 + if ($privateUrl = (string)$request->getUri()) {
660 1403 $result = array(
661 1404 'success' => true,
662 1405 'code' => 200,
663 - 'file_url' => $presignedUrl,
664 - 'message' => esc_html__('Got Presigned URL Successfully', 'media-cloud-sync')
1406 + 'file_url' => $privateUrl,
1407 + 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync')
665 1408 );
666 1409 } else {
667 1410 $result = array(
668 1411 'success' => false,
669 1412 'code' => 200,
670 - 'message' => esc_html__('Error getting presigned URL', 'media-cloud-sync')
1413 + 'message' => esc_html__('Error getting private URL', 'media-cloud-sync')
671 1414 );
672 1415 }
673 1416 } catch (AwsException $e) {
674 1417 $result = array(
@@ -684,8 +1427,29 @@
684 1427 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
685 1428 );
686 1429 }
687 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);
688 1452 }
689 1453
690 1454 /**
691 1455 * Get domain URL