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
media-cloud-sync / includes / base / services / gcloud.php

gcloud.php in Media Cloud Sync 1.4.1, at includes/base/services/gcloud.php

1,405 lines 58.1 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\GCP\Google\Cloud\Storage\StorageClient;
8 use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\Exception\ServiceException;
9 use Dudlewebs\WPMCS\GCP\Google\Auth\CredentialsLoader;
10
11 use Exception;
12
13 class GCloud {
14 private $assets_url;
15 private $version;
16 private $token;
17
18 protected $config;
19 protected $bucketConfig;
20 protected $settings;
21 protected $credentials;
22 protected $bucket_name;
23 protected $bucket; // Object
24 protected $cdnConfig;
25
26 public $service = 'gcloud';
27 public $gcloudClient = false;
28
29 /**
30 * Admin constructor.
31 * @since 1.0.0
32 */
33 public function __construct($credentials = null) {
34 $this->assets_url = WPMCS_ASSETS_URL;
35 $this->version = WPMCS_VERSION;
36 $this->token = WPMCS_TOKEN;
37
38 // Initialize setup
39 $this->init($credentials);
40 }
41
42 /**
43 * Initialise Client
44 *
45 * @param array|null $credentials Optional explicit credentials; falls back to
46 * Utils::get_credentials() when omitted.
47 */
48 public function init($credentials = null) {
49 $this->settings = Utils::get_settings();
50 $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
51 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
52 ? $this->credentials['config']
53 : [];
54 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
55 ? $this->credentials['bucketConfig']
56 : [];
57 $this->bucket_name = isset($this->bucketConfig['bucket_name']) && !empty($this->bucketConfig['bucket_name'])
58 ? $this->bucketConfig['bucket_name']
59 : '';
60 $this->cdnConfig = isset($this->credentials['cdn']) && !empty($this->credentials['cdn'])
61 ? $this->credentials['cdn']
62 : [];
63
64 if (
65 isset($this->config['config_json']) && !empty($this->config['config_json']) &&
66 isset($this->bucket_name) && !empty($this->bucket_name)
67 ) {
68 if(Utils::is_json($this->config['config_json'])){
69 // Set google client
70 $keyArray = json_decode($this->config['config_json'], true);
71
72 if (is_array($keyArray)) {
73 $this->gcloudClient = new StorageClient([
74 'keyFile' => $keyArray,
75 ]);
76 $this->bucket = $this->gcloudClient->bucket($this->bucket_name);
77 } else {
78 // Handle JSON decode failure
79 throw new \Exception('Invalid JSON provided for GCloud credentials.');
80 }
81 } else {
82 add_action('admin_notices', function (){
83 echo wp_kses_post(sprintf( "<div class='error'><p><strong>%s: </strong><br>Google Cloud Storage configuration is invalid.
84 It may break the media url's as well as media uploads.<br>
85 <a href='%s'>Re-configure</a> plugin to fix the issue.
86 </p></div>",
87 esc_html__('Media Cloud Sync', 'media-cloud-sync'),
88 admin_url('admin.php?page='.$this->token . '-admin-ui#/configure')
89 ));
90 });
91
92 }
93 }
94 }
95
96 /**
97 * Verify Credentials
98 * @since 1.0.0
99 * @return boolean
100 */
101 public function verifyCredentials( $config = [] ){
102 $config_json = isset($config['config_json']) ? $config['config_json'] : '';
103 if (!Service::has_missing_fields([$config_json])) {
104 if(!Utils::is_json($config_json)){
105 return [
106 'success' => false,
107 'code' => 200,
108 'message' => esc_html__('Invalid JSON configuration, please try again', 'media-cloud-sync'),
109 ];
110 }
111
112 try {
113 $config_array = json_decode($config_json, true);
114 if (is_array($config_array)) {
115 $googleClient = new StorageClient([
116 'keyFile' => $config_array
117 ]);
118 } else {
119 return [
120 'success' => false,
121 'code' => 200,
122 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
123 ];
124 }
125
126 $result = [
127 'success' => false,
128 'code' => 200,
129 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
130 ];
131
132 try {
133 $bucket = $googleClient->bucket($this->token . '_dummy-bucket-for-auth-check');
134 $exists = $bucket->exists(); // Triggers the API call
135
136 // If we reach here, the credentials are valid
137 $result = [
138 'success' => true,
139 'code' => 200,
140 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
141 ];
142 } catch (ServiceException $e) {
143 $statusCode = $e->getCode();
144
145 $validErrors = [200, 403, 404];
146
147 if (in_array($statusCode, $validErrors)) {
148 // If we reach here, the credentials are valid
149 $result = [
150 'success' => true,
151 'code' => 200,
152 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
153 ];
154 }
155 }
156
157 if($result['success'] == false) {
158 return $result;
159 }
160
161
162 try {
163 $buckets = $googleClient->buckets();
164 $newBucketFormat = [];
165 if(isset($buckets) && !empty($buckets)){
166 foreach($buckets as $bucket) {
167 $name = $bucket->name();
168 if(!empty($name)) {
169 // Fetch the bucket's metadata
170 $bucketInfo = $bucket->info();
171 $newBucketFormat[] = ['Name' => $name, 'CreationDate' => $bucketInfo['timeCreated']];
172 }
173 }
174 }
175 $result['buckets_data']['buckets'] = $newBucketFormat;
176 $result['buckets_data']['message'] = esc_html__('Buckets listed successfully', 'media-cloud-sync');
177 $result['buckets_data']['status'] = true;
178 } catch (Exception $e) {
179 $result ['buckets_data']['buckets'] = [];
180 $result ['buckets_data']['message'] = esc_html__('Unable to list buckets, Please check the bucket listing permission', 'media-cloud-sync');
181 $result ['buckets_data']['status'] = false;
182 }
183 return $result;
184 } catch (Exception $ex) {
185 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
186 }
187 }
188 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
189 }
190
191
192 /**
193 * Verify Bucket Exists
194 * @since 1.0.0
195 * @return boolean
196 */
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]) ) {
201 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
202 }
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
208 try {
209 $config_array = json_decode($config_json, true);
210 if (is_array($config_array)) {
211 $googleClient = new StorageClient([
212 'keyFile' => $config_array
213 ]);
214 } else {
215 return [
216 'success' => false,
217 'code' => 200,
218 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
219 ];
220 }
221
222 try {
223 $bucket = $googleClient->bucket($bucket_name);
224
225 if ($bucket->exists()) {
226 return [
227 'message' => esc_html__('Bucket exists', 'media-cloud-sync'),
228 'code' => 200,
229 'success' => true,
230 ];
231 } else {
232 return [
233 'message' => esc_html__('Bucket does not exist', 'media-cloud-sync'),
234 'code' => 200,
235 'success' => false,
236 ];
237 }
238 } catch (ServiceException $e) {
239 return [
240 'message' => esc_html__('Bucket does not exist or credentials are invalid: ', 'media-cloud-sync') . $e->getMessage(),
241 'code' => 200,
242 'success' => false,
243 ];
244 }
245 } catch (Exception $ex) {
246 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
247 }
248 }
249
250
251 /**
252 * Create Bucket
253 * @since 1.0.0
254 * @return boolean
255 */
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]) ) {
261 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
262 }
263
264 if ( !Utils::is_json( $config_json ) ) {
265 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
266 }
267
268 try {
269 $config_array = json_decode($config_json, true);
270 if (is_array($config_array)) {
271 $googleClient = new StorageClient([
272 'keyFile' => $config_array
273 ]);
274 } else {
275 return [
276 'success' => false,
277 'code' => 200,
278 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
279 ];
280 }
281
282 // Create Bucket
283 $bucket = $googleClient->createBucket($bucket_name, [
284 'location' => $region,
285 'iamConfiguration' => [
286 'uniformBucketLevelAccess' => [
287 'enabled' => true
288 ]
289 ]
290 ]);
291
292 // Fetch the bucket's IAM
293 try {
294 $iam = $bucket->iam();
295
296 $policy = $iam->policy();
297
298 // Add allUsers as a Storage Object Viewer
299 $policy['bindings'][] = [
300 'role' => 'roles/storage.objectViewer',
301 'members' => ['allUsers'],
302 ];
303
304 // Set the updated policy
305 $iam->setPolicy($policy);
306
307 } catch (ServiceException $e) {
308 return [
309 'message' => esc_html__('Bucket created successfully. But failed to set IAM policy.', 'media-cloud-sync'),
310 'data' => [
311 'Name' => $bucket_name,
312 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
313 ],
314 'code' => 200,
315 'success' => true,
316 ];
317 } catch (Exception $e) {
318 return [
319 'message' => esc_html__('Bucket created successfully. But failed to set IAM policy.', 'media-cloud-sync'),
320 'data' => [
321 'Name' => $bucket_name,
322 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
323 ],
324 'code' => 200,
325 'success' => true,
326 ];
327 }
328
329 return [
330 'message' => esc_html__('Bucket created successfully.', 'media-cloud-sync'),
331 'data' => [
332 'Name' => $bucket_name,
333 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
334 ],
335 'code' => 200,
336 'success' => true,
337 ];
338
339 } catch (Exception $ex) {
340 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
341 }
342 }
343
344 /**
345 * Check Bucket Write Permission
346 * @since 1.0.0
347 */
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]) ) {
352 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
353 }
354
355 if ( !Utils::is_json( $config_json ) ) {
356 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
357 }
358
359 try {
360 $config_array = json_decode($config_json, true);
361 if (is_array($config_array)) {
362 $googleClient = new StorageClient([
363 'keyFile' => $config_array
364 ]);
365 } else {
366 return [
367 'success' => false,
368 'code' => 200,
369 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
370 ];
371 }
372 $bucket = $googleClient->bucket($bucket_name);
373 if ($bucket->exists()) {
374 $bucket_found = true;
375 } else {
376 return ['message' => esc_html__('No Buckets found', 'media-cloud-sync'), 'code' => 200, 'success' => false];
377 }
378 if ($bucket_found) {
379 $object_key = Utils::get_permission_check_object_key();
380
381 // Prepare a temporary file with content to check write permission
382 $stream = fopen('php://temp', 'r+');
383 fwrite($stream, 'This is a test object to check write permission.');
384 rewind($stream);
385
386 // Upload the object to the bucket
387 $object = $bucket->upload(
388 $stream,
389 [
390 'name' => $object_key,
391 ]
392 );
393
394 if ($object->exists()) {
395 return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
396 } else {
397 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
398 }
399 }
400 } catch (Exception $ex) {
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 }
406 }
407 }
408
409 /**
410 * Check Bucket Delete Permission
411 * @since 1.0.0
412 */
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]) ) {
418 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
419 }
420 if( !Utils::is_json( $config_json ) ) {
421 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
422 }
423
424 try {
425 $config_array = json_decode($config_json, true);
426 if (is_array($config_array)) {
427 $googleClient = new StorageClient([
428 'keyFile' => $config_array
429 ]);
430 } else {
431 return [
432 'success' => false,
433 'code' => 200,
434 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
435 ];
436 }
437
438 $bucket = $googleClient->bucket($bucket_name);
439 try {
440 $object_key = Utils::get_permission_check_object_key();
441
442 // Try fetching a dummy object to test access
443 $object = $bucket->object($object_key);
444 if ($object->exists()) {
445 $object->delete();
446 if (!$object->exists()) {
447 return [
448 'message' => esc_html__('Bucket exists', 'media-cloud-sync'),
449 'code' => 200,
450 'success' => true,
451 ];
452 } else {
453 return [
454 'message' => esc_html__('You do not have permission to delete object', 'media-cloud-sync'),
455 'code' => 200,
456 'success' => false,
457 ];
458 }
459 } else {
460 return [
461 'message' => esc_html__('Object does not exist', 'media-cloud-sync'),
462 'code' => 200,
463 'success' => false,
464 ];
465 }
466 } catch (Exception $ex) {
467 return [
468 'message' => esc_html__('Object does not exist or credentials are invalid: ', 'media-cloud-sync') . $ex->getMessage(),
469 'code' => 200,
470 'success' => false,
471 ];
472 }
473
474 } catch (Exception $ex) {
475 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
476 }
477 }
478
479 /**
480 * Check Bucket Read Permission
481 * @since 1.2.4
482 */
483 public function verifyObjectReadPermission() {
484 $result = [
485 'status' => false,
486 'message' => '',
487 'lastChecked' => time(),
488 ];
489 if (Service::has_missing_fields([$this->gcloudClient, $this->bucket_name])) {
490 $result['message'] = esc_html__('Please check the authorization details', 'media-cloud-sync');
491 return [
492 'message' => $result['message'],
493 'code' => 200,
494 'success' => false,
495 'lastChecked' => $result['lastChecked'],
496 ];
497 }
498
499 try {
500 $object_key = Utils::get_permission_check_object_key();
501
502 // Check if the object was created successfully
503 if (!$this->exists($object_key)) {
504 // Create a dummy object to check write permission
505 $stream = fopen('php://temp', 'r+');
506 fwrite($stream, 'This is a test object to check read permission.');
507 rewind($stream);
508 $this->bucket->upload(
509 $stream,
510 [
511 'name' => $object_key,
512 'metadata' => ['cacheControl' => 'no-cache, no-store, must-revalidate'],
513 ]
514 );
515 // Re-check if the object was created successfully
516 if (!$this->exists($object_key)) {
517 $result['status'] = false;
518 $result['message'] = esc_html__('Failed to create an object for read permission check, please check service configuration', 'media-cloud-sync');
519 return [
520 'message' => $result['message'],
521 'code' => 200,
522 'success' => false,
523 'lastChecked' => $result['lastChecked'],
524 ];
525 }
526 }
527
528 $url = $this->generate_file_url($object_key);
529 $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
530
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) {
540 $result['status'] = true;
541 $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
542 } else if ($status_code === 403) {
543 $result['status'] = false;
544 if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
545 $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
546 } else {
547 $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
548 }
549 } else if ($status_code === 404) {
550 $result['status'] = false;
551 $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
552 } else if ($status_code === 500) {
553 $result['status'] = false;
554 $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
555 } else {
556 $result['status'] = false;
557 $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
558 }
559 $this->deleteSingle($object_key);
560 return [
561 'message' => $result['message'],
562 'code' => 200,
563 'success' => $result['status'],
564 'lastChecked' => $result['lastChecked'],
565 ];
566 } catch (ServiceException $ex) {
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()];
569 } catch (Exception $ex) {
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 }
576 }
577 }
578
579 /**
580 * isConfigured Function To Identify the congfigurations are correct
581 * @since 1.0.0
582 */
583 public function isConfigured(){
584 if ($this->gcloudClient) {
585 try {
586 $bucket = $this->gcloudClient->bucket($this->token . '_dummy-bucket-for-auth-check');
587 $exists = $bucket->exists(); // Triggers the API call
588 return true;
589 } catch (ServiceException $e) {
590 $statusCode = $e->getCode();
591
592 $validErrors = [200, 403, 404];
593
594 if (in_array($statusCode, $validErrors)) {
595 // If we reach here, the credentials are valid
596 return true;
597 } else {
598 // If we reach here, the credentials are not valid
599 return false;
600 }
601 }
602 }
603 return false;
604 }
605
606 /**
607 * Make Object Private
608 * @since 1.0.0
609 *
610 */
611 public function toPrivate($key) {
612 if(!$key) return false;
613 if(!$this->bucket) return false;
614
615 try {
616 $object = $this->bucket->object($key);
617 if ($object->exists()) {
618 $object->update(['acl' => []], ['predefinedAcl' => 'private']);
619 return true;
620 }
621 } catch (ServiceException $e) {
622 // Handle exception if needed
623 return false;
624 } catch (Exception $e) {
625 // Handle other exceptions if needed
626 return false;
627 }
628 return false;
629 }
630
631
632 /**
633 * Make Object Public
634 * @since 1.0.0
635 *
636 */
637 public function toPublic($key) {
638 if(!$key) return false;
639 if(!$this->bucket) return false;
640
641 try {
642 $object = $this->bucket->object($key);
643 if ($object->exists()) {
644 $object->update(['acl' => []], ['predefinedAcl' => 'publicRead']);
645 return true;
646 }
647 return false;
648 } catch (ServiceException $e) {
649 // Handle exception if needed
650 return false;
651 } catch (Exception $e) {
652 // Handle other exceptions if needed
653 return false;
654 }
655 }
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]);
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
686 /**
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
950 * @since 1.1.8
951 */
952 public function exists($key, $bucket = null) {
953 if(!$key) return false;
954
955 try {
956 $bucket = $bucket ?? $this->bucket;
957 $object = $bucket->object($key);
958 if ($object->exists()) {
959 return true;
960 } else {
961 return false;
962 }
963 } catch (ServiceException $e) {
964 // Handle exception if needed
965 return false;
966 } catch (Exception $e) {
967 // Handle other exceptions if needed
968 return false;
969 }
970
971 return false;
972 }
973
974
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 /**
1031 * Upload Single
1032 * @since 1.0.0
1033 * @return boolean
1034 */
1035 public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false){
1036 if (
1037 isset($absolute_source_path) && !empty($absolute_source_path) &&
1038 isset($relative_source_path) && !empty($relative_source_path)
1039 ) {
1040 $file_name = wp_basename( $relative_source_path );
1041 if ($file_name) {
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 }
1064
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 }
1072
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 }
1084
1085 try {
1086 $handle = fopen($absolute_source_path, 'rb');
1087 $upload = $this->bucket->upload($handle, $options);
1088
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 ];
1097 }
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 }
1113 }
1114 }
1115
1116 /**
1117 * Save object to server
1118 * @since 1.0.0
1119 */
1120 public function object_to_server($key, $save_path){
1121 if(!$this->bucket) return false;
1122 try {
1123 $object = $this->bucket->object($key);
1124 if ($object->exists()) {
1125 $object->downloadToFile($save_path);
1126 if (file_exists($save_path)) {
1127 return true;
1128 }
1129 }
1130 } catch (Exception $e) {
1131 return false;
1132 }
1133 return false;
1134 }
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 }
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
1271 /**
1272 * Delete Single
1273 * @since 1.0.0
1274 * @return boolean
1275 */
1276 public function deleteSingle($key){
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 }
1285 if (isset($key) && !empty($key)) {
1286 try {
1287 $object = $this->bucket->object($key);
1288 $object->delete();
1289
1290 if (!$object->exists()) {
1291 $result = array(
1292 'success' => true,
1293 'code' => 200,
1294 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync'),
1295 );
1296 } else {
1297 $result = array(
1298 'success' => false,
1299 'code' => 200,
1300 'message' => esc_html__('File not deleted', 'media-cloud-sync'),
1301 );
1302 }
1303 } catch (Exception $e) {
1304 $result = array(
1305 'success' => false,
1306 'code' => 200,
1307 'message' => $e->getMessage(),
1308 );
1309 }
1310 } else {
1311 $result = array(
1312 'success' => false,
1313 'code' => 200,
1314 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
1315 );
1316 }
1317 return $result;
1318 }
1319
1320
1321 /**
1322 * get private URL
1323 * @since 1.0.0
1324 * @return boolean
1325 */
1326 public function get_private_url($key) {
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 }
1335 if (isset($key) && !empty($key)) {
1336 try {
1337 $object = $this->bucket->object($key);
1338
1339 $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
1340
1341 $privateUrl = $object->signedUrl(new \DateTime(sprintf('+%s minutes', $expires)));
1342
1343 if ($privateUrl) {
1344 $result = array(
1345 'success' => true,
1346 'code' => 200,
1347 'file_url' => $privateUrl,
1348 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync'),
1349 );
1350 } else {
1351 $result = array(
1352 'success' => false,
1353 'code' => 200,
1354 'message' => esc_html__('Error getting private URL', 'media-cloud-sync'),
1355 );
1356 }
1357 } catch (Exception $e) {
1358 $result = array(
1359 'success' => false,
1360 'code' => 200,
1361 'message' => $e->getMessage(),
1362 );
1363 }
1364 } else {
1365 $result = array(
1366 'success' => false,
1367 'code' => 200,
1368 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
1369 );
1370 }
1371 return $result;
1372 }
1373
1374
1375 /**
1376 * Generate file URL
1377 */
1378 public function generate_file_url($key){
1379 $domain = $this->get_domain();
1380
1381 return apply_filters('wpmcs_generate_google_file_url',
1382 $domain . '/' . $this->bucket_name . '/' . $key,
1383 $domain, $key,
1384 $this->bucket_name
1385 );
1386 }
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 }
1396
1397 /**
1398 * Get domain URL
1399 */
1400 public function get_domain() {
1401 $url_base = 'https://storage.googleapis.com';
1402
1403 return $url_base;
1404 }
1405 }