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 / service.php

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

755 lines 26.6 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 class Service {
7 private static $instance = null;
8 private $assets_url;
9 private $version;
10 private $token;
11 private $service = false;
12 private $providers = [
13 's3' => ['class' => 'S3', 'sdk' => 's3'],
14 'gcloud' => ['class' => 'GCloud', 'sdk' => 'google'],
15 'docean' => ['class' => 'DOcean', 'sdk' => 's3'],
16 'cloudflareR2' => ['class' => 'CloudflareR2', 'sdk' => 's3'],
17 's3compatible' => ['class' => 'S3Compatible', 'sdk' => 's3'],
18 ];
19
20 protected $settings;
21
22
23 /**
24 * Service constructor.
25 * @since 1.0.0
26 */
27 public function __construct() {
28 $this->assets_url = WPMCS_ASSETS_URL;
29 $this->version = WPMCS_VERSION;
30 $this->token = WPMCS_TOKEN;
31
32 $this->settings = Utils::get_settings();
33
34 $current_service = Utils::get_service();
35
36 if($current_service) {
37 $this->service = $this->get_handler_class($current_service);
38 }
39 }
40
41 /**
42 * Verify Service Credentials
43 * @since 1.0.0
44 */
45 public function verifyCredentials($data) {
46 $result = [
47 'success' => false,
48 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
49 ];
50
51 $service = isset($data['service'])? $data['service'] : false;
52 $handler_class = $this->get_handler_class($service);
53
54 if($service == false || $handler_class == false) {
55 $result = [
56 'success' => false,
57 'message' => esc_html__('No service selected', 'media-cloud-sync')
58 ];
59 return $result;
60 }
61
62 $configSource = isset($data['configSource']) ? $data['configSource'] : 'database';
63
64 if($configSource === 'config') {
65 // Credentials come from a wp-config.php constant — WPMCS_CONFIG by default, or
66 // a caller-supplied constant (e.g. Bucket-to-Bucket's destination uses WPMCS_BTB_CONFIG).
67 $constant = !empty($data['wpConfigConstant']) ? $data['wpConfigConstant'] : 'WPMCS_CONFIG';
68 if(!Utils::is_wp_config_credentials_defined($constant)) {
69 return [
70 'success' => false,
71 /* translators: %s: wp-config.php constant name */
72 'message' => sprintf(esc_html__('%s is not defined in wp-config.php', 'media-cloud-sync'), $constant)
73 ];
74 }
75
76 $config = Utils::get_wp_config_credentials($constant);
77 $missing = [];
78 foreach(self::get_required_config_keys($service) as $key) {
79 if(!isset($config[$key]) || $config[$key] === '') {
80 $missing[] = $key;
81 }
82 }
83 if(!empty($missing)) {
84 return [
85 'success' => false,
86 /* translators: 1: wp-config.php constant name, 2: comma separated list of missing configuration keys */
87 'message' => sprintf(esc_html__('%1$s is missing key(s): %2$s', 'media-cloud-sync'), $constant, implode(', ', $missing))
88 ];
89 }
90 } else {
91 $config = $this->resolve_config($data);
92 }
93
94 return $handler_class->verifyCredentials($config);
95 }
96
97 /**
98 * True if any of the given values is empty.
99 * Shared guard for provider config-validation checks (region/access_key/
100 * secret_key/bucket_name/etc.) so each provider class doesn't reimplement
101 * the same empty() chain.
102 * @since 1.3.11
103 * @param array $fields
104 * @return bool
105 */
106 public static function has_missing_fields(array $fields) {
107 foreach ($fields as $field) {
108 if (empty($field)) {
109 return true;
110 }
111 }
112 return false;
113 }
114
115 /**
116 * Resolve the credential config for a wizard request. 'config' pulls credentials from a
117 * wp-config.php constant instead of the payload; 'existing' reuses the site's currently
118 * connected primary connection's credentials (e.g. Bucket-to-Bucket's destination form,
119 * migrating to a different bucket under the same account).
120 * @since 1.3.11
121 * @param array $data
122 * @return array
123 */
124 private function resolve_config($data) {
125 $configSource = isset($data['configSource']) ? $data['configSource'] : 'database';
126 if($configSource === 'config') {
127 $constant = !empty($data['wpConfigConstant']) ? $data['wpConfigConstant'] : 'WPMCS_CONFIG';
128 return Utils::get_wp_config_credentials($constant);
129 }
130 if($configSource === 'existing') {
131 $current = Utils::get_credentials('', []);
132 return isset($current['config']) && is_array($current['config']) ? $current['config'] : [];
133 }
134 return isset($data['config']) ? $data['config'] : [];
135 }
136
137 /**
138 * Required configuration keys per provider, used to validate the
139 * WPMCS_CONFIG constant before attempting credential verification.
140 * @since 1.3.11
141 * @param string $service
142 * @return array
143 */
144 public static function get_required_config_keys($service) {
145 $required = [
146 's3' => ['access_key', 'secret_key', 'region'],
147 'gcloud' => ['config_json'],
148 'docean' => ['access_key', 'secret_key', 'region'],
149 'cloudflareR2' => ['account_id', 'access_key', 'secret_key'],
150 's3compatible' => ['endpoint', 'access_key', 'secret_key'],
151 ];
152 return isset($required[$service]) ? $required[$service] : [];
153 }
154
155 /**
156 * Persist a connection status result and normalize the response payload.
157 * @since 1.3.11
158 * @param string $status_key
159 * @param array $result
160 * @return array
161 */
162 private function persist_connection_status($status_key, $result) {
163 $lastChecked = isset($result['lastChecked']) ? $result['lastChecked'] : time();
164 $success = !empty($result['success']);
165 $message = isset($result['message']) ? $result['message'] : '';
166
167 Utils::set_status($status_key, [
168 'status' => $success,
169 'message' => $message,
170 'lastChecked' => $lastChecked,
171 ]);
172
173 return [
174 'success' => $success,
175 'message' => $message,
176 'lastChecked' => $lastChecked,
177 ];
178 }
179
180 /**
181 * Verify saved storage credentials and bucket write access.
182 * @since 1.3.11
183 * @return array
184 */
185 private function run_storage_status_check() {
186 if(!Utils::is_service_enabled()) {
187 return $this->persist_connection_status('storageCredentials', [
188 'success' => false,
189 'message' => Utils::get_service_configuration_error(),
190 'lastChecked' => time(),
191 ]);
192 }
193
194 $credentials = Utils::get_credentials();
195 $data = [
196 'service' => isset($credentials['service']) ? $credentials['service'] : Utils::get_service(),
197 'configSource' => Utils::get_credentials_source(),
198 'config' => isset($credentials['config']) ? $credentials['config'] : [],
199 'bucketData' => [
200 'config' => isset($credentials['bucketConfig']) ? $credentials['bucketConfig'] : [],
201 ],
202 ];
203
204 $result = $this->verifyObjectWritePermission($data);
205
206 return $this->persist_connection_status('storageCredentials', $result);
207 }
208
209 /**
210 * Verify CDN / delivery read access using saved credentials.
211 * @since 1.3.11
212 * @return array
213 */
214 private function run_cdn_status_check() {
215 if(!Utils::is_service_enabled()) {
216 return $this->persist_connection_status('cdnRead', [
217 'success' => false,
218 'message' => Utils::get_service_configuration_error(),
219 'lastChecked' => time(),
220 ]);
221 }
222
223 if(!$this->service) {
224 return $this->persist_connection_status('cdnRead', [
225 'success' => false,
226 'message' => esc_html__('No service selected', 'media-cloud-sync'),
227 'lastChecked' => time(),
228 ]);
229 }
230
231 return $this->persist_connection_status('cdnRead', $this->service->verifyObjectReadPermission());
232 }
233
234 /**
235 * Run one or more saved-connection status checks.
236 * @since 1.3.11
237 * @param string $check storage|cdn|all
238 * @return array
239 */
240 public function verifyStatus($check = 'storage') {
241 $check = is_string($check) ? strtolower($check) : 'storage';
242
243 if($check === 'write') {
244 $check = 'storage';
245 } elseif($check === 'read') {
246 $check = 'cdn';
247 }
248
249 if($check === 'all') {
250 $storage = $this->run_storage_status_check();
251 $cdn = $this->run_cdn_status_check();
252
253 return [
254 'success' => !empty($storage['success']) && !empty($cdn['success']),
255 'checks' => [
256 'storageCredentials' => $storage,
257 'cdnRead' => $cdn,
258 ],
259 ];
260 }
261
262 if($check === 'cdn') {
263 return $this->run_cdn_status_check();
264 }
265
266 if($check !== 'storage') {
267 return [
268 'success' => false,
269 'message' => esc_html__('Invalid status check type', 'media-cloud-sync'),
270 ];
271 }
272
273 return $this->run_storage_status_check();
274 }
275
276 /**
277 * @deprecated 1.3.11 Use verifyStatus( 'storage' ).
278 */
279 public function verifyWrite() {
280 return $this->verifyStatus('storage');
281 }
282
283 /**
284 * @deprecated 1.3.11 Use verifyStatus( 'cdn' ).
285 */
286 public function verifyRead() {
287 return $this->verifyStatus('cdn');
288 }
289
290 /**
291 * Verify Bucket Exist
292 * @since 1.0.0
293 */
294 public function verifyBucketExist($data) {
295 $result = [
296 'success' => false,
297 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
298 ];
299
300 $service = isset($data['service'])? $data['service'] : false;
301 $handler_class = $this->get_handler_class($service);
302
303 if($service == false || $handler_class == false) {
304 $result = [
305 'success' => false,
306 'message' => esc_html__('No service selected', 'media-cloud-sync')
307 ];
308 return $result;
309 }
310
311 $config = $this->resolve_config($data);
312 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
313 $bucketConfig = isset($bucketData['config']) ? $bucketData['config'] : [];
314
315 return $handler_class->verifyBucketExist($config, $bucketConfig);
316 }
317
318 /**
319 * Verify Bucket Credentials
320 * @since 1.0.0
321 */
322 public function createBucket($data) {
323 $result = [
324 'success' => false,
325 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
326 ];
327
328 $service = isset($data['service'])? $data['service'] : false;
329 $handler_class = $this->get_handler_class($service);
330
331 if($service == false || $handler_class == false) {
332 $result = [
333 'success' => false,
334 'message' => esc_html__('No service selected', 'media-cloud-sync')
335 ];
336 return $result;
337 }
338
339 $config = $this->resolve_config($data);
340 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
341 $bucketAddNewConfig = isset($bucketData['addNewConfig']) ? $bucketData['addNewConfig'] : [];
342
343 return $handler_class->createBucket( $config, $bucketAddNewConfig );
344 }
345
346 /**
347 * Verify Object write permission
348 * @since 1.0.0
349 */
350 public function verifyObjectWritePermission($data) {
351 $result = [
352 'success' => false,
353 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
354 ];
355
356 $service = isset($data['service'])? $data['service'] : false;
357 $handler_class = $this->get_handler_class($service);
358
359 if($service == false || $handler_class == false) {
360 $result = [
361 'success' => false,
362 'message' => esc_html__('No service selected', 'media-cloud-sync')
363 ];
364 return $result;
365 }
366
367 $config = $this->resolve_config($data);
368 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
369 if(isset($bucketData['addNew']) && $bucketData['addNew']) {
370 $bucketConfig = isset($bucketData['addNewConfig']) ? $bucketData['addNewConfig'] : [];
371 } else {
372 $bucketConfig = isset($bucketData['config']) ? $bucketData['config'] : [];
373 }
374
375 return $handler_class->verifyObjectWritePermission($config, $bucketConfig);
376 }
377
378
379 /**
380 * Verify Object delete permission
381 * @since 1.0.0
382 */
383 public function verifyObjectDeletePermission($data) {
384 $result = [
385 'success' => false,
386 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
387 ];
388
389 $service = isset($data['service'])? $data['service'] : false;
390 $handler_class = $this->get_handler_class($service);
391
392 if($service == false || $handler_class == false) {
393 $result = [
394 'success' => false,
395 'message' => esc_html__('No service selected', 'media-cloud-sync')
396 ];
397 return $result;
398 }
399
400 $config = $this->resolve_config($data);
401 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
402 if(isset($bucketData['addNew']) && $bucketData['addNew']) {
403 $bucketConfig = isset($bucketData['addNewConfig']) ? $bucketData['addNewConfig'] : [];
404 } else {
405 $bucketConfig = isset($bucketData['config']) ? $bucketData['config'] : [];
406 }
407
408 return $handler_class->verifyObjectDeletePermission( $config, $bucketConfig );
409 }
410
411
412 /**
413 * Get Bucket Security Settings
414 */
415 public function getBucketSecuritySettings($data) {
416 $result = [
417 'success' => false,
418 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
419 ];
420
421 $service = isset($data['service'])? $data['service'] : false;
422 $handler_class = $this->get_handler_class($service);
423
424 if($service == false || $handler_class == false) {
425 $result = [
426 'success' => false,
427 'message' => esc_html__('No service selected', 'media-cloud-sync')
428 ];
429 return $result;
430 }
431
432 if(!method_exists($handler_class, 'getBucketSecuritySettings')) {
433 $result = [
434 'success' => false,
435 'message' => esc_html__('Service does not have getBucketSecuritySettings method', 'media-cloud-sync')
436 ];
437 return $result;
438 }
439
440 $config = $this->resolve_config($data);
441 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
442 if(isset($bucketData['addNew']) && $bucketData['addNew']) {
443 $bucketConfig = isset($bucketData['addNewConfig']) ? $bucketData['addNewConfig'] : [];
444 } else {
445 $bucketConfig = isset($bucketData['config']) ? $bucketData['config'] : [];
446 }
447
448 return $handler_class->getBucketSecuritySettings( $config, $bucketConfig );
449 }
450
451
452 /**
453 * Change Bucket Public Access
454 * @since 1.0.0
455 * @param array $data
456 */
457 public function changePublicAccess($data) {
458 $result = [
459 'success' => false,
460 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
461 ];
462
463 $service = isset($data['service'])? $data['service'] : false;
464 $handler_class = $this->get_handler_class($service);
465
466 if($service == false || $handler_class == false) {
467 $result = [
468 'success' => false,
469 'message' => esc_html__('No service selected', 'media-cloud-sync')
470 ];
471 return $result;
472 }
473
474 if(method_exists($handler_class, 'changePublicAccess') == false) {
475 $result = [
476 'success' => false,
477 'message' => esc_html__('Method not supported for this service', 'media-cloud-sync')
478 ];
479 return $result;
480 }
481
482 $config = $this->resolve_config($data);
483 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
484 if(isset($bucketData['addNew']) && $bucketData['addNew']) {
485 $bucketConfig = isset($bucketData['addNewConfig']) ? $bucketData['addNewConfig'] : [];
486 } else {
487 $bucketConfig = isset($bucketData['config']) ? $bucketData['config'] : [];
488 }
489 $value = isset($data['value']) ? $data['value'] : false;
490
491 return $handler_class->changePublicAccess( $config, $bucketConfig, $value );
492 }
493
494 /**
495 * Change bucket ownership
496 */
497 public function changeObjectOwnership($data) {
498 $result = [
499 'success' => false,
500 'message' => esc_html__('Something went wrong', 'media-cloud-sync')
501 ];
502
503 $service = isset($data['service'])? $data['service'] : false;
504 $handler_class = $this->get_handler_class($service);
505
506 if($service == false || $handler_class == false) {
507 $result = [
508 'success' => false,
509 'message' => esc_html__('No service selected', 'media-cloud-sync')
510 ];
511 return $result;
512 }
513
514 if(method_exists($handler_class, 'changeObjectOwnership') == false) {
515 $result = [
516 'success' => false,
517 'message' => esc_html__('Method not supported for this service', 'media-cloud-sync')
518 ];
519 return $result;
520 }
521
522 $config = $this->resolve_config($data);
523 $bucketData = isset($data['bucketData']) ? $data['bucketData'] : [];
524 if(isset($bucketData['addNew']) && $bucketData['addNew']) {
525 $bucketConfig = isset($bucketData['addNewConfig']) ? $bucketData['addNewConfig'] : [];
526 } else {
527 $bucketConfig = isset($bucketData['config']) ? $bucketData['config'] : [];
528 }
529 $value = isset($data['value']) ? $data['value'] : false;
530
531 return $handler_class->changeObjectOwnership( $config, $bucketConfig, $value );
532 }
533
534
535 /**
536 * Generates a URL for a given key in the cloud storage.
537 *
538 * @param string $key The key of the object in the cloud storage.
539 *
540 * @return string The URL of the object.
541 */
542 public function get_url($key) {
543 if (!$this->service) {
544 return '';
545 }
546 return $this->service->generate_file_url($key);
547 }
548
549
550 /**
551 * Checks if a given URL is from a provider.
552 *
553 * @param string $url The URL to be checked.
554 *
555 * @return bool True if the URL is from a provider, false otherwise.
556 */
557 public function is_provider_url($url) {
558 if (!$this->service) {
559 return false;
560 }
561 return $this->service->is_provider_url($url);
562 }
563
564
565 /**
566 * Get private URL
567 * @since 1.0.0
568 */
569 public function get_private_url($path) {
570 if (!$this->service) {
571 return false;
572 }
573 $url_result = $this->service->get_private_url($path);
574 if(isset($url_result['success']) && $url_result['success']) {
575 return isset($url_result['file_url']) ? $url_result['file_url'] : false;
576 }
577 return false;
578 }
579
580
581 /**
582 * Upload a single file to the cloud storage.
583 *
584 * @param string $file_path The absolute path to the file on the local server.
585 * @param string $relative_source_path The relative path to the file on the local server.
586 * @param string $prefix An optional prefix to add to the cloud storage path.
587 * @return array The result of the upload operation, including success status and any relevant messages.
588 */
589 public function uploadSingle($file_path, $relative_source_path, $prefix = '') {
590 if (!$this->service) {
591 return ['success' => false, 'code' => 200, 'message' => esc_html__('No storage service configured', 'media-cloud-sync')];
592 }
593 return $this->service->uploadSingle($file_path, $relative_source_path, $prefix);
594 }
595
596
597 public function deleteSingle($key) {
598 if (!$this->service) {
599 return ['success' => false, 'code' => 200, 'message' => esc_html__('No storage service configured', 'media-cloud-sync')];
600 }
601 return $this->service->deleteSingle($key);
602 }
603
604 /**
605 * Deletes every version/generation of $key, including delete markers — a plain
606 * deleteSingle() on a versioned bucket only removes the live copy, leaving older
607 * versions (and the storage they use) behind. Used where a key is being permanently
608 * relocated (e.g. moving a folder) and shouldn't leave anything recoverable at the old
609 * path. Safe to call on a non-versioned bucket too — it's then equivalent to deleteSingle().
610 * @since 1.3.14
611 */
612 public function purge_all_versions($key) {
613 if (!$this->service || !method_exists($this->service, 'purge_all_versions')) {
614 return ['success' => false, 'code' => 200, 'message' => esc_html__('No storage service configured', 'media-cloud-sync')];
615 }
616 return $this->service->purge_all_versions($key);
617 }
618
619
620 /**
621 * Move object to server from cloud
622 */
623 public function object_to_server($key, $save_path) {
624 if (!$this->service) {
625 return false;
626 }
627 $path_parts = pathinfo($save_path);
628 if (!file_exists($path_parts['dirname'])) {
629 mkdir($path_parts['dirname'], 0755, true);
630 }
631 return $this->service->object_to_server($key, $save_path);
632 }
633
634 /**
635 * Object bytes in memory, no local file — for callers (e.g. zip download) that need
636 * the content itself rather than a copy on the server's filesystem.
637 * @since 1.3.13
638 */
639 public function get_object_content($key) {
640 if (!$this->service) {
641 return false;
642 }
643 return $this->service->get_object_content($key);
644 }
645
646 /**
647 * Copy an object to a new path in the cloud storage
648 *
649 * @param string $key The key of the object to be copied
650 * @param string $new_path The new path to move the object to
651 * @return array The result of the copy operation
652 */
653 public function copy_to_new_path($key, $new_path) {
654 if (!$this->service) {
655 return ['success' => false, 'code' => 200, 'message' => esc_html__('No storage service configured', 'media-cloud-sync')];
656 }
657 return $this->service->copy_to_new_path($key, $new_path);
658 }
659
660 // Cross-bucket server-side copy on the singleton's own (source) connection.
661 public function copy_to_bucket($key, $new_key, $dest_bucket) {
662 if (!$this->service || !method_exists($this->service, 'copy_to_bucket')) {
663 return ['success' => false, 'code' => 200, 'message' => esc_html__('Not supported for this service', 'media-cloud-sync')];
664 }
665 return $this->service->copy_to_bucket($key, $new_key, $dest_bucket);
666 }
667
668 /**
669 * List objects in the currently configured bucket, one folder level at a time by default.
670 *
671 * @param string $prefix The folder path to list within (empty = bucket root).
672 * @param string|null $continuationToken Provider-issued token for the next page.
673 * @param int $maxKeys Page size.
674 * @param string|null $delimiter '/' for folder-level listing, null for flat/recursive.
675 * @return array {success, code, message, folders, objects, next_token}
676 */
677 public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
678 if (!$this->service) {
679 return ['success' => false, 'code' => 200, 'message' => esc_html__('No storage service configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
680 }
681 return $this->service->listObjects($prefix, $continuationToken, $maxKeys, $delimiter);
682 }
683
684 /**
685 * Get Service Handler
686 *
687 * @param string $service
688 * @param array|null $credentials Optional explicit credentials to bind the handler to,
689 * instead of the global Utils::get_credentials() default —
690 * e.g. a migration destination, independent of this
691 * singleton's own source-bound connection. Not a singleton
692 * itself — every call constructs a new SDK client; callers
693 * reusing one connection across many operations should call
694 * this once and hold onto the returned handler.
695 */
696 public function get_handler_class($service, $credentials = null) {
697 if(isset($this->providers[$service])) {
698 $provider = $this->providers[$service];
699 if(!empty($provider['sdk'])) {
700 self::load_sdk($provider['sdk']);
701 }
702 $class = __NAMESPACE__ . '\\' . $provider['class'];
703 if(class_exists($class)) {
704 return new $class($credentials);
705 }
706 }
707 return false;
708 }
709
710 /**
711 * Lazy load the bundled SDK autoloader for the given service.
712 * Each SDK is required at most once per request.
713 *
714 * @since 1.3.10
715 */
716 private static function load_sdk($sdk) {
717 static $loaded = [];
718 if (isset($loaded[$sdk])) {
719 return;
720 }
721 if ($sdk === 's3') {
722 require_once WPMCS_SDK_PATH . 's3/aws-autoloader.php';
723 } elseif ($sdk === 'google') {
724 require_once WPMCS_SDK_PATH . 'google/autoload.php';
725 } else {
726 return;
727 }
728 $loaded[$sdk] = true;
729 }
730
731 /**
732 * Get the service domain
733 *
734 */
735 public function get_domain() {
736 if (!$this->service) {
737 return '';
738 }
739 return $this->service->get_domain();
740 }
741
742 /**
743 * Ensures only one instance of Class is loaded or can be loaded.
744 *
745 * @return Service Class instance
746 * @since 1.0.0
747 * @static
748 */
749 public static function instance(){
750 if (is_null(self::$instance)) {
751 self::$instance = new self();
752 }
753 return self::$instance;
754 }
755 }