PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / base / services / gcloud.php

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

1,107 lines 42.4 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
10 use Exception;
11
12 class GCloud {
13 private $assets_url;
14 private $version;
15 private $token;
16
17 protected $config;
18 protected $bucketConfig;
19 protected $settings;
20 protected $credentials;
21 protected $bucket_name;
22 protected $bucket; // Object
23 protected $cdnConfig;
24
25 public $service = 'gcloud';
26 public $gcloudClient = false;
27
28 /**
29 * Admin constructor.
30 * @since 1.0.0
31 */
32 public function __construct($credentials = null) {
33 $this->assets_url = WPMCS_ASSETS_URL;
34 $this->version = WPMCS_VERSION;
35 $this->token = WPMCS_TOKEN;
36
37 // Initialize setup
38 $this->init($credentials);
39 }
40
41 /**
42 * Initialise Client
43 *
44 * @param array|null $credentials Optional explicit credentials; falls back to
45 * Utils::get_credentials() when omitted.
46 */
47 public function init($credentials = null) {
48 $this->settings = Utils::get_settings();
49 $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
50 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
51 ? $this->credentials['config']
52 : [];
53 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
54 ? $this->credentials['bucketConfig']
55 : [];
56 $this->bucket_name = isset($this->bucketConfig['bucket_name']) && !empty($this->bucketConfig['bucket_name'])
57 ? $this->bucketConfig['bucket_name']
58 : '';
59 $this->cdnConfig = isset($this->credentials['cdn']) && !empty($this->credentials['cdn'])
60 ? $this->credentials['cdn']
61 : [];
62
63 if (
64 isset($this->config['config_json']) && !empty($this->config['config_json']) &&
65 isset($this->bucket_name) && !empty($this->bucket_name)
66 ) {
67 if(Utils::is_json($this->config['config_json'])){
68 // Set google client
69 $keyArray = json_decode($this->config['config_json'], true);
70
71 if (is_array($keyArray)) {
72 $this->gcloudClient = new StorageClient([
73 'keyFile' => $keyArray,
74 ]);
75 $this->bucket = $this->gcloudClient->bucket($this->bucket_name);
76 } else {
77 // Handle JSON decode failure
78 throw new \Exception('Invalid JSON provided for GCloud credentials.');
79 }
80 } else {
81 add_action('admin_notices', function (){
82 echo wp_kses_post(sprintf( "<div class='error'><p><strong>%s: </strong><br>Google Cloud Storage configuration is invalid.
83 It may break the media url's as well as media uploads.<br>
84 <a href='%s'>Re-configure</a> plugin to fix the issue.
85 </p></div>",
86 esc_html__('Media Cloud Sync', 'media-cloud-sync'),
87 admin_url('admin.php?page='.$this->token . '-admin-ui#/configure')
88 ));
89 });
90
91 }
92 }
93 }
94
95 /**
96 * Verify Credentials
97 * @since 1.0.0
98 * @return boolean
99 */
100 public function verifyCredentials( $config = [] ){
101 $config_json = isset($config['config_json']) ? $config['config_json'] : '';
102 if (!Service::has_missing_fields([$config_json])) {
103 if(!Utils::is_json($config_json)){
104 return [
105 'success' => false,
106 'code' => 200,
107 'message' => esc_html__('Invalid JSON configuration, please try again', 'media-cloud-sync'),
108 ];
109 }
110
111 try {
112 $config_array = json_decode($config_json, true);
113 if (is_array($config_array)) {
114 $googleClient = new StorageClient([
115 'keyFile' => $config_array
116 ]);
117 } else {
118 return [
119 'success' => false,
120 'code' => 200,
121 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
122 ];
123 }
124
125 $result = [
126 'success' => false,
127 'code' => 200,
128 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
129 ];
130
131 try {
132 $bucket = $googleClient->bucket($this->token . '_dummy-bucket-for-auth-check');
133 $exists = $bucket->exists(); // Triggers the API call
134
135 // If we reach here, the credentials are valid
136 $result = [
137 'success' => true,
138 'code' => 200,
139 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
140 ];
141 } catch (ServiceException $e) {
142 $statusCode = $e->getCode();
143
144 $validErrors = [200, 403, 404];
145
146 if (in_array($statusCode, $validErrors)) {
147 // If we reach here, the credentials are valid
148 $result = [
149 'success' => true,
150 'code' => 200,
151 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
152 ];
153 }
154 }
155
156 if($result['success'] == false) {
157 return $result;
158 }
159
160
161 try {
162 $buckets = $googleClient->buckets();
163 $newBucketFormat = [];
164 if(isset($buckets) && !empty($buckets)){
165 foreach($buckets as $bucket) {
166 $name = $bucket->name();
167 if(!empty($name)) {
168 // Fetch the bucket's metadata
169 $bucketInfo = $bucket->info();
170 $newBucketFormat[] = ['Name' => $name, 'CreationDate' => $bucketInfo['timeCreated']];
171 }
172 }
173 }
174 $result['buckets_data']['buckets'] = $newBucketFormat;
175 $result['buckets_data']['message'] = esc_html__('Buckets listed successfully', 'media-cloud-sync');
176 $result['buckets_data']['status'] = true;
177 } catch (Exception $e) {
178 $result ['buckets_data']['buckets'] = [];
179 $result ['buckets_data']['message'] = esc_html__('Unable to list buckets, Please check the bucket listing permission', 'media-cloud-sync');
180 $result ['buckets_data']['status'] = false;
181 }
182 return $result;
183 } catch (Exception $ex) {
184 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
185 }
186 }
187 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
188 }
189
190
191 /**
192 * Verify Bucket Exists
193 * @since 1.0.0
194 * @return boolean
195 */
196 public function verifyBucketExist( $config = [], $bucketConfig = [] ){
197 $config_json = isset($config['config_json']) ? $config['config_json'] : '';
198 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
199 if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
200 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
201 }
202
203 if ( !Utils::is_json( $config_json ) ) {
204 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
205 }
206
207 try {
208 $config_array = json_decode($config_json, true);
209 if (is_array($config_array)) {
210 $googleClient = new StorageClient([
211 'keyFile' => $config_array
212 ]);
213 } else {
214 return [
215 'success' => false,
216 'code' => 200,
217 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
218 ];
219 }
220
221 try {
222 $bucket = $googleClient->bucket($bucket_name);
223
224 if ($bucket->exists()) {
225 return [
226 'message' => esc_html__('Bucket exists', 'media-cloud-sync'),
227 'code' => 200,
228 'success' => true,
229 ];
230 } else {
231 return [
232 'message' => esc_html__('Bucket does not exist', 'media-cloud-sync'),
233 'code' => 200,
234 'success' => false,
235 ];
236 }
237 } catch (ServiceException $e) {
238 return [
239 'message' => esc_html__('Bucket does not exist or credentials are invalid: ', 'media-cloud-sync') . $e->getMessage(),
240 'code' => 200,
241 'success' => false,
242 ];
243 }
244 } catch (Exception $ex) {
245 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
246 }
247 }
248
249
250 /**
251 * Create Bucket
252 * @since 1.0.0
253 * @return boolean
254 */
255 public function createBucket( $config = [], $bucketConfig = [] ){
256 $config_json = isset($config['config_json']) ? $config['config_json'] : '';
257 $region = isset($bucketConfig['region']) ? $bucketConfig['region'] : '';
258 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
259 if ( Service::has_missing_fields([$config_json, $region, $bucket_name]) ) {
260 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
261 }
262
263 if ( !Utils::is_json( $config_json ) ) {
264 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
265 }
266
267 try {
268 $config_array = json_decode($config_json, true);
269 if (is_array($config_array)) {
270 $googleClient = new StorageClient([
271 'keyFile' => $config_array
272 ]);
273 } else {
274 return [
275 'success' => false,
276 'code' => 200,
277 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
278 ];
279 }
280
281 // Create Bucket
282 $bucket = $googleClient->createBucket($bucket_name, [
283 'location' => $region,
284 'iamConfiguration' => [
285 'uniformBucketLevelAccess' => [
286 'enabled' => true
287 ]
288 ]
289 ]);
290
291 // Fetch the bucket's IAM
292 try {
293 $iam = $bucket->iam();
294
295 $policy = $iam->policy();
296
297 // Add allUsers as a Storage Object Viewer
298 $policy['bindings'][] = [
299 'role' => 'roles/storage.objectViewer',
300 'members' => ['allUsers'],
301 ];
302
303 // Set the updated policy
304 $iam->setPolicy($policy);
305
306 } catch (ServiceException $e) {
307 return [
308 'message' => esc_html__('Bucket created successfully. But failed to set IAM policy.', 'media-cloud-sync'),
309 'data' => [
310 'Name' => $bucket_name,
311 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
312 ],
313 'code' => 200,
314 'success' => true,
315 ];
316 } catch (Exception $e) {
317 return [
318 'message' => esc_html__('Bucket created successfully. But failed to set IAM policy.', 'media-cloud-sync'),
319 'data' => [
320 'Name' => $bucket_name,
321 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
322 ],
323 'code' => 200,
324 'success' => true,
325 ];
326 }
327
328 return [
329 'message' => esc_html__('Bucket created successfully.', 'media-cloud-sync'),
330 'data' => [
331 'Name' => $bucket_name,
332 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
333 ],
334 'code' => 200,
335 'success' => true,
336 ];
337
338 } catch (Exception $ex) {
339 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
340 }
341 }
342
343 /**
344 * Check Bucket Write Permission
345 * @since 1.0.0
346 */
347 public function verifyObjectWritePermission( $config = [], $bucketConfig = [] ) {
348 $config_json = isset($config['config_json']) ? $config['config_json'] : '';
349 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
350 if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
351 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
352 }
353
354 if ( !Utils::is_json( $config_json ) ) {
355 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
356 }
357
358 try {
359 $config_array = json_decode($config_json, true);
360 if (is_array($config_array)) {
361 $googleClient = new StorageClient([
362 'keyFile' => $config_array
363 ]);
364 } else {
365 return [
366 'success' => false,
367 'code' => 200,
368 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
369 ];
370 }
371 $bucket = $googleClient->bucket($bucket_name);
372 if ($bucket->exists()) {
373 $bucket_found = true;
374 } else {
375 return ['message' => esc_html__('No Buckets found', 'media-cloud-sync'), 'code' => 200, 'success' => false];
376 }
377 if ($bucket_found) {
378 $object_key = Utils::get_permission_check_object_key();
379
380 // Prepare a temporary file with content to check write permission
381 $stream = fopen('php://temp', 'r+');
382 fwrite($stream, 'This is a test object to check write permission.');
383 rewind($stream);
384
385 // Upload the object to the bucket
386 $object = $bucket->upload(
387 $stream,
388 [
389 'name' => $object_key,
390 ]
391 );
392
393 if ($object->exists()) {
394 return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
395 } else {
396 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
397 }
398 }
399 } catch (Exception $ex) {
400 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
401 } finally {
402 if (isset($stream) && is_resource($stream)) {
403 fclose($stream);
404 }
405 }
406 }
407
408 /**
409 * Check Bucket Delete Permission
410 * @since 1.0.0
411 */
412 public function verifyObjectDeletePermission( $config = [], $bucketConfig = [] ) {
413 $config_json = isset($config['config_json']) ? $config['config_json'] : '';
414 $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
415
416 if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
417 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
418 }
419 if( !Utils::is_json( $config_json ) ) {
420 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
421 }
422
423 try {
424 $config_array = json_decode($config_json, true);
425 if (is_array($config_array)) {
426 $googleClient = new StorageClient([
427 'keyFile' => $config_array
428 ]);
429 } else {
430 return [
431 'success' => false,
432 'code' => 200,
433 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
434 ];
435 }
436
437 $bucket = $googleClient->bucket($bucket_name);
438 try {
439 $object_key = Utils::get_permission_check_object_key();
440
441 // Try fetching a dummy object to test access
442 $object = $bucket->object($object_key);
443 if ($object->exists()) {
444 $object->delete();
445 if (!$object->exists()) {
446 return [
447 'message' => esc_html__('Bucket exists', 'media-cloud-sync'),
448 'code' => 200,
449 'success' => true,
450 ];
451 } else {
452 return [
453 'message' => esc_html__('You do not have permission to delete object', 'media-cloud-sync'),
454 'code' => 200,
455 'success' => false,
456 ];
457 }
458 } else {
459 return [
460 'message' => esc_html__('Object does not exist', 'media-cloud-sync'),
461 'code' => 200,
462 'success' => false,
463 ];
464 }
465 } catch (Exception $ex) {
466 return [
467 'message' => esc_html__('Object does not exist or credentials are invalid: ', 'media-cloud-sync') . $ex->getMessage(),
468 'code' => 200,
469 'success' => false,
470 ];
471 }
472
473 } catch (Exception $ex) {
474 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
475 }
476 }
477
478 /**
479 * Check Bucket Read Permission
480 * @since 1.2.4
481 */
482 public function verifyObjectReadPermission() {
483 $result = [
484 'status' => false,
485 'message' => '',
486 'lastChecked' => time(),
487 ];
488 if (Service::has_missing_fields([$this->gcloudClient, $this->bucket_name])) {
489 $result['message'] = esc_html__('Please check the authorization details', 'media-cloud-sync');
490 return [
491 'message' => $result['message'],
492 'code' => 200,
493 'success' => false,
494 'lastChecked' => $result['lastChecked'],
495 ];
496 }
497
498 try {
499 $object_key = Utils::get_permission_check_object_key();
500
501 // Check if the object was created successfully
502 if (!$this->exists($object_key)) {
503 // Create a dummy object to check write permission
504 $stream = fopen('php://temp', 'r+');
505 fwrite($stream, 'This is a test object to check read permission.');
506 rewind($stream);
507 $this->bucket->upload(
508 $stream,
509 [
510 'name' => $object_key,
511 'metadata' => ['cacheControl' => 'no-cache, no-store, must-revalidate'],
512 ]
513 );
514 // Re-check if the object was created successfully
515 if (!$this->exists($object_key)) {
516 $result['status'] = false;
517 $result['message'] = esc_html__('Failed to create an object for read permission check, please check service configuration', 'media-cloud-sync');
518 return [
519 'message' => $result['message'],
520 'code' => 200,
521 'success' => false,
522 'lastChecked' => $result['lastChecked'],
523 ];
524 }
525 }
526
527 $url = $this->generate_file_url($object_key);
528 $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
529
530 // Never trust a cached response for this fixed, predictable URL — a stale cached
531 // error would otherwise keep failing the check long after real access is fine.
532 $no_cache_context = stream_context_create(['http' => ['header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"]]);
533 $headers = @get_headers($cdn_url, false, $no_cache_context);
534 $status_code = (is_array($headers) && !empty($headers[0]) && preg_match('/\s(\d{3})\s/', $headers[0], $matches))
535 ? (int) $matches[1]
536 : 0;
537
538 if ($status_code === 200) {
539 $result['status'] = true;
540 $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
541 } else if ($status_code === 403) {
542 $result['status'] = false;
543 if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
544 $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
545 } else {
546 $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
547 }
548 } else if ($status_code === 404) {
549 $result['status'] = false;
550 $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
551 } else if ($status_code === 500) {
552 $result['status'] = false;
553 $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
554 } else {
555 $result['status'] = false;
556 $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
557 }
558 $this->deleteSingle($object_key);
559 return [
560 'message' => $result['message'],
561 'code' => 200,
562 'success' => $result['status'],
563 'lastChecked' => $result['lastChecked'],
564 ];
565 } catch (ServiceException $ex) {
566 $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
567 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
568 } catch (Exception $ex) {
569 $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
570 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
571 } finally {
572 if (isset($stream) && is_resource($stream)) {
573 fclose($stream);
574 }
575 }
576 }
577
578 /**
579 * isConfigured Function To Identify the congfigurations are correct
580 * @since 1.0.0
581 */
582 public function isConfigured(){
583 if ($this->gcloudClient) {
584 try {
585 $bucket = $this->gcloudClient->bucket($this->token . '_dummy-bucket-for-auth-check');
586 $exists = $bucket->exists(); // Triggers the API call
587 return true;
588 } catch (ServiceException $e) {
589 $statusCode = $e->getCode();
590
591 $validErrors = [200, 403, 404];
592
593 if (in_array($statusCode, $validErrors)) {
594 // If we reach here, the credentials are valid
595 return true;
596 } else {
597 // If we reach here, the credentials are not valid
598 return false;
599 }
600 }
601 }
602 return false;
603 }
604
605 /**
606 * Make Object Private
607 * @since 1.0.0
608 *
609 */
610 public function toPrivate($key) {
611 if(!$key) return false;
612 if(!$this->bucket) return false;
613
614 try {
615 $object = $this->bucket->object($key);
616 if ($object->exists()) {
617 $object->update(['acl' => []], ['predefinedAcl' => 'private']);
618 return true;
619 }
620 } catch (ServiceException $e) {
621 // Handle exception if needed
622 return false;
623 } catch (Exception $e) {
624 // Handle other exceptions if needed
625 return false;
626 }
627 return false;
628 }
629
630
631 /**
632 * Make Object Public
633 * @since 1.0.0
634 *
635 */
636 public function toPublic($key) {
637 if(!$key) return false;
638 if(!$this->bucket) return false;
639
640 try {
641 $object = $this->bucket->object($key);
642 if ($object->exists()) {
643 $object->update(['acl' => []], ['predefinedAcl' => 'publicRead']);
644 return true;
645 }
646 return false;
647 } catch (ServiceException $e) {
648 // Handle exception if needed
649 return false;
650 } catch (Exception $e) {
651 // Handle other exceptions if needed
652 return false;
653 }
654 }
655
656
657 /**
658 * Check the object exist
659 * @since 1.1.8
660 */
661 public function exists($key, $bucket = null) {
662 if(!$key) return false;
663
664 try {
665 $bucket = $bucket ?? $this->bucket;
666 $object = $bucket->object($key);
667 if ($object->exists()) {
668 return true;
669 } else {
670 return false;
671 }
672 } catch (ServiceException $e) {
673 // Handle exception if needed
674 return false;
675 } catch (Exception $e) {
676 // Handle other exceptions if needed
677 return false;
678 }
679
680 return false;
681 }
682
683
684 /**
685 * List Objects — $delimiter = null gives a flat/recursive listing instead of one folder level.
686 * resultLimit=$maxKeys caps the iterator to this page only (Bucket::objects() would otherwise auto-paginate the whole bucket).
687 * @since 1.3.13
688 */
689 public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
690 if (!$this->bucket) {
691 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
692 }
693 try {
694 $options = [
695 'maxResults' => $maxKeys,
696 'resultLimit' => $maxKeys,
697 ];
698 if (!empty($delimiter)) {
699 $options['delimiter'] = $delimiter;
700 }
701 if (!empty($prefix)) {
702 $options['prefix'] = $prefix;
703 }
704 if (!empty($continuationToken)) {
705 $options['pageToken'] = $continuationToken;
706 }
707
708 $iterator = $this->bucket->objects($options);
709
710 $objects = [];
711 foreach ($iterator as $object) {
712 $key = $object->name();
713 if ($key === $prefix) {
714 continue; // the folder placeholder object itself, not a file
715 }
716 $info = $object->info();
717 $objects[] = [
718 'key' => $key,
719 'size' => isset($info['size']) ? (int) $info['size'] : 0,
720 'last_modified' => isset($info['updated']) ? $info['updated'] : '',
721 ];
722 }
723
724 return [
725 'success' => true,
726 'code' => 200,
727 'message' => '',
728 'folders' => $iterator->prefixes(),
729 'objects' => $objects,
730 'next_token' => $iterator->nextResultToken(),
731 ];
732 } catch (ServiceException $e) {
733 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
734 } catch (Exception $e) {
735 return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
736 }
737 }
738
739 /**
740 * Upload Single
741 * @since 1.0.0
742 * @return boolean
743 */
744 public function uploadSingle($absolute_source_path, $relative_source_path, $prefix=''){
745 if (
746 isset($absolute_source_path) && !empty($absolute_source_path) &&
747 isset($relative_source_path) && !empty($relative_source_path)
748 ) {
749 $file_name = wp_basename( $relative_source_path );
750 if ($file_name) {
751 $upload_path = Utils::generate_object_key($relative_source_path, $prefix);
752 return $this->execute_upload($absolute_source_path, $upload_path);
753 }
754 return [
755 'success' => false,
756 'code' => 200,
757 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync'),
758 ];
759 }
760 return [
761 'success' => false,
762 'code' => 200,
763 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
764 ];
765 }
766
767 /**
768 * Upload a local file to an exact destination key (no Utils::generate_object_key() derivation).
769 * @since 1.4.0
770 */
771 public function uploadObjectAtKey($absolute_source_path, $key) {
772 return $this->execute_upload($absolute_source_path, $key);
773 }
774
775 // Chunked upload above GCLOUD_MULTIPART_MIN_FILE_SIZE, single request below it — same
776 // threshold uploadSingle() always used, now shared with uploadObjectAtKey().
777 private function execute_upload($absolute_source_path, $key) {
778 $options = ['name' => $key];
779 if (filesize($absolute_source_path) > Schema::getConstant('GCLOUD_MULTIPART_MIN_FILE_SIZE')) {
780 $options['chunkSize'] = 262144 * 2;
781 }
782 $cache_control = Utils::get_cache_control_header();
783 if ($cache_control) {
784 $options['cacheControl'] = $cache_control;
785 }
786
787 try {
788 $handle = fopen($absolute_source_path, 'rb');
789 $upload = $this->bucket->upload($handle, $options);
790
791 if ($upload->exists()) {
792 return [
793 'success' => true,
794 'code' => 200,
795 'file_url' => $this->generate_file_url($key),
796 'key' => $key,
797 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
798 ];
799 }
800 return [
801 'success' => false,
802 'code' => 200,
803 'message' => esc_html__('Object not found at server.', 'media-cloud-sync'),
804 ];
805 } catch (Exception $e) {
806 return [
807 'success' => false,
808 'code' => 200,
809 'message' => $e->getMessage(),
810 ];
811 } finally {
812 if (isset($handle) && is_resource($handle)) {
813 fclose($handle);
814 }
815 }
816 }
817
818 /**
819 * Save object to server
820 * @since 1.0.0
821 */
822 public function object_to_server($key, $save_path){
823 if(!$this->bucket) return false;
824 try {
825 $object = $this->bucket->object($key);
826 if ($object->exists()) {
827 $object->downloadToFile($save_path);
828 if (file_exists($save_path)) {
829 return true;
830 }
831 }
832 } catch (Exception $e) {
833 return false;
834 }
835 return false;
836 }
837
838 /**
839 * Object bytes in memory, no local file — for callers (e.g. zip download) that need
840 * the content itself rather than a copy on the server's filesystem.
841 * @since 1.3.13
842 */
843 public function get_object_content($key) {
844 if(!$this->bucket) return false;
845 try {
846 $object = $this->bucket->object($key);
847 if ($object->exists()) {
848 return $object->downloadAsString();
849 }
850 } catch (Exception $e) {
851 return false;
852 }
853 return false;
854 }
855
856 /**
857 * Deletes the live generation, then best-effort purges every prior generation too — a
858 * bucket with Object Versioning enabled otherwise keeps old generations (and the storage
859 * they use) around at the old key. The live delete happens unconditionally first, in its
860 * own try/catch, so the object still ends up gone even if the generation-listing call
861 * below fails for any reason.
862 * @since 1.3.14
863 */
864 public function purge_all_versions($key) {
865 if (!$this->bucket) {
866 return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
867 }
868
869 try {
870 $this->bucket->object($key)->delete();
871 } catch (ServiceException $e) {
872 return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
873 } catch (\Exception $e) {
874 return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
875 }
876
877 // Best-effort only from here — the live copy above is already gone regardless of
878 // whether this bucket has Object Versioning enabled or this call succeeds.
879 try {
880 foreach ($this->bucket->objects(['prefix' => $key, 'versions' => true]) as $object) {
881 if ($object->name() === $key) {
882 $object->delete();
883 }
884 }
885 } catch (ServiceException $e) {
886 // Generation history cleanup failed — not fatal, live object is gone.
887 } catch (\Exception $e) {
888 // Generation history cleanup failed — not fatal, live object is gone.
889 }
890
891 return ['success' => true, 'code' => 200, 'message' => esc_html__('Purged Successfully', 'media-cloud-sync')];
892 }
893
894
895 /**
896 * Copy an object to a new path in Google Cloud Storage
897 *
898 * @param string $key Original object key (path in bucket)
899 * @param string $new_path Destination object key
900 * @return bool True if object was copied successfully, false otherwise
901 * @since 1.3.4
902 */
903 // Trusts copy()'s own success/failure rather than pre/post-verifying with extra
904 // exists() calls — each one is a full network round-trip, and with move/copy processing
905 // keys sequentially, extra round-trips per file add up fast on a folder with many files.
906 // copy() itself throws (caught below) if the source is missing or the copy otherwise
907 // fails, so nothing is lost by not checking first.
908 public function copy_to_new_path($key, $new_path) {
909 if (!$this->bucket) {
910 return [
911 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
912 'code' => 200,
913 'success' => false
914 ];
915 }
916 try {
917 $sourceObject = $this->bucket->object($key);
918 $sourceObject->copy($this->bucket, ['name' => $new_path]);
919 return [
920 'success' => true,
921 'code' => 200,
922 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
923 ];
924 } catch (ServiceException $e) {
925 return [
926 'success' => false,
927 'code' => 200,
928 'message' => $e->getMessage()
929 ];
930 } catch (\Exception $e) {
931 return [
932 'success' => false,
933 'code' => 200,
934 'message' => $e->getMessage()
935 ];
936 }
937 }
938
939 // Like copy_to_new_path() but into an explicit (possibly different) bucket — needs write
940 // access there too, so callers should fall back to download+upload on failure.
941 public function copy_to_bucket($key, $new_key, $dest_bucket) {
942 if (!$this->bucket || !$this->gcloudClient) {
943 return [
944 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
945 'code' => 200,
946 'success' => false
947 ];
948 }
949 try {
950 $sourceObject = $this->bucket->object($key);
951 $sourceObject->copy($this->gcloudClient->bucket($dest_bucket), ['name' => $new_key]);
952 return [
953 'success' => true,
954 'code' => 200,
955 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
956 ];
957 } catch (ServiceException $e) {
958 return [
959 'success' => false,
960 'code' => 200,
961 'message' => $e->getMessage()
962 ];
963 } catch (\Exception $e) {
964 return [
965 'success' => false,
966 'code' => 200,
967 'message' => $e->getMessage()
968 ];
969 }
970 }
971
972
973 /**
974 * Delete Single
975 * @since 1.0.0
976 * @return boolean
977 */
978 public function deleteSingle($key){
979 $result = array();
980 if (!$this->bucket) {
981 return array(
982 'success' => false,
983 'code' => 200,
984 'message' => esc_html__('Client not configured', 'media-cloud-sync')
985 );
986 }
987 if (isset($key) && !empty($key)) {
988 try {
989 $object = $this->bucket->object($key);
990 $object->delete();
991
992 if (!$object->exists()) {
993 $result = array(
994 'success' => true,
995 'code' => 200,
996 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync'),
997 );
998 } else {
999 $result = array(
1000 'success' => false,
1001 'code' => 200,
1002 'message' => esc_html__('File not deleted', 'media-cloud-sync'),
1003 );
1004 }
1005 } catch (Exception $e) {
1006 $result = array(
1007 'success' => false,
1008 'code' => 200,
1009 'message' => $e->getMessage(),
1010 );
1011 }
1012 } else {
1013 $result = array(
1014 'success' => false,
1015 'code' => 200,
1016 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
1017 );
1018 }
1019 return $result;
1020 }
1021
1022
1023 /**
1024 * get private URL
1025 * @since 1.0.0
1026 * @return boolean
1027 */
1028 public function get_private_url($key) {
1029 $result = array();
1030 if (!$this->bucket) {
1031 return array(
1032 'success' => false,
1033 'code' => 200,
1034 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1035 );
1036 }
1037 if (isset($key) && !empty($key)) {
1038 try {
1039 $object = $this->bucket->object($key);
1040
1041 $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
1042
1043 $privateUrl = $object->signedUrl(new \DateTime(sprintf('+%s minutes', $expires)));
1044
1045 if ($privateUrl) {
1046 $result = array(
1047 'success' => true,
1048 'code' => 200,
1049 'file_url' => $privateUrl,
1050 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync'),
1051 );
1052 } else {
1053 $result = array(
1054 'success' => false,
1055 'code' => 200,
1056 'message' => esc_html__('Error getting private URL', 'media-cloud-sync'),
1057 );
1058 }
1059 } catch (Exception $e) {
1060 $result = array(
1061 'success' => false,
1062 'code' => 200,
1063 'message' => $e->getMessage(),
1064 );
1065 }
1066 } else {
1067 $result = array(
1068 'success' => false,
1069 'code' => 200,
1070 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
1071 );
1072 }
1073 return $result;
1074 }
1075
1076
1077 /**
1078 * Generate file URL
1079 */
1080 public function generate_file_url($key){
1081 $domain = $this->get_domain();
1082
1083 return apply_filters('wpmcs_generate_google_file_url',
1084 $domain . '/' . $this->bucket_name . '/' . $key,
1085 $domain, $key,
1086 $this->bucket_name
1087 );
1088 }
1089
1090 /**
1091 * Is provider URL
1092 * @since 1.3.6
1093 */
1094 public function is_provider_url($url) {
1095 $domain = $this->get_domain();
1096 return (strpos($url, $domain . '/' . $this->bucket_name . '/') !== false);
1097 }
1098
1099 /**
1100 * Get domain URL
1101 */
1102 public function get_domain() {
1103 $url_base = 'https://storage.googleapis.com';
1104
1105 return $url_base;
1106 }
1107 }