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

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

746 lines 28.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 // Libraries
7 use Dudlewebs\WPMCS\s3\Aws\S3\S3Client;
8 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
9 use Dudlewebs\WPMCS\s3\Aws\S3\Exception\S3Exception;
10 use Dudlewebs\WPMCS\s3\Aws\S3\MultipartUploader;
11 use Dudlewebs\WPMCS\s3\Aws\Exception\MultipartUploadException;
12 use Exception;
13
14 class DOcean {
15 private $assets_url;
16 private $version;
17 private $token;
18
19 protected $config;
20 protected $bucketConfig;
21 protected $settings;
22 protected $credentials;
23 protected $bucket_name;
24
25 public $service = 'docean';
26 public $DOClient = false;
27
28 /**
29 * Admin constructor.
30 * @since 1.0.0
31 */
32 public function __construct() {
33 $this->assets_url = WPMCS_ASSETS_URL;
34 $this->version = WPMCS_VERSION;
35 $this->token = WPMCS_TOKEN;
36
37 // Initialize setup
38 $this->init();
39 }
40
41 /**
42 * Initialise Client
43 */
44 public function init() {
45 $this->settings = Utils::get_settings();
46 $this->credentials = Utils::get_credentials();
47 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
48 ? $this->credentials['config']
49 : [];
50 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
51 ? $this->credentials['bucketConfig']
52 : [];
53 $this->bucket_name = isset($this->bucketConfig['bucket_name']) && !empty($this->bucketConfig['bucket_name'])
54 ? $this->bucketConfig['bucket_name']
55 : '';
56
57 if (
58 isset($this->config['region']) && !empty($this->config['region']) &&
59 isset($this->config['access_key']) && !empty($this->config['access_key']) &&
60 isset($this->config['secret_key']) && !empty($this->config['secret_key'])
61 ) {
62 $endpoint = $this->get_domain();
63
64 $this->DOClient = new S3Client([
65 'version' => '2006-03-01',
66 'region' => $this->config['region'],
67 'endpoint' => $endpoint, // DigitalOcean Spaces requires a custom endpoint
68 'use_accelerate_endpoint' => false,
69 'use_path_style_endpoint' => true, // DigitalOcean Spaces often requires path-style endpoints
70 'use_aws_shared_config_files' => false,
71 'credentials' => [
72 'key' => $this->config['access_key'],
73 'secret' => $this->config['secret_key'],
74 ],
75 ]);
76 }
77
78 }
79
80
81 /**
82 * Verify Credentials
83 * @since 1.0.0
84 * @return boolean
85 */
86 public function verifyCredentials($access_key, $secret_key, $region){
87 if (
88 isset($region) && !empty($region) &&
89 isset($access_key) && !empty($access_key) &&
90 isset($secret_key) && !empty($secret_key)
91 ) {
92 try {
93 $endpoint = $this->get_domain($region);
94
95 $DOClient = new S3Client([
96 'version' => '2006-03-01',
97 'region' => $region,
98 'endpoint' => $endpoint,
99 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
100 'use_accelerate_endpoint' => false,
101 'use_aws_shared_config_files' => false,
102 'credentials' => [
103 'key' => $access_key,
104 'secret' => $secret_key,
105 ],
106 ]);
107
108 $result = [
109 'success' => false,
110 'code' => 200,
111 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
112 ];
113
114 try {
115 $DOClient->listObjectsV2([
116 'Bucket' => $this->token . '_dummy-bucket-for-auth-check'
117 ]);
118
119 // If we reach here, the credentials are valid
120 $result = [
121 'success' => true,
122 'code' => 200,
123 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
124 ];
125 } catch (AwsException $e) {
126 $code = $e->getAwsErrorCode();
127
128 $validErrors = [
129 'AccessDenied',
130 'NoSuchBucket',
131 'AllAccessDisabled',
132 'AuthorizationHeaderMalformed',
133 'PermanentRedirect',
134 ];
135
136 if (in_array($code, $validErrors)) {
137 // If we reach here, the credentials are valid
138 $result = [
139 'success' => true,
140 'code' => 200,
141 'message' => esc_html__('Credentials are valid', 'media-cloud-sync'),
142 ];
143 }
144 }
145
146 if($result['success'] == false) {
147 return $result;
148 }
149 $result['buckets_data']['buckets'] = [];
150 $result['buckets_data']['message'] = esc_html__('Buckets listed successfully', 'media-cloud-sync');
151 $result['buckets_data']['status'] = true;
152 return $result;
153 } catch (S3Exception $ex) {
154 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
155 } catch (Exception $ex) {
156 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
157 }
158 }
159 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
160 }
161
162 /**
163 * Verify Bucket
164 * @since 1.0.0
165 * @return boolean
166 */
167 public function verifyBucketExist($access_key, $secret_key, $region, $bucket_name){
168 if (
169 isset($region) && !empty($region) &&
170 isset($access_key) && !empty($access_key) &&
171 isset($secret_key) && !empty($secret_key) &&
172 isset($bucket_name) && !empty($bucket_name)
173 ) {
174 try {
175 $endpoint = $this->get_domain($region);
176
177 $DOClient = new S3Client([
178 'version' => '2006-03-01',
179 'region' => $region,
180 'endpoint' => $endpoint,
181 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
182 'use_accelerate_endpoint' => false,
183 'use_aws_shared_config_files' => false,
184 'credentials' => [
185 'key' => $access_key,
186 'secret' => $secret_key,
187 ],
188 ]);
189
190 //get S3 object
191 $bucket_found = false;
192 try {
193 $DOClient->getObject([
194 'Bucket' => $bucket_name,
195 'Key' => $this->token . '_dummy-object-for-bucket-exist-check'
196 ]);
197 $bucket_found = true;
198 } catch (AwsException $e) {
199 $code = $e->getAwsErrorCode();
200 if ($code === 'NoSuchKey') {
201 $bucket_found = true;
202 }
203 }
204
205 if($bucket_found) {
206 return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
207 } else {
208 return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
209 }
210 }
211 catch (S3Exception $ex) {
212 return array('message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
213 } catch (Exception $ex) {
214 return array('message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false);
215 }
216 }
217 return array('message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false);
218 }
219
220 /**
221 * Create Bucket
222 * @since 1.0.0
223 * @return boolean
224 */
225 public function createBucket($access_key, $secret_key, $region, $bucket_name){
226 if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
227 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
228 }
229
230 try {
231 $endpoint = $this->get_domain($region);
232
233 $DOClient = new S3Client([
234 'version' => '2006-03-01',
235 'region' => $region,
236 'endpoint' => $endpoint,
237 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
238 'use_accelerate_endpoint' => false,
239 'use_aws_shared_config_files' => false,
240 'credentials' => [
241 'key' => $access_key,
242 'secret' => $secret_key,
243 ],
244 ]);
245
246 // Create Bucket
247 $DOClient->createBucket([
248 'Bucket' => $bucket_name,
249 ]);
250
251 // Optionally wait for bucket existence (recommended)
252 $DOClient->waitUntil('BucketExists', ['Bucket' => $bucket_name]);
253
254 try {
255 $this->putBucketPolicy($bucket_name, $DOClient);
256
257 return [
258 'message' => esc_html__('Bucket created successfully.', 'media-cloud-sync'),
259 'data' => [
260 'Name' => $bucket_name,
261 'CreationDate' => date('Y-m-d\TH:i:s\Z'),
262 ],
263 'code' => 200,
264 'success' => true,
265 ];
266
267 } catch (AwsException $ex) {
268 return ['message' => esc_html__('Bucket created. But the following error happened while setting the public access,', 'media-cloud-sync') . ' ' . $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
269 }
270 } catch (AwsException $ex) {
271 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
272 } catch (S3Exception $ex) {
273 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
274 } catch (Exception $ex) {
275 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
276 }
277 }
278
279
280 /**
281 * Add Bucket Policy
282 */
283 private function putBucketPolicy($bucket, $DOClient = false) {
284 if($DOClient == false) {
285 $DOClient = $this->DOClient;
286 }
287
288 if(empty($bucket)) return false;
289
290 $policy = json_encode([
291 "Version" => "2012-10-17",
292 "Statement" => [
293 [
294 "Effect" => "Allow",
295 "Principal" => "*",
296 "Action" => [
297 "s3:DeleteObjectTagging",
298 "s3:ListBucketMultipartUploads",
299 "s3:DeleteObjectVersion",
300 "s3:ListBucket",
301 "s3:DeleteObjectVersionTagging",
302 "s3:GetBucketAcl",
303 "s3:ListMultipartUploadParts",
304 "s3:PutObject",
305 "s3:GetObjectAcl",
306 "s3:GetObject",
307 "s3:AbortMultipartUpload",
308 "s3:DeleteObject",
309 "s3:GetBucketLocation",
310 "s3:PutObjectAcl",
311 "s3:putBucketOwnershipControls",
312 "s3:putBucketPolicy"
313 ],
314 "Resource" => [
315 "arn:aws:s3:::$bucket/*",
316 "arn:aws:s3:::$bucket"
317 ]
318 ]
319 ]
320 ]);
321
322 // Add bucket policy
323 $DOClient->putBucketPolicy(['Bucket' => $bucket, 'Policy' => $policy]);
324
325 return true;
326 }
327
328
329
330 /**
331 * Check Bucket Write Permission
332 * @since 1.0.0
333 */
334 public function verifyObjectWritePermission($access_key, $secret_key, $region, $bucket_name){
335 if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
336 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
337 }
338
339 try {
340 $endpoint = $this->get_domain($region);
341
342 $DOClient = new S3Client([
343 'version' => '2006-03-01',
344 'region' => $region,
345 'endpoint' => $endpoint,
346 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
347 'use_accelerate_endpoint' => false,
348 'use_aws_shared_config_files' => false,
349 'credentials' => [
350 'key' => $access_key,
351 'secret' => $secret_key,
352 ],
353 ]);
354
355 $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
356
357
358 // Create a dummy object to check write permission
359 $DOClient->putObject([
360 'Bucket' => $bucket_name,
361 'Key' => $object_key,
362 'Body' => 'This is a test object to check write permission.',
363 ]);
364 // Check if the object was created successfully
365 if ($DOClient->doesObjectExist($bucket_name, $object_key)) {
366 return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
367 } else {
368 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
369 }
370
371 } catch (AwsException $ex) {
372 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
373 } catch (S3Exception $ex) {
374 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
375 } catch (Exception $ex) {
376 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
377 }
378 }
379
380
381
382 /**
383 * Check Bucket Delete Permission
384 * @since 1.0.0
385 */
386 public function verifyObjectDeletePermission($access_key, $secret_key, $region, $bucket_name){
387 if (empty($region) || empty($access_key) || empty($secret_key) || empty($bucket_name)) {
388 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
389 }
390
391 try {
392 $endpoint = $this->get_domain($region);
393
394 $DOClient = new S3Client([
395 'version' => '2006-03-01',
396 'region' => $region,
397 'endpoint' => $endpoint,
398 'use_path_style_endpoint' => true, // Required for DigitalOcean Spaces
399 'use_accelerate_endpoint' => false,
400 'use_aws_shared_config_files' => false,
401 'credentials' => [
402 'key' => $access_key,
403 'secret' => $secret_key,
404 ],
405 ]);
406
407 $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
408
409 // Create a dummy object to check dlete permission
410 $DOClient->deleteObject([
411 'Bucket' => $bucket_name,
412 'Key' => $object_key,
413 ]);
414
415 // Check if the object was created successfully
416 if (!$DOClient->doesObjectExist($bucket_name, $object_key)) {
417 return ['message' => esc_html__('Bucket delete permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
418 } else {
419 return ['message' => esc_html__('Bucket delete permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
420 }
421
422 } catch (AwsException $ex) {
423 return ['message' => $ex->getAwsErrorMessage(), 'code' => 200, 'success' => false];
424 } catch (S3Exception $ex) {
425 return ['message' => $ex->getAwsErrorMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
426 } catch (Exception $ex) {
427 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
428 }
429 }
430
431 /**
432 * isConfigured Function To Identify the congfigurations are correct
433 * @since 1.0.0
434 */
435 public function isConfigured(){
436 if ($this->DOClient) {
437 try {
438 $buckets = $DOClient->listBuckets();
439 if(!empty($buckets)){
440 foreach($buckets as $bucket) {
441 if ($bucket['Name']==$this->bucket_name) {
442 return true;
443 }
444 }
445 }
446 return false;
447 } catch (AwsException $ex) {
448 return false;
449 }
450 }
451 return false;
452 }
453
454 /**
455 * Make Object Private
456 * @since 1.0.0
457 *
458 */
459 public function toPrivate($key) {
460 if(!$key) return false;
461 try {
462 $this->DOClient->putObjectAcl([
463 'Bucket' => $this->bucket_name,
464 'Key' => $key,
465 'ACL' => 'private'
466 ]);
467 return true;
468 } catch (AwsException $ex) {
469 return false;
470 }
471 return false;
472 }
473
474
475
476 /**
477 * Make Object Public
478 * @since 1.0.0
479 *
480 */
481 public function toPublic($key) {
482 if(!$key) return false;
483 try {
484 $this->DOClient->putObjectAcl([
485 'Bucket' => $this->bucket_name,
486 'Key' => $key,
487 'ACL' => 'public-read'
488 ]);
489 return true;
490 } catch (AwsException $ex) {
491 return false;
492 }
493 return false;
494 }
495
496
497
498 /**
499 * Check the object exist
500 * @since 1.1.8
501 */
502 public function exists($key) {
503 if(!$key) return false;
504
505 if($this->DOClient->doesObjectExist($this->bucket_name, $key)) {
506 return true;
507 }
508
509 return false;
510 }
511
512 /**
513 * Upload Single
514 * @since 1.0.0
515 * @return boolean
516 */
517 public function uploadSingle($media_absolute_path, $media_path, $prefix='') {
518 $result = array();
519 if (
520 isset($media_absolute_path) && !empty($media_absolute_path) &&
521 isset($media_path) && !empty($media_path)
522 ) {
523 $file_name = wp_basename( $media_path );
524 if ($file_name) {
525 $upload_path = Utils::generate_object_key($media_path, $prefix);
526
527 // Decide Multipart upload or normal put object
528 if (filesize($media_absolute_path) <= Schema::getConstant('DOCEAN_MULTIPART_MIN_FILE_SIZE')) {
529 // Upload a publicly accessible file. The file size and type are determined by the SDK.
530 try {
531 $upload = $this->DOClient->putObject([
532 'Bucket' => $this->bucket_name,
533 'Key' => $upload_path,
534 'Body' => fopen($media_absolute_path, 'r'),
535 ]);
536
537 $result = array(
538 'success' => true,
539 'code' => 200,
540 'file_url' => $this->generate_file_url($upload_path),
541 'key' => $upload_path,
542 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
543 );
544 } catch (AwsException $e) {
545 $result = array(
546 'success' => false,
547 'code' => 200,
548 'message' => $e->getMessage()
549 );
550 }
551 } else {
552 $multiUploader = new MultipartUploader($this->DOClient, $media_absolute_path, [
553 'bucket' => $this->bucket_name,
554 'key' => $upload_path,
555 ]);
556
557 try {
558 do {
559 try {
560 $uploaded = $multiUploader->upload();
561 } catch (MultipartUploadException $e) {
562 $multiUploader = new MultipartUploader($this->DOClient, $media_absolute_path, [
563 'state' => $e->getState(),
564 ]);
565 }
566 } while (!isset($uploaded));
567
568 if (isset($uploaded['ObjectURL']) && !empty($uploaded['ObjectURL'])) {
569 $result = array(
570 'success' => true,
571 'code' => 200,
572 'file_url' => $this->generate_file_url($upload_path),
573 'key' => $upload_path,
574 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync')
575 );
576 } else {
577 $result = array(
578 'success' => false,
579 'code' => 200,
580 'message' => esc_html__('Something happened while uploading to server', 'media-cloud-sync')
581 );
582 }
583 } catch (MultipartUploadException $e) {
584 $result = array(
585 'success' => false,
586 'code' => 200,
587 'message' => $e->getMessage()
588 );
589 }
590 }
591 } else {
592 $result = array(
593 'success' => false,
594 'code' => 200,
595 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync')
596 );
597 }
598 } else {
599 $result = array(
600 'success' => false,
601 'code' => 200,
602 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
603 );
604 }
605 return $result;
606 }
607
608 /**
609 * Save object to server
610 * @since 1.0.0
611 */
612 public function object_to_server($key, $save_path) {
613 try {
614 $getObject = $this->DOClient->GetObject([
615 'Bucket' => $this->bucket_name,
616 'Key' => $key,
617 'SaveAs' => $save_path
618 ]);
619 if (file_exists($save_path)) {
620 return true;
621 }
622 } catch (AwsException $e) {
623 return false;
624 }
625 return false;
626 }
627
628
629 /**
630 * Delete Single
631 * @since 1.0.0
632 * @return boolean
633 */
634 public function deleteSingle($key) {
635 $result = array();
636 if (isset($key) && !empty($key)) {
637 try {
638 $this->DOClient->deleteObject([
639 'Bucket' => $this->bucket_name,
640 'Key' => $key
641 ]);
642
643 if (!$this->DOClient->doesObjectExist($this->bucket_name, $key)) {
644 $result = array(
645 'success' => true,
646 'code' => 200,
647 'message' => esc_html__('Deleted Successfully', 'media-cloud-sync')
648 );
649 } else {
650 $result = array(
651 'success' => false,
652 'code' => 200,
653 'message' => esc_html__('File not deleted', 'media-cloud-sync')
654 );
655 }
656 } catch (AwsException $e) {
657 $result = array(
658 'success' => false,
659 'code' => 200,
660 'message' => $e->getMessage()
661 );
662 }
663 } else {
664 $result = array(
665 'success' => false,
666 'code' => 200,
667 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
668 );
669 }
670 return $result;
671 }
672
673 /**
674 * get presigned URL
675 * @since 1.0.0
676 * @return boolean
677 */
678 public function get_presigned_url($key) {
679 $result = array();
680 if (isset($key) && !empty($key)) {
681 try {
682 $cmd = $this->DOClient->getCommand('GetObject', [
683 'Bucket' => $this->bucket_name,
684 'Key' => $key
685 ]);
686
687 $expires = isset($this->settings['presigned_expire']) ? $this->settings['presigned_expire'] : 20;
688
689 $request = $this->DOClient->createPresignedRequest($cmd, sprintf('+%s minutes', $expires));
690
691 if ($presignedUrl = (string)$request->getUri()) {
692 $result = array(
693 'success' => true,
694 'code' => 200,
695 'file_url' => $presignedUrl,
696 'message' => esc_html__('Got Presigned URL Successfully', 'media-cloud-sync')
697 );
698 } else {
699 $result = array(
700 'success' => false,
701 'code' => 200,
702 'message' => esc_html__('Error getting presigned URL', 'media-cloud-sync')
703 );
704 }
705 } catch (AwsException $e) {
706 $result = array(
707 'success' => false,
708 'code' => 200,
709 'message' => $e->getMessage()
710 );
711 }
712 } else {
713 $result = array(
714 'success' => false,
715 'code' => 200,
716 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync')
717 );
718 }
719 return $result;
720 }
721
722 /**
723 * Generate file URL
724 */
725 private function generate_file_url($key){
726 $domain = $this->get_domain();
727
728 return apply_filters('wpmcs_generate_do_file_url',
729 $domain . '/' . $this->bucket_name . '/' . $key,
730 $domain,
731 $this->bucket_name,
732 $key
733 );
734 }
735
736 /**
737 * Get domain URL
738 */
739 public function get_domain($region = '') {
740 if(empty($region)) {
741 $region = isset($this->config['region']) ? $this->config['region'] : '';
742 }
743 return "https://{$region}.digitaloceanspaces.com";
744 }
745
746 }