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/gcloud.php +665 -154 1.2.121.4.1 View file →
@@ -3,10 +3,11 @@
3 3
4 4 defined('ABSPATH') || exit;
5 5
6 6 // Libraries
7 -use Dudlewebs\WPMCS\Google\Cloud\Storage\StorageClient;
8 -use Dudlewebs\WPMCS\Google\Cloud\Core\Exception\ServiceException;
7 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Storage\StorageClient;
8 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\Exception\ServiceException;
9 +use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialsLoader;
9 10
10 11 use Exception;
11 12
12 13 class GCloud {
@@ -28,23 +29,26 @@
28 29 /**
29 30 * Admin constructor.
30 31 * @since 1.0.0
31 32 */
32 - public function __construct() {
33 + public function __construct($credentials = null) {
33 34 $this->assets_url = WPMCS_ASSETS_URL;
34 35 $this->version = WPMCS_VERSION;
35 36 $this->token = WPMCS_TOKEN;
36 37
37 38 // Initialize setup
38 - $this->init();
39 + $this->init($credentials);
39 40 }
40 41
41 42 /**
42 43 * Initialise Client
44 + *
45 + * @param array|null $credentials Optional explicit credentials; falls back to
46 + * Utils::get_credentials() when omitted.
43 47 */
44 - public function init() {
48 + public function init($credentials = null) {
45 49 $this->settings = Utils::get_settings();
46 - $this->credentials = Utils::get_credentials();
50 + $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
47 51 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
48 52 ? $this->credentials['config']
49 53 : [];
50 54 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
@@ -93,15 +97,16 @@
93 97 * Verify Credentials
94 98 * @since 1.0.0
95 99 * @return boolean
96 100 */
97 - public function verifyCredentials( $config_json ){
98 - if (isset($config_json) && !empty($config_json)) {
101 + public function verifyCredentials( $config = [] ){
102 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
103 + if (!Service::has_missing_fields([$config_json])) {
99 104 if(!Utils::is_json($config_json)){
100 105 return [
101 106 'success' => false,
102 107 'code' => 200,
103 - 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
108 + 'message' => esc_html__('Invalid JSON configuration, please try again', 'media-cloud-sync'),
104 109 ];
105 110 }
106 111
107 112 try {
@@ -117,9 +122,13 @@
117 122 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
118 123 ];
119 124 }
120 125
121 -
126 + $result = [
127 + 'success' => false,
128 + 'code' => 200,
129 + 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
130 + ];
122 131
123 132 try {
124 133 $bucket = $googleClient->bucket($this->token . '_dummy-bucket-for-auth-check');
125 134 $exists = $bucket->exists(); // Triggers the API call
@@ -184,13 +193,19 @@
184 193 * Verify Bucket Exists
185 194 * @since 1.0.0
186 195 * @return boolean
187 196 */
188 - public function verifyBucketExist( $config_json, $bucket_name ){
189 - if ( !( isset($config_json) && !empty($config_json) && !empty($bucket_name) ) ) {
197 + public function verifyBucketExist( $config = [], $bucketConfig = [] ){
198 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
199 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
200 + if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
190 201 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
191 202 }
192 203
204 + if ( !Utils::is_json( $config_json ) ) {
205 + return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
206 + }
207 +
193 208 try {
194 209 $config_array = json_decode($config_json, true);
195 210 if (is_array($config_array)) {
196 211 $googleClient = new StorageClient([
@@ -226,15 +241,8 @@
226 241 'code' => 200,
227 242 'success' => false,
228 243 ];
229 244 }
230 -
231 -
232 - if($bucket_found) {
233 - return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
234 - } else {
235 - return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
236 - }
237 245 } catch (Exception $ex) {
238 246 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
239 247 }
240 248 }
@@ -244,10 +252,13 @@
244 252 * Create Bucket
245 253 * @since 1.0.0
246 254 * @return boolean
247 255 */
248 - public function createBucket( $config_json, $region, $bucket_name ){
249 - if ( !( isset($config_json) && !empty($config_json) && !empty($region) && !empty($bucket_name) ) ) {
256 + public function createBucket( $config = [], $bucketConfig = [] ){
257 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
258 + $region = isset($bucketConfig['region']) ? $bucketConfig['region'] : '';
259 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
260 + if ( Service::has_missing_fields([$config_json, $region, $bucket_name]) ) {
250 261 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
251 262 }
252 263
253 264 if ( !Utils::is_json( $config_json ) ) {
@@ -325,9 +336,9 @@
325 336 'success' => true,
326 337 ];
327 338
328 339 } catch (Exception $ex) {
329 - return ['message' => $e->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
340 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
330 341 }
331 342 }
332 343
333 344 /**
@@ -333,10 +344,12 @@
333 344 /**
334 345 * Check Bucket Write Permission
335 346 * @since 1.0.0
336 347 */
337 - public function verifyObjectWritePermission($config_json, $bucket_name){
338 - if ( !( isset($config_json) && !empty($config_json) && !empty($bucket_name) ) ) {
348 + public function verifyObjectWritePermission( $config = [], $bucketConfig = [] ) {
349 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
350 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
351 + if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
339 352 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
340 353 }
341 354
342 355 if ( !Utils::is_json( $config_json ) ) {
@@ -362,9 +375,9 @@
362 375 } else {
363 376 return ['message' => esc_html__('No Buckets found', 'media-cloud-sync'), 'code' => 200, 'success' => false];
364 377 }
365 378 if ($bucket_found) {
366 - $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
379 + $object_key = Utils::get_permission_check_object_key();
367 380
368 381 // Prepare a temporary file with content to check write permission
369 382 $stream = fopen('php://temp', 'r+');
370 383 fwrite($stream, 'This is a test object to check write permission.');
@@ -376,14 +389,11 @@
376 389 [
377 390 'name' => $object_key,
378 391 ]
379 392 );
380 - if(is_resource($stream)) {
381 - fclose($stream);
382 - }
383 393
384 394 if ($object->exists()) {
385 - return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
395 + return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
386 396 } else {
387 397 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
388 398 }
389 399 }
@@ -388,8 +398,12 @@
388 398 }
389 399 }
390 400 } catch (Exception $ex) {
391 401 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
402 + } finally {
403 + if (isset($stream) && is_resource($stream)) {
404 + fclose($stream);
405 + }
392 406 }
393 407 }
394 408
395 409 /**
@@ -395,10 +409,13 @@
395 409 /**
396 410 * Check Bucket Delete Permission
397 411 * @since 1.0.0
398 412 */
399 - public function verifyObjectDeletePermission($config_json, $bucket_name){
400 - if ( !( isset($config_json) && !empty($config_json) && !empty($bucket_name) ) ) {
413 + public function verifyObjectDeletePermission( $config = [], $bucketConfig = [] ) {
414 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
415 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
416 +
417 + if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
401 418 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
402 419 }
403 420 if( !Utils::is_json( $config_json ) ) {
404 421 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
@@ -419,9 +436,9 @@
419 436 }
420 437
421 438 $bucket = $googleClient->bucket($bucket_name);
422 439 try {
423 - $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
440 + $object_key = Utils::get_permission_check_object_key();
424 441
425 442 // Try fetching a dummy object to test access
426 443 $object = $bucket->object($object_key);
427 444 if ($object->exists()) {
@@ -447,9 +464,9 @@
447 464 ];
448 465 }
449 466 } catch (Exception $ex) {
450 467 return [
451 - 'message' => esc_html__('Object does not exist or credentials are invalid: ', 'media-cloud-sync') . $e->getMessage(),
468 + 'message' => esc_html__('Object does not exist or credentials are invalid: ', 'media-cloud-sync') . $ex->getMessage(),
452 469 'code' => 200,
453 470 'success' => false,
454 471 ];
455 472 }
@@ -468,11 +485,10 @@
468 485 'status' => false,
469 486 'message' => '',
470 487 'lastChecked' => time(),
471 488 ];
472 - if (empty($this->gcloudClient) || empty($this->bucket_name)) {
489 + if (Service::has_missing_fields([$this->gcloudClient, $this->bucket_name])) {
473 490 $result['message'] = esc_html__('Please check the authorization details', 'media-cloud-sync');
474 - Utils::set_status('cdnRead', $result);
475 491 return [
476 492 'message' => $result['message'],
477 493 'code' => 200,
478 494 'success' => false,
@@ -480,12 +496,12 @@
480 496 ];
481 497 }
482 498
483 499 try {
484 - $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
500 + $object_key = Utils::get_permission_check_object_key();
485 501
486 502 // Check if the object was created successfully
487 - if (!$this->bucket->object($object_key)->exists()) {
503 + if (!$this->exists($object_key)) {
488 504 // Create a dummy object to check write permission
489 505 $stream = fopen('php://temp', 'r+');
490 506 fwrite($stream, 'This is a test object to check read permission.');
491 507 rewind($stream);
@@ -492,18 +508,15 @@
492 508 $this->bucket->upload(
493 509 $stream,
494 510 [
495 511 'name' => $object_key,
512 + 'metadata' => ['cacheControl' => 'no-cache, no-store, must-revalidate'],
496 513 ]
497 514 );
498 - if (is_resource($stream)) {
499 - fclose($stream);
500 - }
501 515 // Re-check if the object was created successfully
502 - if (!$this->bucket->object($object_key)->exists()) {
516 + if (!$this->exists($object_key)) {
503 517 $result['status'] = false;
504 518 $result['message'] = esc_html__('Failed to create an object for read permission check, please check service configuration', 'media-cloud-sync');
505 - Utils::set_status('cdnRead', $result);
506 519 return [
507 520 'message' => $result['message'],
508 521 'code' => 200,
509 522 'success' => false,
@@ -514,24 +527,30 @@
514 527
515 528 $url = $this->generate_file_url($object_key);
516 529 $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
517 530
518 - $headers = @get_headers($cdn_url);
519 - if (strpos($headers[0], '200') !== false) {
531 + // Never trust a cached response for this fixed, predictable URL — a stale cached
532 + // error would otherwise keep failing the check long after real access is fine.
533 + $no_cache_context = stream_context_create(['http' => ['header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"]]);
534 + $headers = @get_headers($cdn_url, false, $no_cache_context);
535 + $status_code = (is_array($headers) && !empty($headers[0]) && preg_match('/\s(\d{3})\s/', $headers[0], $matches))
536 + ? (int) $matches[1]
537 + : 0;
538 +
539 + if ($status_code === 200) {
520 540 $result['status'] = true;
521 541 $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
522 - } else if (strpos($headers[0], '403') !== false) {
542 + } else if ($status_code === 403) {
523 543 $result['status'] = false;
524 - if($this->cdnConfig['service'] == $this->service) {
544 + if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
525 545 $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
526 546 } else {
527 547 $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
528 548 }
529 - $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
530 - } else if (strpos($headers[0], '404') !== false) {
549 + } else if ($status_code === 404) {
531 550 $result['status'] = false;
532 551 $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
533 - } else if (strpos($headers[0], '500') !== false) {
552 + } else if ($status_code === 500) {
534 553 $result['status'] = false;
535 554 $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
536 555 } else {
537 556 $result['status'] = false;
@@ -536,9 +555,8 @@
536 555 } else {
537 556 $result['status'] = false;
538 557 $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
539 558 }
540 - Utils::set_status('cdnRead', $result);
541 559 $this->deleteSingle($object_key);
542 560 return [
543 561 'message' => $result['message'],
544 562 'code' => 200,
@@ -545,11 +563,17 @@
545 563 'success' => $result['status'],
546 564 'lastChecked' => $result['lastChecked'],
547 565 ];
548 566 } catch (ServiceException $ex) {
549 - return ['message' => $ex->getMessage(), 'code' => 200, 'success' => false];
567 + $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
568 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
550 569 } catch (Exception $ex) {
551 - return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
570 + $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
571 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
572 + } finally {
573 + if (isset($stream) && is_resource($stream)) {
574 + fclose($stream);
575 + }
552 576 }
553 577 }
554 578
555 579 /**
@@ -560,9 +584,9 @@
560 584 if ($this->gcloudClient) {
561 585 try {
562 586 $bucket = $this->gcloudClient->bucket($this->token . '_dummy-bucket-for-auth-check');
563 587 $exists = $bucket->exists(); // Triggers the API call
564 - return false;
588 + return true;
565 589 } catch (ServiceException $e) {
566 590 $statusCode = $e->getCode();
567 591
568 592 $validErrors = [200, 403, 404];
@@ -585,8 +609,9 @@
585 609 *
586 610 */
587 611 public function toPrivate($key) {
588 612 if(!$key) return false;
613 + if(!$this->bucket) return false;
589 614
590 615 try {
591 616 $object = $this->bucket->object($key);
592 617 if ($object->exists()) {
@@ -606,12 +631,13 @@
606 631
607 632 /**
608 633 * Make Object Public
609 634 * @since 1.0.0
610 - *
635 + *
611 636 */
612 637 public function toPublic($key) {
613 638 if(!$key) return false;
639 + if(!$this->bucket) return false;
614 640
615 641 try {
616 642 $object = $this->bucket->object($key);
617 643 if ($object->exists()) {
@@ -627,18 +653,309 @@
627 653 return false;
628 654 }
629 655 }
630 656
657 + /**
658 + * Fetch the bucket's IAM policy with the plugin's own
659 + * allUsers:roles/storage.objectViewer binding(s) dropped — shared by
660 + * both drop_bucket_level_grant() and restore_bucket_level_grant() so
661 + * the find-and-drop logic isn't written twice. Every other binding
662 + * (project owners/editors, other service accounts, etc.) is left
663 + * exactly as found, unlike S3 where the whole policy is safely one
664 + * plugin-owned statement.
665 + * @since 1.4.1
666 + */
667 + private function bucket_policy_without_own_binding() {
668 + $iam = $this->bucket->iam();
669 + $policy = $iam->policy(['requestedPolicyVersion' => 3]);
631 670
671 + $bindings = [];
672 + foreach (($policy['bindings'] ?? []) as $binding) {
673 + if (
674 + isset($binding['role'], $binding['members']) &&
675 + $binding['role'] === 'roles/storage.objectViewer' &&
676 + in_array('allUsers', (array) $binding['members'], true)
677 + ) {
678 + continue;
679 + }
680 + $bindings[] = $binding;
681 + }
682 +
683 + return ['iam' => $iam, 'policy' => $policy, 'bindings' => $bindings];
684 + }
685 +
632 686 /**
633 - * Check the object exist
687 + * Drop the plugin's bucket-wide allUsers:objectViewer binding, if any,
688 + * and do not re-add it — used by the enable path, once the
689 + * Managed-Folder-scoped grant is already confirmed in effect.
690 + * @since 1.4.1
691 + */
692 + private function drop_bucket_level_grant() {
693 + $state = $this->bucket_policy_without_own_binding();
694 + $state['policy']['bindings'] = $state['bindings'];
695 + $state['policy']['version'] = 3;
696 + $state['iam']->setPolicy($state['policy'], ['requestedPolicyVersion' => 3]);
697 + }
698 +
699 + /**
700 + * Find-and-drop then re-add exactly one bucket-wide
701 + * allUsers:objectViewer binding — mirrors createBucket()'s original
702 + * grant. Used by the disable path to restore the plugin's original,
703 + * pre-private-media public-access mechanism; find-and-drop-first
704 + * guarantees a repeated apply/remove cycle never accumulates
705 + * duplicate bindings.
706 + * @since 1.4.1
707 + */
708 + private function restore_bucket_level_grant() {
709 + $state = $this->bucket_policy_without_own_binding();
710 + $state['bindings'][] = [
711 + 'role' => 'roles/storage.objectViewer',
712 + 'members' => ['allUsers'],
713 + ];
714 + $state['policy']['bindings'] = $state['bindings'];
715 + $state['policy']['version'] = 3;
716 + $state['iam']->setPolicy($state['policy'], ['requestedPolicyVersion' => 3]);
717 + }
718 +
719 + /**
720 + * Hand-written, authenticated REST call against GCS's Managed Folders
721 + * API (storage/v1/b/{bucket}/managedFolders/...) — the vendored SDK has
722 + * no native class for this resource. Mints a fresh Guzzle client from
723 + * the same service-account JSON already trusted for the ordinary
724 + * StorageClient, since Bucket::$connection/StorageClient::$connection
725 + * have no public accessor into their internal auth machinery.
726 + *
727 + * $http_errors is disabled so 4xx/5xx responses are returned (not
728 + * thrown) — callers need to distinguish e.g. 409 (already exists) and
729 + * 404 (already gone) from genuine failures, which is far cleaner done
730 + * by inspecting the status code than by parsing exception messages.
731 + * @since 1.4.1
732 + */
733 + private function managed_folder_iam_request($method, $path, $body = null) {
734 + $keyArray = json_decode($this->config['config_json'], true);
735 + $fetcher = CredentialsLoader::makeCredentials(
736 + // Matches the vendored StorageClient's own implicit default scope list
737 + // (StorageClient.php:166-167) — every StorageClient construction in this
738 + // file omits `scopes` and gets this same pair; FULL_CONTROL_SCOPE alone
739 + // is narrower and risks a 403 at the OAuth-scope layer, independent of
740 + // and prior to whatever IAM role/permission the service account holds.
741 + ['https://www.googleapis.com/auth/iam', StorageClient::FULL_CONTROL_SCOPE],
742 + $keyArray
743 + );
744 + $httpClient = CredentialsLoader::makeHttpClient($fetcher, [
745 + 'timeout' => 15,
746 + 'connect_timeout' => 5,
747 + ]);
748 +
749 + $url = 'https://storage.googleapis.com/storage/v1/b/' . rawurlencode($this->bucket_name) . '/managedFolders' . $path;
750 +
751 + $options = ['http_errors' => false];
752 + if ($body !== null) {
753 + $options['json'] = $body;
754 + }
755 +
756 + $response = $httpClient->request($method, $url, $options);
757 +
758 + return [
759 + 'status' => $response->getStatusCode(),
760 + 'body' => json_decode((string) $response->getBody(), true),
761 + ];
762 + }
763 +
764 + /**
765 + * Apply (or, with an empty $private_prefix, un-apply) the private-path
766 + * carve-out via GCS Managed Folders.
767 + *
768 + * Google Cloud permanently disallows attaching an IAM Condition to a
769 + * binding whose principal is allUsers, so the previous CEL-conditional
770 + * approach here could never succeed. Managed Folders let a role be
771 + * granted to allUsers scoped to one prefix with no condition at all —
772 + * but the grant is purely additive (it can only add access, never
773 + * restrict it), so exclusion only works because private_path is a
774 + * sibling of base_path, not nested inside it: the Managed Folder is
775 + * always scoped to base_path (read directly from settings, not derived
776 + * from $private_prefix, which is the *private*-path prefix).
777 + * @since 1.4.1
778 + */
779 + public function applyPrivatePathPolicy($private_prefix) {
780 + if (!$this->bucket || empty($this->bucket_name)) {
781 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
782 + }
783 +
784 + $base_path = isset($this->settings['base_path']) ? trim($this->settings['base_path'], " \n\r\t\v\x00\/ ") : '';
785 + // Trailing slash: unverified against a live GCS project — Google's own
786 + // managedFolder.insert REST reference shows no trailing slash in its
787 + // examples, while its separate CLI guide uses one. Captured once here and
788 + // reused verbatim (URL-encoded) at every call site below so insert/
789 + // setIamPolicy/delete always address the exact same resource name.
790 + $folder_name = $base_path . '/';
791 +
792 + try {
793 + if (empty($private_prefix)) {
794 + // Disable: restore the bucket-wide public grant FIRST, so there's
795 + // never a window where base_path content has no public grant at
796 + // all — then clean up the now-redundant Managed Folder
797 + // (best-effort, not security-critical: the grant that actually
798 + // matters is already restored by the time this runs).
799 + $this->restore_bucket_level_grant();
800 +
801 + if (!empty($base_path)) {
802 + $delete = $this->managed_folder_iam_request('DELETE', '/' . rawurlencode($folder_name) . '?allowNonEmpty=true');
803 + if ($delete['status'] >= 300 && $delete['status'] !== 404) {
804 + error_log('Media Cloud Sync: failed to delete the GCS Managed Folder for base_path while disabling private media — ' . wp_json_encode($delete['body']));
805 + }
806 + }
807 +
808 + return ['success' => true, 'code' => 200, 'message' => esc_html__('Policy removed successfully', 'media-cloud-sync')];
809 + }
810 +
811 + if (empty($base_path)) {
812 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Google Cloud Storage private media requires a base path — enable it in Storage Settings first.', 'media-cloud-sync')];
813 + }
814 +
815 + // Uniform Bucket-Level Access and Public Access Prevention need a live
816 + // $bucket->info() call, which is why these checks live here rather than
817 + // in ProPrivateMedia::apply_policy() (which only has settings, not the
818 + // bucket) — the enable_base_path / outside-base_path checks that DON'T
819 + // need a live call already ran there, before this method was reached.
820 + $info = $this->bucket->info();
821 + $iamConfig = isset($info['iamConfiguration']) ? $info['iamConfiguration'] : [];
822 + $ublaEnabled = !empty($iamConfig['uniformBucketLevelAccess']['enabled']);
823 + $pap = isset($iamConfig['publicAccessPrevention']) ? $iamConfig['publicAccessPrevention'] : 'inherited';
824 +
825 + if (!$ublaEnabled) {
826 + return ['success' => false, 'code' => 200, 'message' => esc_html__("This bucket doesn't have Uniform Bucket-Level Access enabled — enable it in your Google Cloud Storage bucket settings first.", 'media-cloud-sync')];
827 + }
828 + if ($pap === 'enforced') {
829 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Public Access Prevention is enabled for this bucket — disable it first in Bucket Security, since it blocks the public side of this feature too.', 'media-cloud-sync')];
830 + }
831 +
832 + // Enable, in an order that never leaves a window with no public access:
833 + // create + set the Managed Folder's grant first (purely additive — safe
834 + // to briefly overlap with the still-present bucket-wide grant), only
835 + // then drop the bucket-wide grant.
836 + $insert = $this->managed_folder_iam_request('POST', '', ['name' => $folder_name]);
837 + if ($insert['status'] >= 300 && $insert['status'] !== 409) {
838 + $message = isset($insert['body']['error']['message']) ? $insert['body']['error']['message'] : esc_html__('Failed to create the Managed Folder for your base path.', 'media-cloud-sync');
839 + return ['success' => false, 'code' => 200, 'message' => $message];
840 + }
841 +
842 + $setIam = $this->managed_folder_iam_request('PUT', '/' . rawurlencode($folder_name) . '/iam', [
843 + 'bindings' => [
844 + [
845 + 'role' => 'roles/storage.objectViewer',
846 + 'members' => ['allUsers'],
847 + ],
848 + ],
849 + ]);
850 + if ($setIam['status'] >= 300) {
851 + $message = isset($setIam['body']['error']['message']) ? $setIam['body']['error']['message'] : esc_html__('Failed to grant public access on the Managed Folder.', 'media-cloud-sync');
852 + return ['success' => false, 'code' => 200, 'message' => $message];
853 + }
854 +
855 + // Only once the Managed Folder grant is confirmed in effect (both calls
856 + // above succeeded): drop the bucket-wide grant so nothing is public
857 + // bucket-wide anymore. If either call above failed, we stop before this
858 + // line — the bucket is left exactly as it was (bucket-level grant still
859 + // in place, no Managed Folder actively granting anything since its IAM
860 + // policy was never successfully set), a safe, easily-retried state.
861 + $this->drop_bucket_level_grant();
862 +
863 + return ['success' => true, 'code' => 200, 'message' => esc_html__('Policy applied successfully', 'media-cloud-sync')];
864 + } catch (ServiceException $e) {
865 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
866 + } catch (Exception $e) {
867 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
868 + }
869 + }
870 +
871 + /**
872 + * Read the bucket's Public Access Prevention state — GCS's closest
873 + * analog to S3's Block Public Access. Built from a fresh StorageClient/
874 + * Bucket from the passed params (not $this->gcloudClient/$this->bucket)
875 + * so this works during initial setup in the Configure wizard, before
876 + * the connection being configured is the saved/active one — matching
877 + * S3's own getBucketSecuritySettings() pattern.
878 + * @since 1.4.1
879 + */
880 + public function getBucketSecuritySettings($config = [], $bucketConfig = []) {
881 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
882 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
883 +
884 + if (empty($config_json) || empty($bucket_name) || !Utils::is_json($config_json)) {
885 + return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
886 + }
887 +
888 + try {
889 + $keyArray = json_decode($config_json, true);
890 + if (!is_array($keyArray)) {
891 + return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
892 + }
893 +
894 + $client = new StorageClient(['keyFile' => $keyArray]);
895 + $bucket = $client->bucket($bucket_name);
896 + $info = $bucket->info();
897 + $pap = isset($info['iamConfiguration']['publicAccessPrevention']) ? $info['iamConfiguration']['publicAccessPrevention'] : 'inherited';
898 +
899 + $security = ['block_public_access' => $pap === 'enforced'];
900 +
901 + return ['message' => '', 'code' => 200, 'success' => true, 'security' => $security];
902 + } catch (ServiceException $e) {
903 + return ['message' => $e->getMessage() ?: esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
904 + } catch (Exception $e) {
905 + return ['message' => $e->getMessage() ?: esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
906 + }
907 + }
908 +
909 + /**
910 + * Set the bucket's Public Access Prevention state. Built from a fresh
911 + * StorageClient/Bucket from the passed params — same reasoning as
912 + * getBucketSecuritySettings() above. No changeObjectOwnership()
913 + * equivalent here — GCS has no matching concept; the generic dispatcher
914 + * simply hides that field via method_exists() when it's undefined.
915 + * @since 1.4.1
916 + */
917 + public function changePublicAccess($config = [], $bucketConfig = [], $value = false) {
918 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
919 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
920 +
921 + if (empty($config_json) || empty($bucket_name) || !Utils::is_json($config_json)) {
922 + return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
923 + }
924 +
925 + try {
926 + $keyArray = json_decode($config_json, true);
927 + if (!is_array($keyArray)) {
928 + return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
929 + }
930 +
931 + $client = new StorageClient(['keyFile' => $keyArray]);
932 + $bucket = $client->bucket($bucket_name);
933 + $bucket->update([
934 + 'iamConfiguration' => [
935 + 'publicAccessPrevention' => $value ? 'enforced' : 'inherited',
936 + ],
937 + ]);
938 +
939 + return ['message' => '', 'code' => 200, 'success' => true];
940 + } catch (ServiceException $e) {
941 + return ['message' => $e->getMessage() ?: esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
942 + } catch (Exception $e) {
943 + return ['message' => $e->getMessage() ?: esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
944 + }
945 + }
946 +
947 +
948 + /**
949 + * Check the object exist
634 950 * @since 1.1.8
635 951 */
636 - public function exists($key) {
952 + public function exists($key, $bucket = null) {
637 953 if(!$key) return false;
638 954
639 955 try {
640 - $object = $this->bucket->object($key);
956 + $bucket = $bucket ?? $this->bucket;
957 + $object = $bucket->object($key);
641 958 if ($object->exists()) {
642 959 return true;
643 960 } else {
644 961 return false;
@@ -655,116 +972,154 @@
655 972 }
656 973
657 974
658 975 /**
976 + * List Objects — $delimiter = null gives a flat/recursive listing instead of one folder level.
977 + * resultLimit=$maxKeys caps the iterator to this page only (Bucket::objects() would otherwise auto-paginate the whole bucket).
978 + * @since 1.3.13
979 + */
980 + public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
981 + if (!$this->bucket) {
982 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
983 + }
984 + try {
985 + $options = [
986 + 'maxResults' => $maxKeys,
987 + 'resultLimit' => $maxKeys,
988 + ];
989 + if (!empty($delimiter)) {
990 + $options['delimiter'] = $delimiter;
991 + }
992 + if (!empty($prefix)) {
993 + $options['prefix'] = $prefix;
994 + }
995 + if (!empty($continuationToken)) {
996 + $options['pageToken'] = $continuationToken;
997 + }
998 +
999 + $iterator = $this->bucket->objects($options);
1000 +
1001 + $objects = [];
1002 + foreach ($iterator as $object) {
1003 + $key = $object->name();
1004 + if ($key === $prefix) {
1005 + continue; // the folder placeholder object itself, not a file
1006 + }
1007 + $info = $object->info();
1008 + $objects[] = [
1009 + 'key' => $key,
1010 + 'size' => isset($info['size']) ? (int) $info['size'] : 0,
1011 + 'last_modified' => isset($info['updated']) ? $info['updated'] : '',
1012 + ];
1013 + }
1014 +
1015 + return [
1016 + 'success' => true,
1017 + 'code' => 200,
1018 + 'message' => '',
1019 + 'folders' => $iterator->prefixes(),
1020 + 'objects' => $objects,
1021 + 'next_token' => $iterator->nextResultToken(),
1022 + ];
1023 + } catch (ServiceException $e) {
1024 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
1025 + } catch (Exception $e) {
1026 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
1027 + }
1028 + }
1029 +
1030 + /**
659 1031 * Upload Single
660 1032 * @since 1.0.0
661 1033 * @return boolean
662 1034 */
663 - public function uploadSingle($media_absolute_path, $media_path, $prefix=''){
664 - $result = array();
1035 + public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false){
665 1036 if (
666 - isset($media_absolute_path) && !empty($media_absolute_path) &&
667 - isset($media_path) && !empty($media_path)
1037 + isset($absolute_source_path) && !empty($absolute_source_path) &&
1038 + isset($relative_source_path) && !empty($relative_source_path)
668 1039 ) {
669 - $file_name = wp_basename( $media_path );
1040 + $file_name = wp_basename( $relative_source_path );
670 1041 if ($file_name) {
671 - $upload_path = Utils::generate_object_key($media_path, $prefix);
1042 + $upload_path = Utils::generate_object_key($relative_source_path, $prefix, $is_private);
1043 + if ($upload_path === false) {
1044 + return [
1045 + 'success' => false,
1046 + 'code' => 200,
1047 + 'message' => esc_html__('This file is marked private, but the private-media add-on is not currently active — reupload skipped to avoid exposing it.', 'media-cloud-sync')
1048 + ];
1049 + }
1050 + return $this->execute_upload($absolute_source_path, $upload_path);
1051 + }
1052 + return [
1053 + 'success' => false,
1054 + 'code' => 200,
1055 + 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync'),
1056 + ];
1057 + }
1058 + return [
1059 + 'success' => false,
1060 + 'code' => 200,
1061 + 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
1062 + ];
1063 + }
672 1064
673 - // Decide Multipart upload or normal put object
674 - if (filesize($media_absolute_path) <= Schema::getConstant('GCLOUD_MULTIPART_MIN_FILE_SIZE')) {
675 - // Upload a publicly accessible file. The file size and type are determined by the SDK.
676 - try {
1065 + /**
1066 + * Upload a local file to an exact destination key (no Utils::generate_object_key() derivation).
1067 + * @since 1.4.0
1068 + */
1069 + public function uploadObjectAtKey($absolute_source_path, $key) {
1070 + return $this->execute_upload($absolute_source_path, $key);
1071 + }
677 1072
678 - $upload = $this->bucket->upload(
679 - fopen($media_absolute_path, 'r'),
680 - [
681 - 'name' => $upload_path,
682 - ]
683 - );
1073 + // Chunked upload above GCLOUD_MULTIPART_MIN_FILE_SIZE, single request below it — same
1074 + // threshold uploadSingle() always used, now shared with uploadObjectAtKey().
1075 + private function execute_upload($absolute_source_path, $key) {
1076 + $options = ['name' => $key];
1077 + if (filesize($absolute_source_path) > Schema::getConstant('GCLOUD_MULTIPART_MIN_FILE_SIZE')) {
1078 + $options['chunkSize'] = 262144 * 2;
1079 + }
1080 + $cache_control = Utils::get_cache_control_header();
1081 + if ($cache_control) {
1082 + $options['cacheControl'] = $cache_control;
1083 + }
684 1084
685 - $object = $this->bucket->object($upload_path);
1085 + try {
1086 + $handle = fopen($absolute_source_path, 'rb');
1087 + $upload = $this->bucket->upload($handle, $options);
686 1088
687 - if ($object->exists()) {
688 - $result = array(
689 - 'success' => true,
690 - 'code' => 200,
691 - 'file_url' => $this->generate_file_url($upload_path),
692 - 'key' => $upload_path,
693 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
694 - );
695 - } else {
696 - $result = array(
697 - 'success' => false,
698 - 'code' => 200,
699 - 'message' => esc_html__('Object not found at server.', 'media-cloud-sync'),
700 - );
701 - }
702 - } catch (Exception $e) {
703 - $result = array(
704 - 'success' => false,
705 - 'code' => 200,
706 - 'message' => $e->getMessage(),
707 - );
708 - }
709 - } else {
710 - try {
711 - $upload = $this->bucket->upload(
712 - fopen($media_absolute_path, 'r'),
713 - [
714 - 'name' => $upload_path,
715 - 'chunkSize' => 262144 * 2,
716 - ]
717 - );
718 -
719 - $object = $this->bucket->object($upload_path);
720 -
721 - if ($object->exists()) {
722 - $result = array(
723 - 'success' => true,
724 - 'code' => 200,
725 - 'file_url' => $this->generate_file_url($upload_path),
726 - 'key' => $upload_path,
727 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
728 - );
729 - } else {
730 - $result = array(
731 - 'success' => false,
732 - 'code' => 200,
733 - 'message' => esc_html__('Something happened while uploading to server', 'media-cloud-sync'),
734 - );
735 - }
736 - } catch (Exception $e) {
737 - $result = array(
738 - 'success' => false,
739 - 'code' => 200,
740 - 'message' => $e->getMessage(),
741 - );
742 - }
743 - }
744 - } else {
745 - $result = array(
746 - 'success' => false,
747 - 'code' => 200,
748 - 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync'),
749 - );
1089 + if ($upload->exists()) {
1090 + return [
1091 + 'success' => true,
1092 + 'code' => 200,
1093 + 'file_url' => $this->generate_file_url($key),
1094 + 'key' => $key,
1095 + 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
1096 + ];
750 1097 }
751 - } else {
752 - $result = array(
753 - 'success' => false,
754 - 'code' => 200,
755 - 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
756 - );
1098 + return [
1099 + 'success' => false,
1100 + 'code' => 200,
1101 + 'message' => esc_html__('Object not found at server.', 'media-cloud-sync'),
1102 + ];
1103 + } catch (Exception $e) {
1104 + return [
1105 + 'success' => false,
1106 + 'code' => 200,
1107 + 'message' => $e->getMessage(),
1108 + ];
1109 + } finally {
1110 + if (isset($handle) && is_resource($handle)) {
1111 + fclose($handle);
1112 + }
757 1113 }
758 - return $result;
759 1114 }
760 1115
761 -
762 1116 /**
763 1117 * Save object to server
764 1118 * @since 1.0.0
765 1119 */
766 1120 public function object_to_server($key, $save_path){
1121 + if(!$this->bucket) return false;
767 1122 try {
768 1123 $object = $this->bucket->object($key);
769 1124 if ($object->exists()) {
770 1125 $object->downloadToFile($save_path);
@@ -777,9 +1132,143 @@
777 1132 }
778 1133 return false;
779 1134 }
780 1135
1136 + /**
1137 + * Object bytes in memory, no local file — for callers (e.g. zip download) that need
1138 + * the content itself rather than a copy on the server's filesystem.
1139 + * @since 1.3.13
1140 + */
1141 + public function get_object_content($key) {
1142 + if(!$this->bucket) return false;
1143 + try {
1144 + $object = $this->bucket->object($key);
1145 + if ($object->exists()) {
1146 + return $object->downloadAsString();
1147 + }
1148 + } catch (Exception $e) {
1149 + return false;
1150 + }
1151 + return false;
1152 + }
781 1153
1154 + /**
1155 + * Deletes the live generation, then best-effort purges every prior generation too — a
1156 + * bucket with Object Versioning enabled otherwise keeps old generations (and the storage
1157 + * they use) around at the old key. The live delete happens unconditionally first, in its
1158 + * own try/catch, so the object still ends up gone even if the generation-listing call
1159 + * below fails for any reason.
1160 + * @since 1.3.14
1161 + */
1162 + public function purge_all_versions($key) {
1163 + if (!$this->bucket) {
1164 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
1165 + }
1166 +
1167 + try {
1168 + $this->bucket->object($key)->delete();
1169 + } catch (ServiceException $e) {
1170 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
1171 + } catch (\Exception $e) {
1172 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
1173 + }
1174 +
1175 + // Best-effort only from here — the live copy above is already gone regardless of
1176 + // whether this bucket has Object Versioning enabled or this call succeeds.
1177 + try {
1178 + foreach ($this->bucket->objects(['prefix' => $key, 'versions' => true]) as $object) {
1179 + if ($object->name() === $key) {
1180 + $object->delete();
1181 + }
1182 + }
1183 + } catch (ServiceException $e) {
1184 + // Generation history cleanup failed — not fatal, live object is gone.
1185 + } catch (\Exception $e) {
1186 + // Generation history cleanup failed — not fatal, live object is gone.
1187 + }
1188 +
1189 + return ['success' => true, 'code' => 200, 'message' => esc_html__('Purged Successfully', 'media-cloud-sync')];
1190 + }
1191 +
1192 +
1193 + /**
1194 + * Copy an object to a new path in Google Cloud Storage
1195 + *
1196 + * @param string $key Original object key (path in bucket)
1197 + * @param string $new_path Destination object key
1198 + * @return bool True if object was copied successfully, false otherwise
1199 + * @since 1.3.4
1200 + */
1201 + // Trusts copy()'s own success/failure rather than pre/post-verifying with extra
1202 + // exists() calls — each one is a full network round-trip, and with move/copy processing
1203 + // keys sequentially, extra round-trips per file add up fast on a folder with many files.
1204 + // copy() itself throws (caught below) if the source is missing or the copy otherwise
1205 + // fails, so nothing is lost by not checking first.
1206 + public function copy_to_new_path($key, $new_path) {
1207 + if (!$this->bucket) {
1208 + return [
1209 + 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
1210 + 'code' => 200,
1211 + 'success' => false
1212 + ];
1213 + }
1214 + try {
1215 + $sourceObject = $this->bucket->object($key);
1216 + $sourceObject->copy($this->bucket, ['name' => $new_path]);
1217 + return [
1218 + 'success' => true,
1219 + 'code' => 200,
1220 + 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1221 + ];
1222 + } catch (ServiceException $e) {
1223 + return [
1224 + 'success' => false,
1225 + 'code' => 200,
1226 + 'message' => $e->getMessage()
1227 + ];
1228 + } catch (\Exception $e) {
1229 + return [
1230 + 'success' => false,
1231 + 'code' => 200,
1232 + 'message' => $e->getMessage()
1233 + ];
1234 + }
1235 + }
1236 +
1237 + // Like copy_to_new_path() but into an explicit (possibly different) bucket — needs write
1238 + // access there too, so callers should fall back to download+upload on failure.
1239 + public function copy_to_bucket($key, $new_key, $dest_bucket) {
1240 + if (!$this->bucket || !$this->gcloudClient) {
1241 + return [
1242 + 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
1243 + 'code' => 200,
1244 + 'success' => false
1245 + ];
1246 + }
1247 + try {
1248 + $sourceObject = $this->bucket->object($key);
1249 + $sourceObject->copy($this->gcloudClient->bucket($dest_bucket), ['name' => $new_key]);
1250 + return [
1251 + 'success' => true,
1252 + 'code' => 200,
1253 + 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
1254 + ];
1255 + } catch (ServiceException $e) {
1256 + return [
1257 + 'success' => false,
1258 + 'code' => 200,
1259 + 'message' => $e->getMessage()
1260 + ];
1261 + } catch (\Exception $e) {
1262 + return [
1263 + 'success' => false,
1264 + 'code' => 200,
1265 + 'message' => $e->getMessage()
1266 + ];
1267 + }
1268 + }
1269 +
1270 +
782 1271 /**
783 1272 * Delete Single
784 1273 * @since 1.0.0
785 1274 * @return boolean
@@ -785,8 +1274,15 @@
785 1274 * @return boolean
786 1275 */
787 1276 public function deleteSingle($key){
788 1277 $result = array();
1278 + if (!$this->bucket) {
1279 + return array(
1280 + 'success' => false,
1281 + 'code' => 200,
1282 + 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1283 + );
1284 + }
789 1285 if (isset($key) && !empty($key)) {
790 1286 try {
791 1287 $object = $this->bucket->object($key);
792 1288 $object->delete();
@@ -822,34 +1318,41 @@
822 1318 }
823 1319
824 1320
825 1321 /**
826 - * get presigned URL
1322 + * get private URL
827 1323 * @since 1.0.0
828 1324 * @return boolean
829 1325 */
830 - public function get_presigned_url($key) {
1326 + public function get_private_url($key) {
831 1327 $result = array();
1328 + if (!$this->bucket) {
1329 + return array(
1330 + 'success' => false,
1331 + 'code' => 200,
1332 + 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1333 + );
1334 + }
832 1335 if (isset($key) && !empty($key)) {
833 1336 try {
834 1337 $object = $this->bucket->object($key);
835 1338
836 - $expires = isset($this->settings['presigned_expire']) ? $this->settings['presigned_expire'] : 20;
1339 + $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
837 1340
838 - $presignedUrl = $object->signedUrl(new \DateTime(sprintf('+%s minutes', $expires)));
1341 + $privateUrl = $object->signedUrl(new \DateTime(sprintf('+%s minutes', $expires)));
839 1342
840 - if ($presignedUrl) {
1343 + if ($privateUrl) {
841 1344 $result = array(
842 1345 'success' => true,
843 1346 'code' => 200,
844 - 'file_url' => $presignedUrl,
845 - 'message' => esc_html__('Got Presigned URL Successfully', 'media-cloud-sync'),
1347 + 'file_url' => $privateUrl,
1348 + 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync'),
846 1349 );
847 1350 } else {
848 1351 $result = array(
849 1352 'success' => false,
850 1353 'code' => 200,
851 - 'message' => esc_html__('Error getting presigned URL', 'media-cloud-sync'),
1354 + 'message' => esc_html__('Error getting private URL', 'media-cloud-sync'),
852 1355 );
853 1356 }
854 1357 } catch (Exception $e) {
855 1358 $result = array(
@@ -871,9 +1374,9 @@
871 1374
872 1375 /**
873 1376 * Generate file URL
874 1377 */
875 - private function generate_file_url($key){
1378 + public function generate_file_url($key){
876 1379 $domain = $this->get_domain();
877 1380
878 1381 return apply_filters('wpmcs_generate_google_file_url',
879 1382 $domain . '/' . $this->bucket_name . '/' . $key,
@@ -881,8 +1384,16 @@
881 1384 $this->bucket_name
882 1385 );
883 1386 }
884 1387
1388 + /**
1389 + * Is provider URL
1390 + * @since 1.3.6
1391 + */
1392 + public function is_provider_url($url) {
1393 + $domain = $this->get_domain();
1394 + return (strpos($url, $domain . '/' . $this->bucket_name . '/') !== false);
1395 + }
885 1396
886 1397 /**
887 1398 * Get domain URL
888 1399 */