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 +777 -302 1.0.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 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,
@@ -104,23 +112,74 @@
104 112 'secret' => $secret_key,
105 113 ],
106 114 ]);
107 115
108 - //Listing all Digital Ocean Buckets
109 - $buckets = $DOClient->listBuckets();
116 + $result = [
117 + 'success' => false,
118 + 'code' => 200,
119 + 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
120 + ];
110 121
111 - $newBucketFormat = [];
112 - if(isset($buckets['Buckets']) && !empty($buckets['Buckets'])){
113 - foreach($buckets['Buckets'] as $bucket) {
114 - if(isset($bucket['Name'])) {
115 - $newBucketFormat[] = ['Name' => $bucket['Name'], 'CreationDate' => $bucket['CreationDate']];
122 + try {
123 + $DOClient->listObjectsV2([
124 + 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
125 + ]);
126 +
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 + }
116 166 }
117 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;
118 179 }
119 -
120 - return array( 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'), 'buckets' => $newBucketFormat, 'code' => 200, 'success' => true);
121 - }
122 - catch (S3Exception $ex) {
180 + return $result;
181 + } catch (S3Exception $ex) {
123 182 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
124 183 } catch (Exception $ex) {
125 184 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
126 185 }
@@ -132,15 +191,15 @@
132 191 * Verify Bucket
133 192 * @since 1.0.0
134 193 * @return boolean
135 194 */
136 - public function verifyBucket($access_key, $secret_key, $region, $bucket_name, $transfer_acceleration=false){
137 - if (
138 - isset($region) && !empty($region) &&
139 - isset($access_key) && !empty($access_key) &&
140 - isset($secret_key) && !empty($secret_key) &&
141 - isset($bucket_name) && !empty($bucket_name)
142 - ) {
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])) {
143 202 try {
144 203 $endpoint = $this->get_domain($region);
145 204
146 205 $DOClient = new S3Client([
@@ -147,9 +206,9 @@
147 206 'version' => '2006-03-01',
148 207 'region' => $region,
149 208 'endpoint' => $endpoint,
150 209 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
151 - 'use_accelerate_endpoint' => $transfer_acceleration, // Transfer acceleration for DigitalOcean Spaces
210 + 'use_accelerate_endpoint' => false,
152 211 'use_aws_shared_config_files' => false,
153 212 'credentials' => [
154 213 'key' => $access_key,
155 214 'secret' => $secret_key,
@@ -155,76 +214,27 @@
155 214 'secret' => $secret_key,
156 215 ],
157 216 ]);
158 217
159 - //Listing all Digital Ocean Buckets
160 - $buckets = $DOClient->listBuckets();
218 + //get S3 object
161 219 $bucket_found = false;
162 - $region_correct = false;
163 - if ($buckets) {
164 - foreach ($buckets['Buckets'] as $bucket) {
165 - if ($bucket['Name']==$bucket_name) {
166 - $bucket_found = true;
167 - }
168 - }
169 - } else {
170 - return array('message' => esc_html__('No matching bucket found', 'media-cloud-sync'), 'code' => 200, 'success' => false);
171 - }
172 - if ($bucket_found) {
173 - $upload_dir = wp_upload_dir();
174 - $file_dir = $upload_dir['basedir'] . '/' . Schema::getConstant('UPLOADS') . '/';
175 -
176 - if (!is_dir($file_dir)) {
177 - do_action( $this->token.'_create_plugin_dir' );
178 - }
179 -
180 - $fileName = $this->token."_verify.txt";
181 - $localFile = $file_dir.$this->token."-local-verify.txt";
182 -
183 - $verify_file = fopen($file_dir.$fileName, "w");
184 - $txt = "We are verifying input/output operations in Digital Ocean\n";
185 - fwrite($verify_file, $txt);
186 - fclose($verify_file);
187 -
188 - $upload = $DOClient->putObject([
220 + try {
221 + $DOClient->getObject([
189 222 'Bucket' => $bucket_name,
190 - 'Key' => $fileName,
191 - 'Body' => fopen($file_dir.$fileName, "r"),
192 - 'ACL' => 'public-read', // make file 'public'
223 + 'Key' => $this->token . '_dummy-object-for-bucket-exist-check'
193 224 ]);
194 -
195 - @unlink($file_dir.$fileName);
196 - if ($upload->get('ObjectURL')) {
197 - try {
198 - $getObject = $DOClient->GetObject([
199 - 'Bucket' => $bucket_name,
200 - 'Key' => $fileName,
201 - 'SaveAs' => $localFile
202 - ]);
203 -
204 - if (file_exists($localFile)) {
205 - @unlink($localFile);
206 - $DOClient->deleteObject([
207 - 'Bucket' => $bucket_name,
208 - 'Key' => $fileName,
209 - ]);
225 + $bucket_found = true;
226 + } catch (AwsException $e) {
227 + $code = $e->getAwsErrorCode();
228 + if ($code === 'NoSuchKey') {
229 + $bucket_found = true;
230 + }
231 + }
210 232
211 - if (!$DOClient->doesObjectExist($bucket_name, $fileName)) {
212 - return array('message' => esc_html__('Configuration for Digital Ocean has verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true);
213 - } else {
214 - 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);
215 - }
216 - } else {
217 - 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);
218 - }
219 - } catch (S3Exception $ex) {
220 - return array('message' => $ex->getAwsErrorMessage(), 'code' => $ex->getAwsErrorCode(), 'success' => false);
221 - }
222 - } else {
223 - 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);
224 - }
233 + if($bucket_found) {
234 + return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
225 235 } else {
226 - return array('message' => esc_html__('Bucket Name is incorrect', '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);
227 237 }
228 238 }
229 239 catch (S3Exception $ex) {
230 240 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
@@ -239,10 +249,15 @@
239 249 * Create Bucket
240 250 * @since 1.0.0
241 251 * @return boolean
242 252 */
243 - public function createBucket($access_key, $secret_key, $region, $bucket_name, $transfer_acceleration = false){
244 - 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])) {
245 260 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
246 261 }
247 262
248 263 try {
@@ -252,8 +267,9 @@
252 267 'version' => '2006-03-01',
253 268 'region' => $region,
254 269 'endpoint' => $endpoint,
255 270 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
271 + 'use_accelerate_endpoint' => false,
256 272 'use_aws_shared_config_files' => false,
257 273 'credentials' => [
258 274 'key' => $access_key,
259 275 'secret' => $secret_key,
@@ -260,29 +276,28 @@
260 276 ],
261 277 ]);
262 278
263 279 // Create Bucket
264 - $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 +
265 287 try {
266 - $this->enablBucketPublicAccess($bucket_name, $DOClient);
267 288 $this->putBucketPolicy($bucket_name, $DOClient);
268 - $this->changeBucketOwnership($bucket_name, $DOClient);
269 - try {
270 -
271 - $this->changeTransferAccilaration($bucket_name, $DOClient, $transfer_acceleration);
272 -
273 - return [
274 - 'message' => esc_html__('Bucket created successfully. Choose bucket from list to select the bucket.', 'media-cloud-sync'),
275 - 'data' => [
276 - 'Name' => $bucket_name,
277 - 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
278 - ],
279 - 'code' => 200,
280 - 'success' => true,
281 - ];
282 - } catch (AwsException $ex) {
283 - 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];
284 - }
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 +
285 300 } catch (AwsException $ex) {
286 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];
287 302 }
288 303 } catch (AwsException $ex) {
@@ -292,14 +307,21 @@
292 307 } catch (Exception $ex) {
293 308 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
294 309 }
295 310 }
311 +
312 +
296 313 /**
297 - * 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.
298 321 * @since 1.0.0
299 322 */
300 -
301 - private function enablBucketPublicAccess($bucket, $DOClient = false) {
323 + private function putBucketPolicy($bucket, $DOClient = false, $private_prefix = '') {
302 324 if($DOClient == false) {
303 325 $DOClient = $this->DOClient;
304 326 }
305 327
@@ -304,109 +326,271 @@
304 326 }
305 327
306 328 if(empty($bucket)) return false;
307 329
308 - $DOClient->putPublicAccessBlock([
309 - 'Bucket' => $bucket,
310 - 'PublicAccessBlockConfiguration' => [
311 - 'BlockPublicPolicy' => false,
312 - 'BlockPublicAcls' => false,
313 - 'IgnorePublicAcls' => false,
314 - 'RestrictPublicBuckets' => false,
315 - ]
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]
316 367 ]);
317 368
318 - 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 + }
319 381 }
320 382
321 383 /**
322 - * 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
323 387 */
324 - private function putBucketPolicy($bucket, $DOClient = false) {
325 - if($DOClient == false) {
326 - $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')];
327 391 }
328 392
329 - if(empty($bucket)) return false;
393 + $ok = $this->putBucketPolicy($this->bucket_name, $this->DOClient, $private_prefix);
330 394
331 - $policy = '{
332 - "Version": "2012-10-17",
333 - "Statement": [
334 - {
335 - "Effect": "Allow",
336 - "Principal": "*",
337 - "Action": [
338 - "s3:DeleteObjectTagging",
339 - "s3:ListBucketMultipartUploads",
340 - "s3:DeleteObjectVersion",
341 - "s3:ListBucket",
342 - "s3:DeleteObjectVersionTagging",
343 - "s3:GetBucketAcl",
344 - "s3:ListMultipartUploadParts",
345 - "s3:PutObject",
346 - "s3:GetObjectAcl",
347 - "s3:GetObject",
348 - "s3:AbortMultipartUpload",
349 - "s3:DeleteObject",
350 - "s3:GetBucketLocation",
351 - "s3:PutObjectAcl",
352 - "s3:putBucketOwnershipControls",
353 - "s3:putBucketPolicy"
354 - ],
355 - "Resource": [
356 - "arn:aws:s3:::' . $bucket . '/*",
357 - "arn:aws:s3:::' . $bucket . '"
358 - ]
359 - }
360 - ]
361 - }';
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 + }
362 399
363 - // Add bucket policy
364 - $DOClient->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
365 400
366 - 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 + }
367 455 }
368 456
457 +
458 +
369 459 /**
370 - * Add Bucket Ownership
371 - */
372 - private function changeBucketOwnership($bucket, $DOClient = false, $ownership = 'BucketOwnerPreferred') {
373 - if($DOClient == false) {
374 - $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];
375 471 }
376 472
377 - if(empty($bucket)) return false;
473 + try {
474 + $endpoint = $this->get_domain($region);
378 475
379 - // Change object ownership ACL enabled
380 - $DOClient->putBucketOwnershipControls([
381 - 'Bucket' => $bucket,
382 - 'OwnershipControls' => [
383 - 'Rules' => [['ObjectOwnership' => $ownership]],
384 - ],
385 - ]);
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();
386 490
387 - 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 + }
388 511 }
389 512
513 +
390 514 /**
391 - * Change transfer accilaration
515 + * Check Bucket Read Permission
516 + * @since 1.2.4
392 517 */
393 - private function changeTransferAccilaration($bucket, $DOClient = false, $enable=false, $force = false) {
394 - if($DOClient == false) {
395 - $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()];
396 527 }
397 528
398 - if(empty($bucket)) return false;
399 - if(!$force && !$enable) return true;
529 + try {
530 + $object_key = Utils::get_permission_check_object_key();
400 531
401 - $DOClient->putBucketAccelerateConfiguration([
402 - 'Bucket' => $bucket,
403 - 'AccelerateConfiguration' => [
404 - 'Status' => $enable ? 'Enabled' : 'Suspended'
405 - ]
406 - ]);
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 + }
407 543
408 - 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 + }
409 593 }
410 594
411 595 /**
412 596 * isConfigured Function To Identify the congfigurations are correct
@@ -414,19 +598,33 @@
414 598 */
415 599 public function isConfigured(){
416 600 if ($this->DOClient) {
417 601 try {
418 - $buckets = $DOClient->listBuckets();
419 - if(!empty($buckets)){
420 - foreach($buckets as $bucket) {
421 - if ($bucket['Name']==$this->bucket_name) {
422 - return true;
423 - }
424 - }
425 - }
426 - return false;
427 - } catch (AwsException $ex) {
428 - 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 + }
429 627 }
430 628 }
431 629 return false;
432 630 }
@@ -437,8 +635,9 @@
437 635 *
438 636 */
439 637 public function toPrivate($key) {
440 638 if(!$key) return false;
639 + if(!$this->DOClient) return false;
441 640 try {
442 641 $this->DOClient->putObjectAcl([
443 642 'Bucket' => $this->bucket_name,
444 643 'Key' => $key,
@@ -447,9 +646,8 @@
447 646 return true;
448 647 } catch (AwsException $ex) {
449 648 return false;
450 649 }
451 - return false;
452 650 }
453 651
454 652
455 653
@@ -455,23 +653,23 @@
455 653
456 654 /**
457 655 * Make Object Public
458 656 * @since 1.0.0
459 - *
657 + *
460 658 */
461 659 public function toPublic($key) {
462 660 if(!$key) return false;
661 + if(!$this->DOClient) return false;
463 662 try {
464 663 $this->DOClient->putObjectAcl([
465 664 'Bucket' => $this->bucket_name,
466 665 'Key' => $key,
467 666 'ACL' => 'public-read'
468 - ]);
667 + ]);
469 668 return true;
470 669 } catch (AwsException $ex) {
471 670 return false;
472 671 }
473 - return false;
474 672 }
475 673
476 674
477 675
@@ -478,114 +676,208 @@
478 676 /**
479 677 * Check the object exist
480 678 * @since 1.1.8
481 679 */
482 - public function exists($key) {
680 + public function exists($key, $bucket_name = '', $client = null) {
483 681 if(!$key) return false;
484 682
485 - if($this->DOClient->doesObjectExist($this->bucket_name, $key)) {
486 - 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;
487 692 }
488 -
489 - return false;
693 + catch (S3Exception $ex) {
694 + return false;
695 + } catch (Exception $ex) {
696 + return false;
697 + }
490 698 }
491 699
492 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 + /**
493 755 * Upload Single
494 756 * @since 1.0.0
495 757 * @return boolean
496 758 */
497 - public function uploadSingle($media_absolute_path, $media_path, $prefix='') {
498 - $result = array();
759 + public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false) {
499 760 if (
500 - isset($media_absolute_path) && !empty($media_absolute_path) &&
501 - isset($media_path) && !empty($media_path)
761 + isset($absolute_source_path) && !empty($absolute_source_path) &&
762 + isset($relative_source_path) && !empty($relative_source_path)
502 763 ) {
503 - $file_name = wp_basename( $media_path );
764 + $file_name = wp_basename( $relative_source_path );
504 765 if ($file_name) {
505 - $upload_path = Utils::generate_object_key($file_name, $prefix);
506 -
507 - // Decide Multipart upload or normal put object
508 - if (filesize($media_absolute_path) <= Schema::getConstant('DOCEAN_MULTIPART_MIN_FILE_SIZE')) {
509 - // Upload a publicly accessible file. The file size and type are determined by the SDK.
510 - try {
511 - $upload = $this->DOClient->putObject([
512 - 'Bucket' => $this->bucket_name,
513 - 'Key' => $upload_path,
514 - 'Body' => fopen($media_absolute_path, 'r'),
515 - 'ACL' => 'public-read', // make file 'public'
516 - ]);
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 + }
517 788
518 - $result = array(
519 - 'success' => true,
520 - 'code' => 200,
521 - 'file_url' => $upload->get('ObjectURL'),
522 - 'key' => $upload_path,
523 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
524 - );
525 - } catch (AwsException $e) {
526 - $result = array(
527 - 'success' => false,
528 - 'code' => 200,
529 - 'message' => $e->getMessage()
530 - );
531 - }
532 - } else {
533 - $multiUploader = new MultipartUploader($this->DOClient, $media_absolute_path, [
534 - 'bucket' => $this->bucket_name,
535 - 'key' => $upload_path,
536 - 'acl' => 'public-read', // make file 'public'
537 - ]);
538 -
539 - try {
540 - do {
541 - try {
542 - $uploaded = $multiUploader->upload();
543 - } catch (MultipartUploadException $e) {
544 - $multiUploader = new MultipartUploader($this->DOClient, $media_absolute_path, [
545 - 'state' => $e->getState(),
546 - ]);
547 - }
548 - } 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 + }
549 796
550 - if (isset($uploaded['ObjectURL']) && !empty($uploaded['ObjectURL'])) {
551 - $result = array(
552 - 'success' => true,
553 - 'code' => 200,
554 - 'file_url' => urldecode($uploaded['ObjectURL']),
555 - 'key' => $upload_path,
556 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
557 - );
558 - } else {
559 - $result = array(
560 - 'success' => false,
561 - 'code' => 200,
562 - 'message' => esc_html__('Something happened while uploading to server', 'media-cloud-sync')
563 - );
564 - }
565 - } catch (MultipartUploadException $e) {
566 - $result = array(
567 - 'success' => false,
568 - 'code' => 200,
569 - 'message' => $e->getMessage()
570 - );
571 - }
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 + ];
572 864 }
573 - } else {
574 - $result = array(
865 + $options = ['state' => $e->getState()];
866 + } catch (AwsException $e) {
867 + return [
575 868 'success' => false,
576 869 'code' => 200,
577 - 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
578 - );
870 + 'message' => $e->getMessage()
871 + ];
872 + } catch (Exception $e) {
873 + return [
874 + 'success' => false,
875 + 'code' => 200,
876 + 'message' => $e->getMessage()
877 + ];
579 878 }
580 - } else {
581 - $result = array(
582 - 'success' => false,
583 - 'code' => 200,
584 - 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
585 - );
586 879 }
587 - return $result;
588 880 }
589 881
590 882 /**
591 883 * Save object to server
@@ -591,8 +883,9 @@
591 883 * Save object to server
592 884 * @since 1.0.0
593 885 */
594 886 public function object_to_server($key, $save_path) {
887 + if(!$this->DOClient) return false;
595 888 try {
596 889 $getObject = $this->DOClient->GetObject([
597 890 'Bucket' => $this->bucket_name,
598 891 'Key' => $key,
@@ -606,10 +899,155 @@
606 899 }
607 900 return false;
608 901 }
609 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 + }
610 920
611 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 + /**
612 1050 * Delete Single
613 1051 * @since 1.0.0
614 1052 * @return boolean
615 1053 */
@@ -614,8 +1052,15 @@
614 1052 * @return boolean
615 1053 */
616 1054 public function deleteSingle($key) {
617 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 + }
618 1063 if (isset($key) && !empty($key)) {
619 1064 try {
620 1065 $this->DOClient->deleteObject([
621 1066 'Bucket' => $this->bucket_name,
@@ -621,9 +1066,9 @@
621 1066 'Bucket' => $this->bucket_name,
622 1067 'Key' => $key
623 1068 ]);
624 1069
625 - if (!$this->DOClient->doesObjectExist($this->bucket_name, $key)) {
1070 + if (!$this->exists($key)) {
626 1071 $result = array(
627 1072 'success' => true,
628 1073 'code' => 200,
629 1074 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync')
@@ -652,14 +1097,21 @@
652 1097 return $result;
653 1098 }
654 1099
655 1100 /**
656 - * get presigned URL
1101 + * get private URL
657 1102 * @since 1.0.0
658 1103 * @return boolean
659 1104 */
660 - public function get_presigned_url($key) {
1105 + public function get_private_url($key) {
661 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 + }
662 1114 if (isset($key) && !empty($key)) {
663 1115 try {
664 1116 $cmd = $this->DOClient->getCommand('GetObject', [
665 1117 'Bucket' => $this->bucket_name,
@@ -665,24 +1117,24 @@
665 1117 'Bucket' => $this->bucket_name,
666 1118 'Key' => $key
667 1119 ]);
668 1120
669 - $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;
670 1122
671 1123 $request = $this->DOClient->createPresignedRequest($cmd, sprintf('+%s minutes', $expires));
672 1124
673 - if ($presignedUrl = (string)$request->getUri()) {
1125 + if ($privateUrl = (string)$request->getUri()) {
674 1126 $result = array(
675 1127 'success' => true,
676 1128 'code' => 200,
677 - 'file_url' => $presignedUrl,
678 - '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')
679 1131 );
680 1132 } else {
681 1133 $result = array(
682 1134 'success' => false,
683 1135 'code' => 200,
684 - 'message' => esc_html__('Error getting presigned URL', 'media-cloud-sync')
1136 + 'message' => esc_html__('Error getting Private URL', 'media-cloud-sync')
685 1137 );
686 1138 }
687 1139 } catch (AwsException $e) {
688 1140 $result = array(
@@ -698,8 +1150,31 @@
698 1150 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
699 1151 );
700 1152 }
701 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);
702 1177 }
703 1178
704 1179 /**
705 1180 * Get domain URL