PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
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 1.3.0 All 34 releases
media-cloud-sync / includes / base / services / docean.php

docean.php in Media Cloud Sync trunk, at includes/base/services/docean.php

1,151 lines 46.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 // Libraries
7 use Dudlewebs\WPMCS\s3\Aws\S3\S3Client;
8 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
9 use Dudlewebs\WPMCS\s3\Aws\S3\Exception\S3Exception;
10 use Dudlewebs\WPMCS\s3\Aws\S3\MultipartUploader;
11 use Dudlewebs\WPMCS\s3\Aws\Exception\MultipartUploadException;
12 use Dudlewebs\WPMCS\s3\Aws\S3\ObjectUploader;
13 use Dudlewebs\WPMCS\s3\Aws\Command;
14 use Exception;
15
16 class DOcean {
17 private $assets_url;
18 private $version;
19 private $token;
20
21 protected $config;
22 protected $bucketConfig;
23 protected $settings;
24 protected $credentials;
25 protected $bucket_name;
26 protected $cdnConfig;
27
28 public $service = 'docean';
29 public $DOClient = false;
30
31 /**
32 * Admin constructor.
33 * @since 1.0.0
34 */
35 public function __construct($credentials = null) {
36 $this->assets_url = WPMCS_ASSETS_URL;
37 $this->version = WPMCS_VERSION;
38 $this->token = WPMCS_TOKEN;
39
40 // Initialize setup
41 $this->init($credentials);
42 }
43
44 /**
45 * Initialise Client
46 *
47 * @param array|null $credentials Optional explicit credentials; falls back to
48 * Utils::get_credentials() when omitted.
49 */
50 public function init($credentials = null) {
51 $this->settings = Utils::get_settings();
52 $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
53 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
54 ? $this->credentials['config']
55 : [];
56 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
57 ? $this->credentials['bucketConfig']
58 : [];
59 $this->bucket_name = isset($this->bucketConfig['bucket_name']) && !empty($this->bucketConfig['bucket_name'])
60 ? $this->bucketConfig['bucket_name']
61 : '';
62 $this->cdnConfig = isset($this->credentials['cdn']) && !empty($this->credentials['cdn'])
63 ? $this->credentials['cdn']
64 : [];
65
66 if (
67 isset($this->config['region']) && !empty($this->config['region']) &&
68 isset($this->config['access_key']) && !empty($this->config['access_key']) &&
69 isset($this->config['secret_key']) && !empty($this->config['secret_key'])
70 ) {
71 $endpoint = $this->get_domain();
72
73 $this->DOClient = new S3Client([
74 'version' => '2006-03-01',
75 'region' => $this->config['region'],
76 'endpoint' => $endpoint, // DigitalOcean Spaces requires a custom endpoint
77 'use_accelerate_endpoint' => false,
78 'use_path_style_endpoint' => true, // DigitalOcean Spaces often requires path-style endpoints
79 'use_aws_shared_config_files' => false,
80 'credentials' => [
81 'key' => $this->config['access_key'],
82 'secret' => $this->config['secret_key'],
83 ],
84 ]);
85 }
86
87 }
88
89
90 /**
91 * Verify Credentials
92 * @since 1.0.0
93 * @return boolean
94 */
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])) {
100 try {
101 $endpoint = $this->get_domain($region);
102
103 $DOClient = new S3Client([
104 'version' => '2006-03-01',
105 'region' => $region,
106 'endpoint' => $endpoint,
107 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
108 'use_accelerate_endpoint' => false,
109 'use_aws_shared_config_files' => false,
110 'credentials' => [
111 'key' => $access_key,
112 'secret' => $secret_key,
113 ],
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 ]);
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 }
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) {
182 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
183 } catch (Exception $ex) {
184 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
185 }
186 }
187 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
188 }
189
190 /**
191 * Verify Bucket
192 * @since 1.0.0
193 * @return boolean
194 */
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])) {
202 try {
203 $endpoint = $this->get_domain($region);
204
205 $DOClient = new S3Client([
206 'version' => '2006-03-01',
207 'region' => $region,
208 'endpoint' => $endpoint,
209 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
210 'use_accelerate_endpoint' => false,
211 'use_aws_shared_config_files' => false,
212 'credentials' => [
213 'key' => $access_key,
214 'secret' => $secret_key,
215 ],
216 ]);
217
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 }
232
233 if($bucket_found) {
234 return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
235 } else {
236 return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
237 }
238 }
239 catch (S3Exception $ex) {
240 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
241 } catch (Exception $ex) {
242 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
243 }
244 }
245 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
246 }
247
248 /**
249 * Create Bucket
250 * @since 1.0.0
251 * @return boolean
252 */
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])) {
260 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
261 }
262
263 try {
264 $endpoint = $this->get_domain($region);
265
266 $DOClient = new S3Client([
267 'version' => '2006-03-01',
268 'region' => $region,
269 'endpoint' => $endpoint,
270 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
271 'use_accelerate_endpoint' => false,
272 'use_aws_shared_config_files' => false,
273 'credentials' => [
274 'key' => $access_key,
275 'secret' => $secret_key,
276 ],
277 ]);
278
279 // Create Bucket
280 $DOClient->createBucket([
281 'Bucket' => $bucket_name,
282 ]);
283
284 // Optionally wait for bucket existence (recommended)
285 $DOClient->waitUntil('BucketExists', ['Bucket' => $bucket_name]);
286
287 try {
288 $this->putBucketPolicy($bucket_name, $DOClient);
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
300 } catch (AwsException $ex) {
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];
302 }
303 } catch (AwsException $ex) {
304 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
305 } catch (S3Exception $ex) {
306 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
307 } catch (Exception $ex) {
308 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
309 }
310 }
311
312
313 /**
314 * Add Bucket Policy
315 */
316 private function putBucketPolicy($bucket, $DOClient = false) {
317 if($DOClient == false) {
318 $DOClient = $this->DOClient;
319 }
320
321 if(empty($bucket)) return false;
322
323 $policy = json_encode([
324 "Version" => "2012-10-17",
325 "Statement" => [
326 [
327 "Effect" => "Allow",
328 "Principal" => "*",
329 "Action" => [
330 "s3:DeleteObjectTagging",
331 "s3:ListBucketMultipartUploads",
332 "s3:DeleteObjectVersion",
333 "s3:ListBucket",
334 "s3:DeleteObjectVersionTagging",
335 "s3:GetBucketAcl",
336 "s3:ListMultipartUploadParts",
337 "s3:PutObject",
338 "s3:GetObjectAcl",
339 "s3:GetObject",
340 "s3:AbortMultipartUpload",
341 "s3:DeleteObject",
342 "s3:GetBucketLocation",
343 "s3:PutObjectAcl",
344 "s3:putBucketOwnershipControls",
345 "s3:putBucketPolicy"
346 ],
347 "Resource" => [
348 "arn:aws:s3:::$bucket/*",
349 "arn:aws:s3:::$bucket"
350 ]
351 ]
352 ]
353 ]);
354
355 try {
356 // Add bucket policy
357 $DOClient->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
358
359 return true;
360 } catch (AwsException $ex) {
361 return false; // Handle AWS specific exceptions
362 } catch (S3Exception $ex) {
363 return false; // Handle S3 specific exceptions
364 } catch (Exception $ex) {
365 return false; // Handle general exceptions
366 }
367 }
368
369
370
371 /**
372 * Check Bucket Write Permission
373 * @since 1.0.0
374 */
375 public function verifyObjectWritePermission( $config = [], $bucketConfig = [] ) {
376 $region = isset($config['region']) ? $config['region'] : '';
377 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
378 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
379 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
380
381 if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
382 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
383 }
384
385 try {
386 $endpoint = $this->get_domain($region);
387
388 $DOClient = new S3Client([
389 'version' => '2006-03-01',
390 'region' => $region,
391 'endpoint' => $endpoint,
392 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
393 'use_accelerate_endpoint' => false,
394 'use_aws_shared_config_files' => false,
395 'credentials' => [
396 'key' => $access_key,
397 'secret' => $secret_key,
398 ],
399 ]);
400
401 $object_key = Utils::get_permission_check_object_key();
402
403
404 // Create a dummy object to check write permission
405 $DOClient->putObject([
406 'Bucket' => $bucket_name,
407 'Key' => $object_key,
408 'Body' => 'This is a test object to check write permission.',
409 ]);
410 // Check if the object was created successfully
411 if ($this->exists($object_key, $bucket_name, $DOClient)) {
412 return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
413 } else {
414 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
415 }
416
417 } catch (AwsException $ex) {
418 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
419 } catch (S3Exception $ex) {
420 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
421 } catch (Exception $ex) {
422 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
423 }
424 }
425
426
427
428 /**
429 * Check Bucket Delete Permission
430 * @since 1.0.0
431 */
432 public function verifyObjectDeletePermission( $config = [], $bucketConfig = [] ) {
433 $region = isset($config['region']) ? $config['region'] : '';
434 $access_key = isset($config['access_key']) ? $config['access_key'] : '';
435 $secret_key = isset($config['secret_key']) ? $config['secret_key'] : '';
436 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
437
438 if (Service::has_missing_fields([$region, $access_key, $secret_key, $bucket_name])) {
439 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
440 }
441
442 try {
443 $endpoint = $this->get_domain($region);
444
445 $DOClient = new S3Client([
446 'version' => '2006-03-01',
447 'region' => $region,
448 'endpoint' => $endpoint,
449 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
450 'use_accelerate_endpoint' => false,
451 'use_aws_shared_config_files' => false,
452 'credentials' => [
453 'key' => $access_key,
454 'secret' => $secret_key,
455 ],
456 ]);
457
458 $object_key = Utils::get_permission_check_object_key();
459
460 // Create a dummy object to check dlete permission
461 $DOClient->deleteObject([
462 'Bucket' => $bucket_name,
463 'Key' => $object_key,
464 ]);
465
466 // Check if the object was created successfully
467 if (!$this->exists($object_key, $bucket_name, $DOClient)) {
468 return ['message' => esc_html__('Bucket delete permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
469 } else {
470 return ['message' => esc_html__('Bucket delete permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
471 }
472
473 } catch (AwsException $ex) {
474 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
475 } catch (S3Exception $ex) {
476 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
477 } catch (Exception $ex) {
478 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
479 }
480 }
481
482
483 /**
484 * Check Bucket Read Permission
485 * @since 1.2.4
486 */
487 public function verifyObjectReadPermission() {
488 $result = [
489 'status' => false,
490 'message' => '',
491 'lastChecked' => time(),
492 ];
493 if (Service::has_missing_fields([$this->DOClient, $this->bucket_name])) {
494 $result['message'] = esc_html__('Invalid Request', 'media-cloud-sync');
495 return ['message' => esc_html__('Invalid Request', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
496 }
497
498 try {
499 $object_key = Utils::get_permission_check_object_key();
500
501 // Check if the object was created successfully
502 if (!$this->exists($object_key)) {
503 // Create a dummy object to check write permission
504 $this->DOClient->putObject([
505 'Bucket' => $this->bucket_name,
506 'Key' => $object_key,
507 'Body' => 'This is a test object to check permission.',
508 'ContentType' => 'text/plain',
509 'CacheControl' => 'no-cache, no-store, must-revalidate',
510 ]);
511 }
512
513
514 $url = $this->generate_file_url($object_key);
515 $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
516 // Never trust a cached response for this fixed, predictable URL — a stale cached
517 // error would otherwise keep failing the check long after real access is fine.
518 $no_cache_context = stream_context_create(['http' => ['header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"]]);
519 $headers = @get_headers($cdn_url, false, $no_cache_context);
520 $status_code = (is_array($headers) && !empty($headers[0]) && preg_match('/\s(\d{3})\s/', $headers[0], $matches))
521 ? (int) $matches[1]
522 : 0;
523
524 if ($status_code === 200) {
525 $result['status'] = true;
526 $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
527 } else if ($status_code === 403) {
528 $result['status'] = false;
529 if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
530 $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
531 } else {
532 $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
533 }
534 } else if ($status_code === 404) {
535 $result['status'] = false;
536 $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
537 } else if ($status_code === 500) {
538 $result['status'] = false;
539 $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
540 } else {
541 $result['status'] = false;
542 $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
543 }
544
545 $this->deleteSingle($object_key);
546 return [
547 'message' => $result['message'],
548 'code' => 200,
549 'success' => $result['status'],
550 'lastChecked' => $result['lastChecked'],
551 ];
552 } catch (AwsException $ex) {
553 $result['message'] = $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
554 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
555 } catch (S3Exception $ex) {
556 $result['message'] = $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
557 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
558 } catch (Exception $ex) {
559 $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
560 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
561 }
562 }
563
564 /**
565 * isConfigured Function To Identify the congfigurations are correct
566 * @since 1.0.0
567 */
568 public function isConfigured(){
569 if ($this->DOClient) {
570 try {
571 $this->DOClient->listObjectsV2([
572 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
573 ]);
574
575 // If we reach here, the credentials are valid
576 return true;
577 } catch (AwsException $e) {
578 $code = $e->getAwsErrorCode();
579
580 $validErrors = [
581 'AccessDenied',
582 'NoSuchBucket',
583 'AllAccessDisabled',
584 'AuthorizationHeaderMalformed',
585 'PermanentRedirect',
586 'InvalidBucketName',
587 ];
588
589 if (in_array($code, $validErrors)) {
590 // If we reach here, the credentials are valid
591 return true;
592 } else {
593 // If we reach here, the credentials are not valid
594 return false;
595 }
596 }
597 }
598 return false;
599 }
600
601 /**
602 * Make Object Private
603 * @since 1.0.0
604 *
605 */
606 public function toPrivate($key) {
607 if(!$key) return false;
608 if(!$this->DOClient) return false;
609 try {
610 $this->DOClient->putObjectAcl([
611 'Bucket' => $this->bucket_name,
612 'Key' => $key,
613 'ACL' => 'private'
614 ]);
615 return true;
616 } catch (AwsException $ex) {
617 return false;
618 }
619 }
620
621
622
623 /**
624 * Make Object Public
625 * @since 1.0.0
626 *
627 */
628 public function toPublic($key) {
629 if(!$key) return false;
630 if(!$this->DOClient) return false;
631 try {
632 $this->DOClient->putObjectAcl([
633 'Bucket' => $this->bucket_name,
634 'Key' => $key,
635 'ACL' => 'public-read'
636 ]);
637 return true;
638 } catch (AwsException $ex) {
639 return false;
640 }
641 }
642
643
644
645 /**
646 * Check the object exist
647 * @since 1.1.8
648 */
649 public function exists($key, $bucket_name = '', $client = null) {
650 if(!$key) return false;
651
652 try {
653 $client = $client ?? $this->DOClient;
654 $bucket_name = !empty($bucket_name) ? $bucket_name : $this->bucket_name;
655 if($client->doesObjectExistV2($bucket_name, $key)) {
656 return true;
657 }
658 return false;
659 } catch (AwsException $ex) {
660 return false;
661 }
662 catch (S3Exception $ex) {
663 return false;
664 } catch (Exception $ex) {
665 return false;
666 }
667 }
668
669 /**
670 * List Objects — $delimiter = null gives a flat/recursive listing instead of one folder level.
671 * @since 1.3.13
672 */
673 public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
674 if (!$this->DOClient) {
675 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
676 }
677 try {
678 $params = ['Bucket' => $this->bucket_name, 'MaxKeys' => $maxKeys];
679 if (!empty($delimiter)) {
680 $params['Delimiter'] = $delimiter;
681 }
682 if (!empty($prefix)) {
683 $params['Prefix'] = $prefix;
684 }
685 if (!empty($continuationToken)) {
686 $params['ContinuationToken'] = $continuationToken;
687 }
688
689 $result = $this->DOClient->listObjectsV2($params);
690 $folders = [];
691 foreach (($result['CommonPrefixes'] ?? []) as $common) {
692 $folders[] = $common['Prefix'];
693 }
694 $objects = [];
695 foreach (($result['Contents'] ?? []) as $object) {
696 if ($object['Key'] === $prefix) {
697 continue; // the folder placeholder object itself, not a file
698 }
699 $objects[] = [
700 'key' => $object['Key'],
701 'size' => (int) $object['Size'],
702 'last_modified' => $object['LastModified'] ? $object['LastModified']->format(DATE_ATOM) : '',
703 ];
704 }
705
706 return [
707 'success' => true,
708 'code' => 200,
709 'message' => '',
710 'folders' => $folders,
711 'objects' => $objects,
712 'next_token' => !empty($result['IsTruncated']) ? ($result['NextContinuationToken'] ?? null) : null,
713 ];
714 } catch (AwsException $e) {
715 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
716 } catch (S3Exception $e) {
717 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
718 } catch (Exception $e) {
719 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
720 }
721 }
722
723 /**
724 * Upload Single
725 * @since 1.0.0
726 * @return boolean
727 */
728 public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='') {
729 if (
730 isset($absolute_source_path) && !empty($absolute_source_path) &&
731 isset($relative_source_path) && !empty($relative_source_path)
732 ) {
733 $file_name = wp_basename( $relative_source_path );
734 if ($file_name) {
735 $upload_path = Utils::generate_object_key($relative_source_path, $prefix);
736 return $this->execute_upload($absolute_source_path, $upload_path);
737 }
738 return [
739 'success' => false,
740 'code' => 200,
741 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
742 ];
743 }
744 return [
745 'success' => false,
746 'code' => 200,
747 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
748 ];
749 }
750
751 /**
752 * Upload a local file to an exact destination key (no Utils::generate_object_key() derivation).
753 * @since 1.4.0
754 */
755 public function uploadObjectAtKey($absolute_source_path, $key) {
756 return $this->execute_upload($absolute_source_path, $key);
757 }
758
759 /**
760 * Build an unexecuted ObjectUploader (single PUT or multipart, decided internally by the
761 * SDK, using this plugin's own multipart threshold rather than the SDK's 16MB default).
762 * ACL is stripped via before_* hooks — this plugin's model is bucket-level, not per-object,
763 * and an explicit `ACL: null` still serializes to an empty x-amz-acl header otherwise.
764 * $options is threaded straight into the SDK (e.g. 'state' => UploadState to resume a
765 * previously-failed multipart attempt).
766 * @since 1.4.0
767 */
768 private function build_object_uploader($absolute_source_path, $key, $options = []) {
769 $handle = fopen($absolute_source_path, 'rb');
770 $params = [];
771 $cache_control = Utils::get_cache_control_header();
772 if ($cache_control) {
773 $params['CacheControl'] = $cache_control;
774 }
775 $options += [
776 'mup_threshold' => Schema::getConstant('DOCEAN_MULTIPART_MIN_FILE_SIZE'),
777 'params' => $params,
778 'before_initiate' => function ($params) { return $this->strip_acl($params); },
779 'before_upload' => function ($params) { return $this->strip_acl($params); },
780 'before_complete' => function ($params) { return $this->strip_acl($params); },
781 ];
782 return new ObjectUploader($this->DOClient, $this->bucket_name, $key, $handle, null, $options);
783 }
784
785 // Mutate in place, not a clone — the SDK's before_* hooks call this and discard the
786 // return value, relying on the same Command object being modified.
787 private function strip_acl($params) {
788 if ($params instanceof Command && $params->hasParam('ACL')) {
789 unset($params['ACL']);
790 } elseif (is_array($params) && isset($params['ACL'])) {
791 unset($params['ACL']);
792 }
793 return $params;
794 }
795
796 /**
797 * Run an ObjectUploader synchronously and normalize the result shape. Retries up to
798 * 3 attempts on MultipartUploadException, resuming from the failed attempt's saved
799 * state rather than restarting the whole upload — same retry contract uploadSingle()
800 * had before the ObjectUploader swap.
801 * @since 1.4.0
802 */
803 private function execute_upload($absolute_source_path, $key) {
804 $max_attempts = 3;
805 $attempt = 0;
806 $options = [];
807
808 while (true) {
809 $attempt++;
810 try {
811 $this->build_object_uploader($absolute_source_path, $key, $options)->upload();
812 return [
813 'success' => true,
814 'code' => 200,
815 'file_url' => $this->generate_file_url($key),
816 'key' => $key,
817 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
818 ];
819 } catch (MultipartUploadException $e) {
820 if ($attempt >= $max_attempts) {
821 return [
822 'success' => false,
823 'code' => 200,
824 'message' => $e->getMessage()
825 ];
826 }
827 $options = ['state' => $e->getState()];
828 } catch (AwsException $e) {
829 return [
830 'success' => false,
831 'code' => 200,
832 'message' => $e->getMessage()
833 ];
834 } catch (Exception $e) {
835 return [
836 'success' => false,
837 'code' => 200,
838 'message' => $e->getMessage()
839 ];
840 }
841 }
842 }
843
844 /**
845 * Save object to server
846 * @since 1.0.0
847 */
848 public function object_to_server($key, $save_path) {
849 if(!$this->DOClient) return false;
850 try {
851 $getObject = $this->DOClient->GetObject([
852 'Bucket' => $this->bucket_name,
853 'Key' => $key,
854 'SaveAs' => $save_path
855 ]);
856 if (file_exists($save_path)) {
857 return true;
858 }
859 } catch (AwsException $e) {
860 return false;
861 }
862 return false;
863 }
864
865 /**
866 * Object bytes in memory, no local file — for callers (e.g. zip download) that need
867 * the content itself rather than a copy on the server's filesystem.
868 * @since 1.3.13
869 */
870 public function get_object_content($key) {
871 if(!$this->DOClient) return false;
872 try {
873 $result = $this->DOClient->GetObject([
874 'Bucket' => $this->bucket_name,
875 'Key' => $key,
876 ]);
877 return (string) $result['Body'];
878 } catch (AwsException $e) {
879 return false;
880 }
881 }
882
883 /**
884 * Deletes the live object, then best-effort purges every historical version too — a
885 * plain deleteSingle() on a versioned bucket only adds a delete marker, leaving prior
886 * versions (and the storage they use) behind at the old key. The live delete happens
887 * unconditionally first: DigitalOcean Spaces doesn't support object versioning at all,
888 * so the version-listing part below simply fails there (caught, non-fatal) — the object
889 * must still end up gone either way, which is why it can't be the only delete call.
890 * @since 1.3.14
891 */
892 public function purge_all_versions($key) {
893 if (!$this->DOClient) {
894 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
895 }
896
897 try {
898 $this->DOClient->deleteObject([
899 'Bucket' => $this->bucket_name,
900 'Key' => $key,
901 ]);
902 } catch (AwsException $e) {
903 return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
904 }
905
906 // Best-effort only from here — Spaces doesn't support version listing at all, so
907 // this always no-ops there; the live object above is already gone regardless.
908 try {
909 $objects = [];
910 $marker = null;
911 do {
912 $args = ['Bucket' => $this->bucket_name, 'Prefix' => $key];
913 if ($marker) {
914 $args['KeyMarker'] = $marker['key'];
915 $args['VersionIdMarker'] = $marker['version'];
916 }
917 $result = $this->DOClient->listObjectVersions($args);
918 foreach (array_merge($result['Versions'] ?? [], $result['DeleteMarkers'] ?? []) as $version) {
919 if (($version['Key'] ?? null) === $key) {
920 $objects[] = ['Key' => $key, 'VersionId' => $version['VersionId']];
921 }
922 }
923 $marker = !empty($result['IsTruncated'])
924 ? ['key' => $result['NextKeyMarker'], 'version' => $result['NextVersionIdMarker']]
925 : null;
926 } while ($marker);
927
928 foreach (array_chunk($objects, 1000) as $chunk) {
929 $this->DOClient->deleteObjects([
930 'Bucket' => $this->bucket_name,
931 'Delete' => ['Objects' => $chunk],
932 ]);
933 }
934 } catch (AwsException $e) {
935 // Version history cleanup unsupported/failed — not fatal, live object is gone.
936 }
937
938 return ['success' => true, 'code' => 200, 'message' => esc_html__('Purged Successfully', 'media-cloud-sync')];
939 }
940
941 /**
942 * Copy to new path
943 * @since 1.3.4
944 */
945 // Trusts copyObject()'s own success/failure rather than pre/post-verifying with extra
946 // exists() HEAD requests — each one is a full network round-trip, and with move/copy
947 // processing keys sequentially, three extra round-trips per file adds up fast on a
948 // folder with many files. copyObject() itself throws (caught below) if the source is
949 // missing or the copy otherwise fails, so nothing is lost by not checking first.
950 public function copy_to_new_path($key, $new_path) {
951 if (!$this->DOClient) {
952 return [
953 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
954 'code' => 200,
955 'success' => false
956 ];
957 }
958 try {
959 $this->DOClient->copyObject([
960 'Bucket' => $this->bucket_name,
961 'CopySource' => "{$this->bucket_name}/{$key}",
962 'Key' => $new_path,
963 'MetadataDirective' => 'COPY',
964 ]);
965 return [
966 'success' => true,
967 'code' => 200,
968 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
969 ];
970 } catch (AwsException $e) {
971 return [
972 'success' => false,
973 'code' => 200,
974 'message' => $e->getMessage()
975 ];
976 }
977 }
978
979 // Like copy_to_new_path() but into an explicit (possibly different) bucket — needs write
980 // access there too, so callers should fall back to download+upload on failure.
981 public function copy_to_bucket($key, $new_key, $dest_bucket) {
982 if (!$this->DOClient) {
983 return [
984 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
985 'code' => 200,
986 'success' => false
987 ];
988 }
989 try {
990 $this->DOClient->copyObject([
991 'Bucket' => $dest_bucket,
992 'CopySource' => "{$this->bucket_name}/{$key}",
993 'Key' => $new_key,
994 'MetadataDirective' => 'COPY',
995 ]);
996 return [
997 'success' => true,
998 'code' => 200,
999 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1000 ];
1001 } catch (AwsException $e) {
1002 return [
1003 'success' => false,
1004 'code' => 200,
1005 'message' => $e->getMessage()
1006 ];
1007 }
1008 }
1009
1010
1011 /**
1012 * Delete Single
1013 * @since 1.0.0
1014 * @return boolean
1015 */
1016 public function deleteSingle($key) {
1017 $result = array();
1018 if (!$this->DOClient) {
1019 return array(
1020 'success' => false,
1021 'code' => 200,
1022 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1023 );
1024 }
1025 if (isset($key) && !empty($key)) {
1026 try {
1027 $this->DOClient->deleteObject([
1028 'Bucket' => $this->bucket_name,
1029 'Key' => $key
1030 ]);
1031
1032 if (!$this->exists($key)) {
1033 $result = array(
1034 'success' => true,
1035 'code' => 200,
1036 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync')
1037 );
1038 } else {
1039 $result = array(
1040 'success' => false,
1041 'code' => 200,
1042 'message' => esc_html__('File not deleted', 'media-cloud-sync')
1043 );
1044 }
1045 } catch (AwsException $e) {
1046 $result = array(
1047 'success' => false,
1048 'code' => 200,
1049 'message' => $e->getMessage()
1050 );
1051 }
1052 } else {
1053 $result = array(
1054 'success' => false,
1055 'code' => 200,
1056 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
1057 );
1058 }
1059 return $result;
1060 }
1061
1062 /**
1063 * get private URL
1064 * @since 1.0.0
1065 * @return boolean
1066 */
1067 public function get_private_url($key) {
1068 $result = array();
1069 if (!$this->DOClient) {
1070 return array(
1071 'success' => false,
1072 'code' => 200,
1073 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1074 );
1075 }
1076 if (isset($key) && !empty($key)) {
1077 try {
1078 $cmd = $this->DOClient->getCommand('GetObject', [
1079 'Bucket' => $this->bucket_name,
1080 'Key' => $key
1081 ]);
1082
1083 $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
1084
1085 $request = $this->DOClient->createPresignedRequest($cmd, sprintf('+%s minutes', $expires));
1086
1087 if ($privateUrl = (string)$request->getUri()) {
1088 $result = array(
1089 'success' => true,
1090 'code' => 200,
1091 'file_url' => $privateUrl,
1092 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync')
1093 );
1094 } else {
1095 $result = array(
1096 'success' => false,
1097 'code' => 200,
1098 'message' => esc_html__('Error getting Private URL', 'media-cloud-sync')
1099 );
1100 }
1101 } catch (AwsException $e) {
1102 $result = array(
1103 'success' => false,
1104 'code' => 200,
1105 'message' => $e->getMessage()
1106 );
1107 }
1108 } else {
1109 $result = array(
1110 'success' => false,
1111 'code' => 200,
1112 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
1113 );
1114 }
1115 return $result;
1116 }
1117
1118 /**
1119 * Generate file URL
1120 */
1121 public function generate_file_url($key){
1122 $domain = $this->get_domain();
1123
1124 return apply_filters('wpmcs_generate_do_file_url',
1125 $domain . '/' . $this->bucket_name . '/' . $key,
1126 $domain,
1127 $this->bucket_name,
1128 $key
1129 );
1130 }
1131
1132 /**
1133 * Is Provider URL
1134 * @since 1.3.6
1135 */
1136 public function is_provider_url($url) {
1137 $domain = $this->get_domain();
1138 return (strpos($url, $domain . '/' . $this->bucket_name . '/') !== false);
1139 }
1140
1141 /**
1142 * Get domain URL
1143 */
1144 public function get_domain($region = '') {
1145 if(empty($region)) {
1146 $region = isset($this->config['region']) ? $this->config['region'] : '';
1147 }
1148 return "https://{$region}.digitaloceanspaces.com";
1149 }
1150
1151 }