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/docean.php +784 -280 1.1.11.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 DOcean {
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 = 'docean';
26 29 public $DOClient = 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']) &&
@@ -64,10 +73,9 @@
64 73 $this->DOClient = new S3Client([
65 74 'version' => '2006-03-01',
66 75 'region' => $this->config['region'],
67 76 'endpoint' => $endpoint, // DigitalOcean Spaces requires a custom endpoint
68 - 'use_accelerate_endpoint' => isset($this->bucketConfig['transfer_acceleration'])
69 - ? $this->bucketConfig['transfer_acceleration'] : false,
77 + 'use_accelerate_endpoint' => false,
70 78 'use_path_style_endpoint' => true, // DigitalOcean Spaces often requires path-style endpoints
71 79 'use_aws_shared_config_files' => false,
72 80 'credentials' => [
73 81 'key' => $this->config['access_key'],
@@ -83,14 +91,13 @@
83 91 * Verify Credentials
84 92 * @since 1.0.0
85 93 * @return boolean
86 94 */
87 - public function verifyCredentials($access_key, $secret_key, $region){
88 - if (
89 - isset($region) && !empty($region) &&
90 - isset($access_key) && !empty($access_key) &&
91 - isset($secret_key) && !empty($secret_key)
92 - ) {
95 + public function verifyCredentials($config = []) {
96 + $region = isset($config['region']) ? $config['region'] : '';
97 + $access_key = isset($config['access_key']) ? $config['access_key'] : '';
98 + $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
99 + if (!Service::has_missing_fields([$region, $access_key, $secret_key])) {
93 100 try {
94 101 $endpoint = $this->get_domain($region);
95 102
96 103 $DOClient = new S3Client([
@@ -97,8 +104,9 @@
97 104 'version' => '2006-03-01',
98 105 'region' => $region,
99 106 'endpoint' => $endpoint,
100 107 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
108 + 'use_accelerate_endpoint' => false,
101 109 'use_aws_shared_config_files' => false,
102 110 'credentials' => [
103 111 'key' => $access_key,
104 112 'secret' => $secret_key,
@@ -103,12 +111,75 @@
103 111 'key' => $access_key,
104 112 'secret' => $secret_key,
105 113 ],
106 114 ]);
115 +
116 + $result = [
117 + 'success' => false,
118 + 'code' => 200,
119 + 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
120 + ];
121 +
122 + try {
123 + $DOClient->listObjectsV2([
124 + 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
125 + ]);
107 126
108 - return array( 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'), 'code' => 200, 'success' => true);
109 - }
110 - catch (S3Exception $ex) {
127 + // If we reach here, the credentials are valid
128 + $result = [
129 + 'success' => true,
130 + 'code' => 200,
131 + 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
132 + ];
133 + } catch (AwsException $e) {
134 + $code = $e->getAwsErrorCode();
135 +
136 + $validErrors = [
137 + 'AccessDenied',
138 + 'NoSuchBucket',
139 + 'AllAccessDisabled',
140 + 'AuthorizationHeaderMalformed',
141 + 'PermanentRedirect',
142 + 'InvalidBucketName',
143 + ];
144 +
145 + if (in_array($code, $validErrors)) {
146 + // If we reach here, the credentials are valid
147 + $result = [
148 + 'success' => true,
149 + 'code' => 200,
150 + 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
151 + ];
152 + }
153 + }
154 +
155 + if($result['success'] == false) {
156 + return $result;
157 + }
158 + try {
159 + $buckets = $DOClient->listBuckets();
160 + $newBucketFormat = [];
161 + if(isset($buckets['Buckets']) && !empty($buckets['Buckets'])){
162 + foreach($buckets['Buckets'] as $bucket) {
163 + if(isset($bucket['Name'])) {
164 + $newBucketFormat[] = ['Name' => $bucket['Name'], 'CreationDate' => $bucket['CreationDate'] ?? ''];
165 + }
166 + }
167 + }
168 + $result['buckets_data']['buckets'] = $newBucketFormat;
169 + $result['buckets_data']['message'] = esc_html__('Buckets listed successfully', 'media-cloud-sync');
170 + $result['buckets_data']['status'] = true;
171 + } catch (S3Exception $e) {
172 + $result ['buckets_data']['buckets'] = [];
173 + $result ['buckets_data']['message'] = esc_html__('Unable to list buckets, Please check the bucket listing permission', 'media-cloud-sync');
174 + $result ['buckets_data']['status'] = false;
175 + } catch (Exception $e) {
176 + $result ['buckets_data']['buckets'] = [];
177 + $result ['buckets_data']['message'] = esc_html__('Unable to list buckets, Please check the bucket listing permission', 'media-cloud-sync');
178 + $result ['buckets_data']['status'] = false;
179 + }
180 + return $result;
181 + } catch (S3Exception $ex) {
111 182 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
112 183 } catch (Exception $ex) {
113 184 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
114 185 }
@@ -120,15 +191,15 @@
120 191 * Verify Bucket
121 192 * @since 1.0.0
122 193 * @return boolean
123 194 */
124 - public function verifyBucket($access_key, $secret_key, $region, $bucket_name, $transfer_acceleration=false){
125 - if (
126 - isset($region) && !empty($region) &&
127 - isset($access_key) && !empty($access_key) &&
128 - isset($secret_key) && !empty($secret_key) &&
129 - isset($bucket_name) && !empty($bucket_name)
130 - ) {
195 + public function verifyBucketExist( $config = [], $bucketConfig = [] ) {
196 + $region = isset($config['region']) ? $config['region'] : '';
197 + $access_key = isset($config['access_key']) ? $config['access_key'] : '';
198 + $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
199 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
200 +
201 + if (!Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
131 202 try {
132 203 $endpoint = $this->get_domain($region);
133 204
134 205 $DOClient = new S3Client([
@@ -135,9 +206,9 @@
135 206 'version' => '2006-03-01',
136 207 'region' => $region,
137 208 'endpoint' => $endpoint,
138 209 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
139 - 'use_accelerate_endpoint' => $transfer_acceleration, // Transfer acceleration for DigitalOcean Spaces
210 + 'use_accelerate_endpoint' => false,
140 211 'use_aws_shared_config_files' => false,
141 212 'credentials' => [
142 213 'key' => $access_key,
143 214 'secret' => $secret_key,
@@ -143,59 +214,27 @@
143 214 'secret' => $secret_key,
144 215 ],
145 216 ]);
146 217
147 - $upload_dir = wp_upload_dir();
148 - $file_dir = $upload_dir['basedir'] . '/' . Schema::getConstant('UPLOADS') . '/';
149 -
150 - if (!is_dir($file_dir)) {
151 - do_action( $this->token.'_create_plugin_dir' );
152 - }
153 -
154 - $fileName = $this->token."_verify.txt";
155 - $localFile = $file_dir.$this->token."-local-verify.txt";
156 -
157 - $verify_file = fopen($file_dir.$fileName, "w");
158 - $txt = "We are verifying input/output operations in Digital Ocean\n";
159 - fwrite($verify_file, $txt);
160 - fclose($verify_file);
161 -
162 - $upload = $DOClient->putObject([
163 - 'Bucket' => $bucket_name,
164 - 'Key' => $fileName,
165 - 'Body' => fopen($file_dir.$fileName, "r"),
166 - 'ACL' => 'public-read', // make file 'public'
167 - ]);
218 + //get S3 object
219 + $bucket_found = false;
220 + try {
221 + $DOClient->getObject([
222 + 'Bucket' => $bucket_name,
223 + 'Key' => $this->token . '_dummy-object-for-bucket-exist-check'
224 + ]);
225 + $bucket_found = true;
226 + } catch (AwsException $e) {
227 + $code = $e->getAwsErrorCode();
228 + if ($code === 'NoSuchKey') {
229 + $bucket_found = true;
230 + }
231 + }
168 232
169 - @unlink($file_dir.$fileName);
170 - if ($upload->get('ObjectURL')) {
171 - try {
172 - $getObject = $DOClient->GetObject([
173 - 'Bucket' => $bucket_name,
174 - 'Key' => $fileName,
175 - 'SaveAs' => $localFile
176 - ]);
177 -
178 - if (file_exists($localFile)) {
179 - @unlink($localFile);
180 - $DOClient->deleteObject([
181 - 'Bucket' => $bucket_name,
182 - 'Key' => $fileName,
183 - ]);
184 -
185 - if (!$DOClient->doesObjectExist($bucket_name, $fileName)) {
186 - return array('message' => esc_html__('Configuration for Digital Ocean has verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true);
187 - } else {
188 - 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);
189 - }
190 - } else {
191 - 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);
192 - }
193 - } catch (S3Exception $ex) {
194 - return array('message' => $ex->getAwsErrorMessage(), 'code' => $ex->getAwsErrorCode(), 'success' => false);
195 - }
233 + if($bucket_found) {
234 + return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
196 235 } else {
197 - 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);
236 + return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
198 237 }
199 238 }
200 239 catch (S3Exception $ex) {
201 240 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
@@ -210,10 +249,15 @@
210 249 * Create Bucket
211 250 * @since 1.0.0
212 251 * @return boolean
213 252 */
214 - public function createBucket($access_key, $secret_key, $region, $bucket_name, $transfer_acceleration = false){
215 - if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
253 + public function createBucket( $config = [], $bucketConfig = [] ) {
254 + $region = isset($config['region']) ? $config['region'] : '';
255 + $access_key = isset($config['access_key']) ? $config['access_key'] : '';
256 + $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
257 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
258 +
259 + if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
216 260 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
217 261 }
218 262
219 263 try {
@@ -223,8 +267,9 @@
223 267 'version' => '2006-03-01',
224 268 'region' => $region,
225 269 'endpoint' => $endpoint,
226 270 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
271 + 'use_accelerate_endpoint' => false,
227 272 'use_aws_shared_config_files' => false,
228 273 'credentials' => [
229 274 'key' => $access_key,
230 275 'secret' => $secret_key,
@@ -231,29 +276,28 @@
231 276 ],
232 277 ]);
233 278
234 279 // Create Bucket
235 - $DOClient->createBucket(['Bucket' => $bucket_name]);
280 + $DOClient->createBucket([
281 + 'Bucket' => $bucket_name,
282 + ]);
283 +
284 + // Optionally wait for bucket existence (recommended)
285 + $DOClient->waitUntil('BucketExists', ['Bucket' => $bucket_name]);
286 +
236 287 try {
237 - $this->enablBucketPublicAccess($bucket_name, $DOClient);
238 288 $this->putBucketPolicy($bucket_name, $DOClient);
239 - $this->changeBucketOwnership($bucket_name, $DOClient);
240 - try {
241 -
242 - $this->changeTransferAccilaration($bucket_name, $DOClient, $transfer_acceleration);
243 -
244 - return [
245 - 'message' => esc_html__('Bucket created successfully. Choose bucket from list to select the bucket.', 'media-cloud-sync'),
246 - 'data' => [
247 - 'Name' => $bucket_name,
248 - 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
249 - ],
250 - 'code' => 200,
251 - 'success' => true,
252 - ];
253 - } catch (AwsException $ex) {
254 - 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];
255 - }
289 +
290 + return [
291 + 'message' => esc_html__('Bucket created successfully.', 'media-cloud-sync'),
292 + 'data' => [
293 + 'Name' => $bucket_name,
294 + 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
295 + ],
296 + 'code' => 200,
297 + 'success' => true,
298 + ];
299 +
256 300 } catch (AwsException $ex) {
257 301 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];
258 302 }
259 303 } catch (AwsException $ex) {
@@ -263,14 +307,21 @@
263 307 } catch (Exception $ex) {
264 308 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
265 309 }
266 310 }
311 +
312 +
267 313 /**
268 - * Make Public Access Block Settings Enable
314 + * Add Bucket Policy
315 + *
316 + * $private_prefix, when non-empty, carves that path out of the public
317 + * grant entirely — every action in the list, not just reads, so an
318 + * anonymous caller can't read, write, or delete anything under it. Same
319 + * NotResource approach as S3::putBucketPolicy() — Spaces' policy API is
320 + * S3-compatible, so the identical fix applies unchanged.
269 321 * @since 1.0.0
270 322 */
271 -
272 - private function enablBucketPublicAccess($bucket, $DOClient = false) {
323 + private function putBucketPolicy($bucket, $DOClient = false, $private_prefix = '') {
273 324 if($DOClient == false) {
274 325 $DOClient = $this->DOClient;
275 326 }
276 327
@@ -275,109 +326,271 @@
275 326 }
276 327
277 328 if(empty($bucket)) return false;
278 329
279 - $DOClient->putPublicAccessBlock([
280 - 'Bucket' => $bucket,
281 - 'PublicAccessBlockConfiguration' => [
282 - 'BlockPublicPolicy' => false,
283 - 'BlockPublicAcls' => false,
284 - 'IgnorePublicAcls' => false,
285 - 'RestrictPublicBuckets' => false,
286 - ]
330 + $actions = [
331 + "s3:DeleteObjectTagging",
332 + "s3:ListBucketMultipartUploads",
333 + "s3:DeleteObjectVersion",
334 + "s3:ListBucket",
335 + "s3:DeleteObjectVersionTagging",
336 + "s3:GetBucketAcl",
337 + "s3:ListMultipartUploadParts",
338 + "s3:PutObject",
339 + "s3:GetObjectAcl",
340 + "s3:GetObject",
341 + "s3:AbortMultipartUpload",
342 + "s3:DeleteObject",
343 + "s3:GetBucketLocation",
344 + "s3:PutObjectAcl",
345 + "s3:putBucketOwnershipControls",
346 + "s3:putBucketPolicy"
347 + ];
348 +
349 + $statement = [
350 + "Effect" => "Allow",
351 + "Principal" => "*",
352 + "Action" => $actions,
353 + ];
354 +
355 + if (!empty($private_prefix)) {
356 + $statement["NotResource"] = ["arn:aws:s3:::$bucket/$private_prefix/*"];
357 + } else {
358 + $statement["Resource"] = [
359 + "arn:aws:s3:::$bucket/*",
360 + "arn:aws:s3:::$bucket"
361 + ];
362 + }
363 +
364 + $policy = json_encode([
365 + "Version" => "2012-10-17",
366 + "Statement" => [$statement]
287 367 ]);
288 368
289 - return true;
369 + try {
370 + // Add bucket policy
371 + $DOClient->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
372 +
373 + return true;
374 + } catch (AwsException $ex) {
375 + return false; // Handle AWS specific exceptions
376 + } catch (S3Exception $ex) {
377 + return false; // Handle S3 specific exceptions
378 + } catch (Exception $ex) {
379 + return false; // Handle general exceptions
380 + }
290 381 }
291 382
292 383 /**
293 - * Add Bucket Policy
384 + * Apply (or, with an empty $private_prefix, un-apply) the private-path
385 + * bucket policy carve-out.
386 + * @since 1.0.0
294 387 */
295 - private function putBucketPolicy($bucket, $DOClient = false) {
296 - if($DOClient == false) {
297 - $DOClient = $this->DOClient;
388 + public function applyPrivatePathPolicy($private_prefix) {
389 + if (!$this->DOClient || empty($this->bucket_name)) {
390 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
298 391 }
299 392
300 - if(empty($bucket)) return false;
393 + $ok = $this->putBucketPolicy($this->bucket_name, $this->DOClient, $private_prefix);
301 394
302 - $policy = '{
303 - "Version": "2012-10-17",
304 - "Statement": [
305 - {
306 - "Effect": "Allow",
307 - "Principal": "*",
308 - "Action": [
309 - "s3:DeleteObjectTagging",
310 - "s3:ListBucketMultipartUploads",
311 - "s3:DeleteObjectVersion",
312 - "s3:ListBucket",
313 - "s3:DeleteObjectVersionTagging",
314 - "s3:GetBucketAcl",
315 - "s3:ListMultipartUploadParts",
316 - "s3:PutObject",
317 - "s3:GetObjectAcl",
318 - "s3:GetObject",
319 - "s3:AbortMultipartUpload",
320 - "s3:DeleteObject",
321 - "s3:GetBucketLocation",
322 - "s3:PutObjectAcl",
323 - "s3:putBucketOwnershipControls",
324 - "s3:putBucketPolicy"
325 - ],
326 - "Resource": [
327 - "arn:aws:s3:::' . $bucket . '/*",
328 - "arn:aws:s3:::' . $bucket . '"
329 - ]
330 - }
331 - ]
332 - }';
395 + return $ok
396 + ? ['success' => true, 'code' => 200, 'message' => esc_html__('Policy applied successfully', 'media-cloud-sync')]
397 + : ['success' => false, 'code' => 200, 'message' => esc_html__('Failed to apply bucket policy', 'media-cloud-sync')];
398 + }
333 399
334 - // Add bucket policy
335 - $DOClient->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
336 400
337 - return true;
401 +
402 + /**
403 + * Check Bucket Write Permission
404 + * @since 1.0.0
405 + */
406 + public function verifyObjectWritePermission( $config = [], $bucketConfig = [] ) {
407 + $region = isset($config['region']) ? $config['region'] : '';
408 + $access_key = isset($config['access_key']) ? $config['access_key'] : '';
409 + $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
410 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
411 +
412 + if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
413 + return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
414 + }
415 +
416 + try {
417 + $endpoint = $this->get_domain($region);
418 +
419 + $DOClient = new S3Client([
420 + 'version' => '2006-03-01',
421 + 'region' => $region,
422 + 'endpoint' => $endpoint,
423 + 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
424 + 'use_accelerate_endpoint' => false,
425 + 'use_aws_shared_config_files' => false,
426 + 'credentials' => [
427 + 'key' => $access_key,
428 + 'secret' => $secret_key,
429 + ],
430 + ]);
431 +
432 + $object_key = Utils::get_permission_check_object_key();
433 +
434 +
435 + // Create a dummy object to check write permission
436 + $DOClient->putObject([
437 + 'Bucket' => $bucket_name,
438 + 'Key' => $object_key,
439 + 'Body' => 'This is a test object to check write permission.',
440 + ]);
441 + // Check if the object was created successfully
442 + if ($this->exists($object_key, $bucket_name, $DOClient)) {
443 + return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
444 + } else {
445 + return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
446 + }
447 +
448 + } catch (AwsException $ex) {
449 + return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
450 + } catch (S3Exception $ex) {
451 + return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
452 + } catch (Exception $ex) {
453 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
454 + }
338 455 }
339 456
457 +
458 +
340 459 /**
341 - * Add Bucket Ownership
342 - */
343 - private function changeBucketOwnership($bucket, $DOClient = false, $ownership = 'BucketOwnerPreferred') {
344 - if($DOClient == false) {
345 - $DOClient = $this->DOClient;
460 + * Check Bucket Delete Permission
461 + * @since 1.0.0
462 + */
463 + public function verifyObjectDeletePermission( $config = [], $bucketConfig = [] ) {
464 + $region = isset($config['region']) ? $config['region'] : '';
465 + $access_key = isset($config['access_key']) ? $config['access_key'] : '';
466 + $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
467 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
468 +
469 + if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
470 + return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
346 471 }
347 472
348 - if(empty($bucket)) return false;
473 + try {
474 + $endpoint = $this->get_domain($region);
349 475
350 - // Change object ownership ACL enabled
351 - $DOClient->putBucketOwnershipControls([
352 - 'Bucket' => $bucket,
353 - 'OwnershipControls' => [
354 - 'Rules' => [['ObjectOwnership' => $ownership]],
355 - ],
356 - ]);
476 + $DOClient = new S3Client([
477 + 'version' => '2006-03-01',
478 + 'region' => $region,
479 + 'endpoint' => $endpoint,
480 + 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
481 + 'use_accelerate_endpoint' => false,
482 + 'use_aws_shared_config_files' => false,
483 + 'credentials' => [
484 + 'key' => $access_key,
485 + 'secret' => $secret_key,
486 + ],
487 + ]);
488 +
489 + $object_key = Utils::get_permission_check_object_key();
357 490
358 - return true;
491 + // Create a dummy object to check dlete permission
492 + $DOClient->deleteObject([
493 + 'Bucket' => $bucket_name,
494 + 'Key' => $object_key,
495 + ]);
496 +
497 + // Check if the object was created successfully
498 + if (!$this->exists($object_key, $bucket_name, $DOClient)) {
499 + return ['message' => esc_html__('Bucket delete permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
500 + } else {
501 + return ['message' => esc_html__('Bucket delete permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
502 + }
503 +
504 + } catch (AwsException $ex) {
505 + return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
506 + } catch (S3Exception $ex) {
507 + return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
508 + } catch (Exception $ex) {
509 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
510 + }
359 511 }
360 512
513 +
361 514 /**
362 - * Change transfer accilaration
515 + * Check Bucket Read Permission
516 + * @since 1.2.4
363 517 */
364 - private function changeTransferAccilaration($bucket, $DOClient = false, $enable=false, $force = false) {
365 - if($DOClient == false) {
366 - $DOClient = $this->DOClient;
518 + public function verifyObjectReadPermission() {
519 + $result = [
520 + 'status' => false,
521 + 'message' => '',
522 + 'lastChecked' => time(),
523 + ];
524 + if (Service::has_missing_fields([$this->DOClient, $this->bucket_name])) {
525 + $result['message'] = esc_html__('Invalid Request', 'media-cloud-sync');
526 + return ['message' => esc_html__('Invalid Request', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
367 527 }
368 528
369 - if(empty($bucket)) return false;
370 - if(!$force && !$enable) return true;
529 + try {
530 + $object_key = Utils::get_permission_check_object_key();
371 531
372 - $DOClient->putBucketAccelerateConfiguration([
373 - 'Bucket' => $bucket,
374 - 'AccelerateConfiguration' => [
375 - 'Status' => $enable ? 'Enabled' : 'Suspended'
376 - ]
377 - ]);
532 + // Check if the object was created successfully
533 + if (!$this->exists($object_key)) {
534 + // Create a dummy object to check write permission
535 + $this->DOClient->putObject([
536 + 'Bucket' => $this->bucket_name,
537 + 'Key' => $object_key,
538 + 'Body' => 'This is a test object to check permission.',
539 + 'ContentType' => 'text/plain',
540 + 'CacheControl' => 'no-cache, no-store, must-revalidate',
541 + ]);
542 + }
378 543
379 - return true;
544 +
545 + $url = $this->generate_file_url($object_key);
546 + $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
547 + // Never trust a cached response for this fixed, predictable URL — a stale cached
548 + // error would otherwise keep failing the check long after real access is fine.
549 + $no_cache_context = stream_context_create(['http' => ['header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"]]);
550 + $headers = @get_headers($cdn_url, false, $no_cache_context);
551 + $status_code = (is_array($headers) && !empty($headers[0]) && preg_match('/\s(\d{3})\s/', $headers[0], $matches))
552 + ? (int) $matches[1]
553 + : 0;
554 +
555 + if ($status_code === 200) {
556 + $result['status'] = true;
557 + $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
558 + } else if ($status_code === 403) {
559 + $result['status'] = false;
560 + if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
561 + $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
562 + } else {
563 + $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
564 + }
565 + } else if ($status_code === 404) {
566 + $result['status'] = false;
567 + $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
568 + } else if ($status_code === 500) {
569 + $result['status'] = false;
570 + $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
571 + } else {
572 + $result['status'] = false;
573 + $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
574 + }
575 +
576 + $this->deleteSingle($object_key);
577 + return [
578 + 'message' => $result['message'],
579 + 'code' => 200,
580 + 'success' => $result['status'],
581 + 'lastChecked' => $result['lastChecked'],
582 + ];
583 + } catch (AwsException $ex) {
584 + $result['message'] = $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
585 + return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
586 + } catch (S3Exception $ex) {
587 + $result['message'] = $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
588 + return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
589 + } catch (Exception $ex) {
590 + $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
591 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
592 + }
380 593 }
381 594
382 595 /**
383 596 * isConfigured Function To Identify the congfigurations are correct
@@ -385,19 +598,33 @@
385 598 */
386 599 public function isConfigured(){
387 600 if ($this->DOClient) {
388 601 try {
389 - $buckets = $DOClient->listBuckets();
390 - if(!empty($buckets)){
391 - foreach($buckets as $bucket) {
392 - if ($bucket['Name']==$this->bucket_name) {
393 - return true;
394 - }
395 - }
396 - }
397 - return false;
398 - } catch (AwsException $ex) {
399 - return false;
602 + $this->DOClient->listObjectsV2([
603 + 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
604 + ]);
605 +
606 + // If we reach here, the credentials are valid
607 + return true;
608 + } catch (AwsException $e) {
609 + $code = $e->getAwsErrorCode();
610 +
611 + $validErrors = [
612 + 'AccessDenied',
613 + 'NoSuchBucket',
614 + 'AllAccessDisabled',
615 + 'AuthorizationHeaderMalformed',
616 + 'PermanentRedirect',
617 + 'InvalidBucketName',
618 + ];
619 +
620 + if (in_array($code, $validErrors)) {
621 + // If we reach here, the credentials are valid
622 + return true;
623 + } else {
624 + // If we reach here, the credentials are not valid
625 + return false;
626 + }
400 627 }
401 628 }
402 629 return false;
403 630 }
@@ -408,8 +635,9 @@
408 635 *
409 636 */
410 637 public function toPrivate($key) {
411 638 if(!$key) return false;
639 + if(!$this->DOClient) return false;
412 640 try {
413 641 $this->DOClient->putObjectAcl([
414 642 'Bucket' => $this->bucket_name,
415 643 'Key' => $key,
@@ -418,9 +646,8 @@
418 646 return true;
419 647 } catch (AwsException $ex) {
420 648 return false;
421 649 }
422 - return false;
423 650 }
424 651
425 652
426 653
@@ -426,23 +653,23 @@
426 653
427 654 /**
428 655 * Make Object Public
429 656 * @since 1.0.0
430 - *
657 + *
431 658 */
432 659 public function toPublic($key) {
433 660 if(!$key) return false;
661 + if(!$this->DOClient) return false;
434 662 try {
435 663 $this->DOClient->putObjectAcl([
436 664 'Bucket' => $this->bucket_name,
437 665 'Key' => $key,
438 666 'ACL' => 'public-read'
439 - ]);
667 + ]);
440 668 return true;
441 669 } catch (AwsException $ex) {
442 670 return false;
443 671 }
444 - return false;
445 672 }
446 673
447 674
448 675
@@ -449,114 +676,208 @@
449 676 /**
450 677 * Check the object exist
451 678 * @since 1.1.8
452 679 */
453 - public function exists($key) {
680 + public function exists($key, $bucket_name = '', $client = null) {
454 681 if(!$key) return false;
455 682
456 - if($this->DOClient->doesObjectExist($this->bucket_name, $key)) {
457 - return true;
683 + try {
684 + $client = $client ?? $this->DOClient;
685 + $bucket_name = !empty($bucket_name) ? $bucket_name : $this->bucket_name;
686 + if($client->doesObjectExistV2($bucket_name, $key)) {
687 + return true;
688 + }
689 + return false;
690 + } catch (AwsException $ex) {
691 + return false;
458 692 }
459 -
460 - return false;
693 + catch (S3Exception $ex) {
694 + return false;
695 + } catch (Exception $ex) {
696 + return false;
697 + }
461 698 }
462 699
463 700 /**
701 + * List Objects — $delimiter = null gives a flat/recursive listing instead of one folder level.
702 + * @since 1.3.13
703 + */
704 + public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
705 + if (!$this->DOClient) {
706 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
707 + }
708 + try {
709 + $params = ['Bucket' => $this->bucket_name, 'MaxKeys' => $maxKeys];
710 + if (!empty($delimiter)) {
711 + $params['Delimiter'] = $delimiter;
712 + }
713 + if (!empty($prefix)) {
714 + $params['Prefix'] = $prefix;
715 + }
716 + if (!empty($continuationToken)) {
717 + $params['ContinuationToken'] = $continuationToken;
718 + }
719 +
720 + $result = $this->DOClient->listObjectsV2($params);
721 + $folders = [];
722 + foreach (($result['CommonPrefixes'] ?? []) as $common) {
723 + $folders[] = $common['Prefix'];
724 + }
725 + $objects = [];
726 + foreach (($result['Contents'] ?? []) as $object) {
727 + if ($object['Key'] === $prefix) {
728 + continue; // the folder placeholder object itself, not a file
729 + }
730 + $objects[] = [
731 + 'key' => $object['Key'],
732 + 'size' => (int) $object['Size'],
733 + 'last_modified' => $object['LastModified'] ? $object['LastModified']->format(DATE_ATOM) : '',
734 + ];
735 + }
736 +
737 + return [
738 + 'success' => true,
739 + 'code' => 200,
740 + 'message' => '',
741 + 'folders' => $folders,
742 + 'objects' => $objects,
743 + 'next_token' => !empty($result['IsTruncated']) ? ($result['NextContinuationToken'] ?? null) : null,
744 + ];
745 + } catch (AwsException $e) {
746 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
747 + } catch (S3Exception $e) {
748 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
749 + } catch (Exception $e) {
750 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
751 + }
752 + }
753 +
754 + /**
464 755 * Upload Single
465 756 * @since 1.0.0
466 757 * @return boolean
467 758 */
468 - public function uploadSingle($media_absolute_path, $media_path, $prefix='') {
469 - $result = array();
759 + public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false) {
470 760 if (
471 - isset($media_absolute_path) && !empty($media_absolute_path) &&
472 - isset($media_path) && !empty($media_path)
761 + isset($absolute_source_path) && !empty($absolute_source_path) &&
762 + isset($relative_source_path) && !empty($relative_source_path)
473 763 ) {
474 - $file_name = wp_basename( $media_path );
764 + $file_name = wp_basename( $relative_source_path );
475 765 if ($file_name) {
476 - $upload_path = Utils::generate_object_key($file_name, $prefix);
477 -
478 - // Decide Multipart upload or normal put object
479 - if (filesize($media_absolute_path) <= Schema::getConstant('DOCEAN_MULTIPART_MIN_FILE_SIZE')) {
480 - // Upload a publicly accessible file. The file size and type are determined by the SDK.
481 - try {
482 - $upload = $this->DOClient->putObject([
483 - 'Bucket' => $this->bucket_name,
484 - 'Key' => $upload_path,
485 - 'Body' => fopen($media_absolute_path, 'r'),
486 - 'ACL' => 'public-read', // make file 'public'
487 - ]);
766 + $upload_path = Utils::generate_object_key($relative_source_path, $prefix, $is_private);
767 + if ($upload_path === false) {
768 + return [
769 + 'success' => false,
770 + 'code' => 200,
771 + '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')
772 + ];
773 + }
774 + return $this->execute_upload($absolute_source_path, $upload_path);
775 + }
776 + return [
777 + 'success' => false,
778 + 'code' => 200,
779 + 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
780 + ];
781 + }
782 + return [
783 + 'success' => false,
784 + 'code' => 200,
785 + 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
786 + ];
787 + }
488 788
489 - $result = array(
490 - 'success' => true,
491 - 'code' => 200,
492 - 'file_url' => $upload->get('ObjectURL'),
493 - 'key' => $upload_path,
494 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
495 - );
496 - } catch (AwsException $e) {
497 - $result = array(
498 - 'success' => false,
499 - 'code' => 200,
500 - 'message' => $e->getMessage()
501 - );
502 - }
503 - } else {
504 - $multiUploader = new MultipartUploader($this->DOClient, $media_absolute_path, [
505 - 'bucket' => $this->bucket_name,
506 - 'key' => $upload_path,
507 - 'acl' => 'public-read', // make file 'public'
508 - ]);
509 -
510 - try {
511 - do {
512 - try {
513 - $uploaded = $multiUploader->upload();
514 - } catch (MultipartUploadException $e) {
515 - $multiUploader = new MultipartUploader($this->DOClient, $media_absolute_path, [
516 - 'state' => $e->getState(),
517 - ]);
518 - }
519 - } while (!isset($uploaded));
789 + /**
790 + * Upload a local file to an exact destination key (no Utils::generate_object_key() derivation).
791 + * @since 1.4.0
792 + */
793 + public function uploadObjectAtKey($absolute_source_path, $key) {
794 + return $this->execute_upload($absolute_source_path, $key);
795 + }
520 796
521 - if (isset($uploaded['ObjectURL']) && !empty($uploaded['ObjectURL'])) {
522 - $result = array(
523 - 'success' => true,
524 - 'code' => 200,
525 - 'file_url' => urldecode($uploaded['ObjectURL']),
526 - 'key' => $upload_path,
527 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
528 - );
529 - } else {
530 - $result = array(
531 - 'success' => false,
532 - 'code' => 200,
533 - 'message' => esc_html__('Something happened while uploading to server', 'media-cloud-sync')
534 - );
535 - }
536 - } catch (MultipartUploadException $e) {
537 - $result = array(
538 - 'success' => false,
539 - 'code' => 200,
540 - 'message' => $e->getMessage()
541 - );
542 - }
797 + /**
798 + * Build an unexecuted ObjectUploader (single PUT or multipart, decided internally by the
799 + * SDK, using this plugin's own multipart threshold rather than the SDK's 16MB default).
800 + * ACL is stripped via before_* hooks — this plugin's model is bucket-level, not per-object,
801 + * and an explicit `ACL: null` still serializes to an empty x-amz-acl header otherwise.
802 + * $options is threaded straight into the SDK (e.g. 'state' => UploadState to resume a
803 + * previously-failed multipart attempt).
804 + * @since 1.4.0
805 + */
806 + private function build_object_uploader($absolute_source_path, $key, $options = []) {
807 + $handle = fopen($absolute_source_path, 'rb');
808 + $params = [];
809 + $cache_control = Utils::get_cache_control_header();
810 + if ($cache_control) {
811 + $params['CacheControl'] = $cache_control;
812 + }
813 + $options += [
814 + 'mup_threshold' => Schema::getConstant('DOCEAN_MULTIPART_MIN_FILE_SIZE'),
815 + 'params' => $params,
816 + 'before_initiate' => function ($params) { return $this->strip_acl($params); },
817 + 'before_upload' => function ($params) { return $this->strip_acl($params); },
818 + 'before_complete' => function ($params) { return $this->strip_acl($params); },
819 + ];
820 + return new ObjectUploader($this->DOClient, $this->bucket_name, $key, $handle, null, $options);
821 + }
822 +
823 + // Mutate in place, not a clone — the SDK's before_* hooks call this and discard the
824 + // return value, relying on the same Command object being modified.
825 + private function strip_acl($params) {
826 + if ($params instanceof Command && $params->hasParam('ACL')) {
827 + unset($params['ACL']);
828 + } elseif (is_array($params) && isset($params['ACL'])) {
829 + unset($params['ACL']);
830 + }
831 + return $params;
832 + }
833 +
834 + /**
835 + * Run an ObjectUploader synchronously and normalize the result shape. Retries up to
836 + * 3 attempts on MultipartUploadException, resuming from the failed attempt's saved
837 + * state rather than restarting the whole upload — same retry contract uploadSingle()
838 + * had before the ObjectUploader swap.
839 + * @since 1.4.0
840 + */
841 + private function execute_upload($absolute_source_path, $key) {
842 + $max_attempts = 3;
843 + $attempt = 0;
844 + $options = [];
845 +
846 + while (true) {
847 + $attempt++;
848 + try {
849 + $this->build_object_uploader($absolute_source_path, $key, $options)->upload();
850 + return [
851 + 'success' => true,
852 + 'code' => 200,
853 + 'file_url' => $this->generate_file_url($key),
854 + 'key' => $key,
855 + 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
856 + ];
857 + } catch (MultipartUploadException $e) {
858 + if ($attempt >= $max_attempts) {
859 + return [
860 + 'success' => false,
861 + 'code' => 200,
862 + 'message' => $e->getMessage()
863 + ];
543 864 }
544 - } else {
545 - $result = array(
865 + $options = ['state' => $e->getState()];
866 + } catch (AwsException $e) {
867 + return [
546 868 'success' => false,
547 869 'code' => 200,
548 - 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
549 - );
870 + 'message' => $e->getMessage()
871 + ];
872 + } catch (Exception $e) {
873 + return [
874 + 'success' => false,
875 + 'code' => 200,
876 + 'message' => $e->getMessage()
877 + ];
550 878 }
551 - } else {
552 - $result = array(
553 - 'success' => false,
554 - 'code' => 200,
555 - 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
556 - );
557 879 }
558 - return $result;
559 880 }
560 881
561 882 /**
562 883 * Save object to server
@@ -562,8 +883,9 @@
562 883 * Save object to server
563 884 * @since 1.0.0
564 885 */
565 886 public function object_to_server($key, $save_path) {
887 + if(!$this->DOClient) return false;
566 888 try {
567 889 $getObject = $this->DOClient->GetObject([
568 890 'Bucket' => $this->bucket_name,
569 891 'Key' => $key,
@@ -577,10 +899,155 @@
577 899 }
578 900 return false;
579 901 }
580 902
903 + /**
904 + * Object bytes in memory, no local file — for callers (e.g. zip download) that need
905 + * the content itself rather than a copy on the server's filesystem.
906 + * @since 1.3.13
907 + */
908 + public function get_object_content($key) {
909 + if(!$this->DOClient) return false;
910 + try {
911 + $result = $this->DOClient->GetObject([
912 + 'Bucket' => $this->bucket_name,
913 + 'Key' => $key,
914 + ]);
915 + return (string) $result['Body'];
916 + } catch (AwsException $e) {
917 + return false;
918 + }
919 + }
581 920
582 921 /**
922 + * Deletes the live object, then best-effort purges every historical version too — a
923 + * plain deleteSingle() on a versioned bucket only adds a delete marker, leaving prior
924 + * versions (and the storage they use) behind at the old key. The live delete happens
925 + * unconditionally first: DigitalOcean Spaces doesn't support object versioning at all,
926 + * so the version-listing part below simply fails there (caught, non-fatal) — the object
927 + * must still end up gone either way, which is why it can't be the only delete call.
928 + * @since 1.3.14
929 + */
930 + public function purge_all_versions($key) {
931 + if (!$this->DOClient) {
932 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
933 + }
934 +
935 + try {
936 + $this->DOClient->deleteObject([
937 + 'Bucket' => $this->bucket_name,
938 + 'Key' => $key,
939 + ]);
940 + } catch (AwsException $e) {
941 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
942 + }
943 +
944 + // Best-effort only from here — Spaces doesn't support version listing at all, so
945 + // this always no-ops there; the live object above is already gone regardless.
946 + try {
947 + $objects = [];
948 + $marker = null;
949 + do {
950 + $args = ['Bucket' => $this->bucket_name, 'Prefix' => $key];
951 + if ($marker) {
952 + $args['KeyMarker'] = $marker['key'];
953 + $args['VersionIdMarker'] = $marker['version'];
954 + }
955 + $result = $this->DOClient->listObjectVersions($args);
956 + foreach (array_merge($result['Versions'] ?? [], $result['DeleteMarkers'] ?? []) as $version) {
957 + if (($version['Key'] ?? null) === $key) {
958 + $objects[] = ['Key' => $key, 'VersionId' => $version['VersionId']];
959 + }
960 + }
961 + $marker = !empty($result['IsTruncated'])
962 + ? ['key' => $result['NextKeyMarker'], 'version' => $result['NextVersionIdMarker']]
963 + : null;
964 + } while ($marker);
965 +
966 + foreach (array_chunk($objects, 1000) as $chunk) {
967 + $this->DOClient->deleteObjects([
968 + 'Bucket' => $this->bucket_name,
969 + 'Delete' => ['Objects' => $chunk],
970 + ]);
971 + }
972 + } catch (AwsException $e) {
973 + // Version history cleanup unsupported/failed — not fatal, live object is gone.
974 + }
975 +
976 + return ['success' => true, 'code' => 200, 'message' => esc_html__('Purged Successfully', 'media-cloud-sync')];
977 + }
978 +
979 + /**
980 + * Copy to new path
981 + * @since 1.3.4
982 + */
983 + // Trusts copyObject()'s own success/failure rather than pre/post-verifying with extra
984 + // exists() HEAD requests — each one is a full network round-trip, and with move/copy
985 + // processing keys sequentially, three extra round-trips per file adds up fast on a
986 + // folder with many files. copyObject() itself throws (caught below) if the source is
987 + // missing or the copy otherwise fails, so nothing is lost by not checking first.
988 + public function copy_to_new_path($key, $new_path) {
989 + if (!$this->DOClient) {
990 + return [
991 + 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
992 + 'code' => 200,
993 + 'success' => false
994 + ];
995 + }
996 + try {
997 + $this->DOClient->copyObject([
998 + 'Bucket' => $this->bucket_name,
999 + 'CopySource' => "{$this->bucket_name}/{$key}",
1000 + 'Key' => $new_path,
1001 + 'MetadataDirective' => 'COPY',
1002 + ]);
1003 + return [
1004 + 'success' => true,
1005 + 'code' => 200,
1006 + 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1007 + ];
1008 + } catch (AwsException $e) {
1009 + return [
1010 + 'success' => false,
1011 + 'code' => 200,
1012 + 'message' => $e->getMessage()
1013 + ];
1014 + }
1015 + }
1016 +
1017 + // Like copy_to_new_path() but into an explicit (possibly different) bucket — needs write
1018 + // access there too, so callers should fall back to download+upload on failure.
1019 + public function copy_to_bucket($key, $new_key, $dest_bucket) {
1020 + if (!$this->DOClient) {
1021 + return [
1022 + 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
1023 + 'code' => 200,
1024 + 'success' => false
1025 + ];
1026 + }
1027 + try {
1028 + $this->DOClient->copyObject([
1029 + 'Bucket' => $dest_bucket,
1030 + 'CopySource' => "{$this->bucket_name}/{$key}",
1031 + 'Key' => $new_key,
1032 + 'MetadataDirective' => 'COPY',
1033 + ]);
1034 + return [
1035 + 'success' => true,
1036 + 'code' => 200,
1037 + 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1038 + ];
1039 + } catch (AwsException $e) {
1040 + return [
1041 + 'success' => false,
1042 + 'code' => 200,
1043 + 'message' => $e->getMessage()
1044 + ];
1045 + }
1046 + }
1047 +
1048 +
1049 + /**
583 1050 * Delete Single
584 1051 * @since 1.0.0
585 1052 * @return boolean
586 1053 */
@@ -585,8 +1052,15 @@
585 1052 * @return boolean
586 1053 */
587 1054 public function deleteSingle($key) {
588 1055 $result = array();
1056 + if (!$this->DOClient) {
1057 + return array(
1058 + 'success' => false,
1059 + 'code' => 200,
1060 + 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1061 + );
1062 + }
589 1063 if (isset($key) && !empty($key)) {
590 1064 try {
591 1065 $this->DOClient->deleteObject([
592 1066 'Bucket' => $this->bucket_name,
@@ -592,9 +1066,9 @@
592 1066 'Bucket' => $this->bucket_name,
593 1067 'Key' => $key
594 1068 ]);
595 1069
596 - if (!$this->DOClient->doesObjectExist($this->bucket_name, $key)) {
1070 + if (!$this->exists($key)) {
597 1071 $result = array(
598 1072 'success' => true,
599 1073 'code' => 200,
600 1074 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync')
@@ -623,14 +1097,21 @@
623 1097 return $result;
624 1098 }
625 1099
626 1100 /**
627 - * get presigned URL
1101 + * get private URL
628 1102 * @since 1.0.0
629 1103 * @return boolean
630 1104 */
631 - public function get_presigned_url($key) {
1105 + public function get_private_url($key) {
632 1106 $result = array();
1107 + if (!$this->DOClient) {
1108 + return array(
1109 + 'success' => false,
1110 + 'code' => 200,
1111 + 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1112 + );
1113 + }
633 1114 if (isset($key) && !empty($key)) {
634 1115 try {
635 1116 $cmd = $this->DOClient->getCommand('GetObject', [
636 1117 'Bucket' => $this->bucket_name,
@@ -636,24 +1117,24 @@
636 1117 'Bucket' => $this->bucket_name,
637 1118 'Key' => $key
638 1119 ]);
639 1120
640 - $expires = isset($this->settings['presigned_expire']) ? $this->settings['presigned_expire'] : 20;
1121 + $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
641 1122
642 1123 $request = $this->DOClient->createPresignedRequest($cmd, sprintf('+%s minutes', $expires));
643 1124
644 - if ($presignedUrl = (string)$request->getUri()) {
1125 + if ($privateUrl = (string)$request->getUri()) {
645 1126 $result = array(
646 1127 'success' => true,
647 1128 'code' => 200,
648 - 'file_url' => $presignedUrl,
649 - 'message' => esc_html__('Got Presigned URL Successfully', 'media-cloud-sync')
1129 + 'file_url' => $privateUrl,
1130 + 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync')
650 1131 );
651 1132 } else {
652 1133 $result = array(
653 1134 'success' => false,
654 1135 'code' => 200,
655 - 'message' => esc_html__('Error getting presigned URL', 'media-cloud-sync')
1136 + 'message' => esc_html__('Error getting Private URL', 'media-cloud-sync')
656 1137 );
657 1138 }
658 1139 } catch (AwsException $e) {
659 1140 $result = array(
@@ -669,8 +1150,31 @@
669 1150 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
670 1151 );
671 1152 }
672 1153 return $result;
1154 + }
1155 +
1156 + /**
1157 + * Generate file URL
1158 + */
1159 + public function generate_file_url($key){
1160 + $domain = $this->get_domain();
1161 +
1162 + return apply_filters('wpmcs_generate_do_file_url',
1163 + $domain . '/' . $this->bucket_name . '/' . $key,
1164 + $domain,
1165 + $this->bucket_name,
1166 + $key
1167 + );
1168 + }
1169 +
1170 + /**
1171 + * Is Provider URL
1172 + * @since 1.3.6
1173 + */
1174 + public function is_provider_url($url) {
1175 + $domain = $this->get_domain();
1176 + return (strpos($url, $domain . '/' . $this->bucket_name . '/') !== false);
673 1177 }
674 1178
675 1179 /**
676 1180 * Get domain URL