PluginProbe
Media Cloud Sync / 1.3.10
Media Cloud Sync v1.3.10
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 / config / utils.php

utils.php in Media Cloud Sync 1.3.10, at includes/config/utils.php

990 lines 30.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS;
4
5 defined('ABSPATH') || exit;
6
7 class Utils {
8 /**
9 * Check a variable is empty
10 * @since 1.0.0
11 * @param string|integer|array|float
12 * @return boolean
13 */
14 public static function is_empty($var){
15 if (is_array($var)) {
16 return empty($var);
17 } else {
18 return ($var === null || $var === false || $var === '');
19 }
20 }
21
22 /**
23 * Function To get Plugin Specific Wordpress Option
24 * @since 1.0.0
25 * @return array|boolean|string|integer|float|double
26 */
27 public static function get_option($key, $default = false, $meta_name = false, $expire = false){
28 $data = Cache::get_object_cache( $key, false, $meta_name, $expire );
29 return $data == false ? $default : $data;
30 }
31
32 /**
33 * Function To update Plugin Specific Wordpress Option
34 * @since 1.0.0
35 * @return boolean
36 */
37 public static function update_option($key, $options, $meta_name = false, $expire = false){
38 return Cache::set_object_cache( $key, $options, false, $meta_name, $expire );
39 }
40
41 /**
42 * Function To delete Plugin Specific Wordpress Option
43 * @since 1.0.0
44 * @return boolean
45 */
46 public static function delete_option($key, $meta_name = false){
47 return Cache::delete_object_cache( $key, false, $meta_name );
48 }
49
50 /**
51 * Function To get Plugin Specific Wordpress post meta
52 * @since 1.0.0
53 * @return array|boolean|string|integer|float|double
54 */
55 public static function get_meta($post_id, $key, $default = false, $meta_name = false, $expire = false){
56 $data = Cache::get_object_cache( $key, $post_id, $meta_name, $expire );
57 return $data == false ? $default : $data;
58 }
59
60 /**
61 * Get Post Meta Data By Query
62 * @since 1.0.0
63 * @return boolean
64 */
65 public static function get_post_meta($post_id, $key, $single=false, $db_query=false){
66 global $wpdb;
67 if(!(!empty($key) || $post_id)) return false;
68
69 if($db_query) {
70 $meta_data = $wpdb->get_row( "SELECT meta_value FROM $wpdb->postmeta WHERE post_id=$post_id AND meta_key='$key'" );
71 if ($wpdb->last_error || null === $meta_data || !isset($meta_data)) {
72 return false;
73 }
74 return $meta_data->meta_value;
75 } else {
76 return get_post_meta( $post_id, $key, $single );
77 }
78 }
79
80 /**
81 * Get Option Data By Query
82 * @since 1.0.0
83 * @return boolean
84 */
85 public static function get_option_meta($key, $single=false, $db_query=false){
86 global $wpdb;
87 if(!(!empty($key))) return false;
88
89 if($db_query) {
90 $meta_data = $wpdb->get_row( "SELECT option_value FROM $wpdb->options WHERE option_name='$key'" );
91 if ($wpdb->last_error || null === $meta_data || !isset($meta_data)) {
92 return false;
93 }
94 return $meta_data->option_value;
95 } else {
96 return get_option( $key, $single );
97 }
98 }
99
100
101 /**
102 * Function To update Plugin Specific Wordpress post meta
103 * @since 1.0.0
104 * @return boolean
105 */
106 public static function update_meta($post_id, $key, $options, $meta_name = false, $expire = false){
107 return Cache::set_object_cache( $key, $options, $post_id, $meta_name, $expire );
108 }
109
110 /**
111 * Function To delete Plugin Specific Wordpress post meta
112 * @since 1.0.0
113 * @return boolean
114 */
115 public static function delete_meta($post_id, $key, $meta_name = false){
116 return Cache::delete_object_cache( $key, $post_id, $meta_name );
117 }
118
119 /**
120 * Function To get Plugin Specific Wordpress user meta
121 * @since 1.0.0
122 * @return array|boolean|string|integer|float|double
123 */
124 public static function get_user_meta($post_id, $key, $default = false, $meta_name = false, $expire = false){
125 $data = Cache::get_object_cache( $key, $post_id, $meta_name, $expire, true );
126 return $data == false ? $default : $data;
127 }
128
129 /**
130 * Function To update Plugin Specific Wordpress user meta
131 * @since 1.0.0
132 * @return boolean
133 */
134 public static function update_user_meta($post_id, $key, $options, $meta_name = false, $expire = false){
135 return Cache::set_object_cache( $key, $options, $post_id, $meta_name, $expire, true );
136 }
137
138 /**
139 * Function To delete Plugin Specific Wordpress user meta
140 * @since 1.0.0
141 * @return boolean
142 */
143 public static function delete_user_meta($post_id, $key, $meta_name = false){
144 return Cache::delete_object_cache( $key, $post_id, $meta_name, true );
145 }
146
147
148 /**
149 * Clear meta from database
150 */
151 public static function clear_all_meta($meta_name = false, $meta_table = 'all') {
152 global $wpdb;
153
154 $meta_name = $meta_name == false || empty($meta_name) ? Schema::getConstant('META_KEY') : $meta_name;
155
156 if ( empty( $meta_name ) ) {
157 return false; // Avoid accidental deletions if the meta_key is empty
158 }
159
160 $meta_tables = $meta_table == 'all' ? ['postmeta', 'usermeta', 'options'] : [$meta_table];
161
162 if( in_array('postmeta', $meta_tables) ) {
163 // Clear post meta
164 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s", $meta_name ) );
165 }
166
167 if( in_array('usermeta', $meta_tables) ) {
168 // Clear user meta
169 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE meta_key = %s", $meta_name ) );
170 }
171
172 if( in_array('options', $meta_tables) ) {
173 // Clear options
174 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name = %s", $meta_name ) );
175 }
176
177 // Clear object cache
178 Cache::flush_object_cache();
179 }
180
181 /**
182 * Clears all content meta from the database
183 *
184 * @param string $meta_name Optional. The meta key to clear. Defaults to the constant CONTENT_META_KEY.
185 */
186 public static function clear_all_content_meta($meta_name = false) {
187 global $wpdb;
188 $meta_name = $meta_name == false || empty($meta_name) ? Schema::getConstant('CONTENT_META_KEY') : $meta_name;
189
190 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s", $meta_name ) );
191 Cache::flush_object_cache();
192 }
193
194
195
196 /**
197 * Function To get Current credentials
198 * @since 1.0.0
199 * @return array|boolean|string|integer|float|double
200 */
201 public static function get_credentials($option='', $default=false, $masked_config = false){
202 $current_setttings = self::get_option('credentials',[], Schema::getConstant('GLOBAL_SETTINGS_KEY'));
203 if(isset($current_setttings) && !empty($current_setttings)){
204 if(isset($option) && !empty($option)){
205 if(isset($current_setttings[$option])) {
206 if($masked_config && $option == 'config'){
207 $current_setttings[$option] = self::mask_config($current_setttings[$option]);
208 }
209 return $current_setttings[$option];
210 } else {
211 return $default;
212 }
213 } else {
214 if($masked_config && isset($current_setttings['config'])){
215 $current_setttings['config'] = self::mask_config($current_setttings['config']);
216 }
217 return $current_setttings;
218 }
219 } else {
220 return $default;
221 }
222 }
223
224 /**
225 * Mask Config
226 * @since 1.2.13
227 * @return array|boolean|string|integer|float|double
228 */
229 public static function mask_config($config){
230 foreach ($config as $key => $value) {
231 if (in_array($key, ['config_json', 'secret_key'])) {
232 $config[$key] = substr($value, 0, 4) . self::mask_string(substr($value, 4));
233 }
234 }
235 return $config;
236 }
237
238 /**
239 * Mask String
240 * @since 1.2.13
241 * @return array|boolean|string|integer|float|double
242 */
243 public static function mask_string($string){
244 return str_repeat('*', strlen($string));
245 }
246
247 /**
248 * Function To get Current settings
249 * @since 1.0.0
250 * @return array|boolean|string|integer|float|double
251 */
252 public static function get_settings($option='', $default=false){
253 $current_setttings = self::get_option('settings',[], Schema::getConstant('GLOBAL_SETTINGS_KEY'));
254 if(isset($current_setttings) && !empty($current_setttings)){
255 if(isset($option) && !empty($option)){
256 if(isset($current_setttings[$option])) {
257 return $current_setttings[$option];
258 } else {
259 return $default;
260 }
261 } else {
262 return $current_setttings;
263 }
264 } else {
265 return $default;
266 }
267 }
268
269 /**
270 * Function To get Current statuses
271 * @since 1.0.0
272 * @return array|boolean|string|integer|float|double
273 */
274 public static function get_status($option='', $default=false){
275 $current_setttings = self::get_option('status',[], Schema::getConstant('STATUS_KEY'));
276 if(isset($current_setttings) && !empty($current_setttings)){
277 if(isset($option) && !empty($option)){
278 if(isset($current_setttings[$option])) {
279 return $current_setttings[$option];
280 } else {
281 return $default;
282 }
283 } else {
284 return $current_setttings;
285 }
286 } else {
287 return $default;
288 }
289 }
290
291 /**
292 * Function To set statuses
293 * @since 1.0.0
294 *
295 */
296 public static function set_status($option='', $data=[]){
297 $current_setttings = self::get_option('status',[], Schema::getConstant('STATUS_KEY'));
298 if(isset($option) && !empty($option)){
299 $current_setttings[$option] = $data;
300 return self::update_option('status', $current_setttings, Schema::getConstant('STATUS_KEY'));
301 } else {
302 return false;
303 }
304 }
305
306 /**
307 * Function To get Current Service
308 * @since 1.0.0
309 * @return array|boolean|string|integer|float|double
310 */
311 public static function get_service(){
312 $current_service = self::get_credentials('service', '');
313 if(isset($current_service) && !empty($current_service)){
314 return $current_service;
315 } else {
316 return false;
317 }
318 }
319
320 /**
321 * Function To get Current config
322 * @since 1.0.0
323 * @return array|boolean|string|integer|float|double
324 */
325 public static function get_config($option='', $default=false){
326 $current_setttings = self::get_credentials('config',[]);
327 if(isset($current_setttings) && !empty($current_setttings)){
328 if(isset($option) && !empty($option)){
329 if(isset($current_setttings[$option])) {
330 return $current_setttings[$option];
331 } else {
332 return $default;
333 }
334 } else {
335 return $current_setttings;
336 }
337 } else {
338 return $default;
339 }
340 }
341
342 /**
343 * Function to check serving media environment is ok
344 * @since 1.0.0
345 * @return boolean
346 */
347 public static function is_ok_to_serve($attachment_id = false, $check_id = true){
348 return (
349 self::get_service() &&
350 self::get_settings('rewrite_url') &&
351 ( $check_id ? isset($attachment_id) && !empty($attachment_id) : true )
352 );
353 }
354
355 /**
356 * Function to check uploading media environment is ok
357 * @since 1.0.0
358 * @return boolean
359 */
360 public static function is_ok_to_upload($attachment_id = false){
361 return (
362 self::get_service() &&
363 self::get_settings('copy_to_bucket') &&
364 isset($attachment_id) && !empty($attachment_id)
365 );
366 }
367
368 /**
369 * Function To check service is enabled
370 * @since 1.0.0
371 * @return array|boolean|string|integer|float|double
372 */
373 public static function is_service_enabled(){
374 return !!self::get_service();
375 }
376
377 /**
378 * Check whether a file exist in a list of files
379 * @since 1.0.1
380 * @return boolean
381 */
382 public static function check_existing_file_names( $filename, $files ) {
383 $fname = pathinfo( $filename, PATHINFO_FILENAME );
384 $ext = pathinfo( $filename, PATHINFO_EXTENSION );
385
386 // Edge case, file names like `.ext`.
387 if ( empty( $fname ) ) {
388 return false;
389 }
390
391 if ( $ext ) {
392 $ext = ".$ext";
393 }
394
395 $regex = '/^' . preg_quote( $fname ) . '-(?:\d+x\d+|scaled|rotated)' . preg_quote( $ext ) . '$/i';
396
397 foreach ( $files as $file ) {
398 if (
399 preg_match( $regex, wp_basename($file) ) ||
400 $filename == $file
401 ) {
402 return true;
403 }
404 }
405
406 return false;
407 }
408
409
410 /**
411 * Get relative attachment path for local source or remote object key.
412 *
413 * @param string $file File path, URL, or object key
414 * @param string $type 'source' (local WP) or 'key' (cloud / CDN)
415 *
416 * @return string|false
417 */
418 public static function get_attachment_source_path( $file, $type = 'source' ) {
419 if ( empty( $file ) || ! is_string( $file ) ) {
420 return false;
421 }
422
423 // Normalize slashes early
424 $file = str_replace( '\\', '/', $file );
425
426 /**
427 * -------------------------------------------------
428 * TYPE: SOURCE (WordPress local paths / URLs)
429 * -------------------------------------------------
430 */
431 if ( $type === 'source' ) {
432
433 $uploads = wp_get_upload_dir();
434 if ( empty( $uploads ) || ! empty( $uploads['error'] ) ) {
435 return false;
436 }
437
438 $basedir = str_replace( '\\', '/', $uploads['basedir'] );
439 $baseurl = str_replace( '\\', '/', $uploads['baseurl'] );
440
441 // If URL → extract path
442 if ( filter_var( $file, FILTER_VALIDATE_URL ) ) {
443 $parsed = wp_parse_url( $file );
444 $file = $parsed['path'] ?? '';
445 }
446
447 // Strip WordPress upload root
448 if ( 0 === strpos( $file, $basedir ) ) {
449 $file = substr( $file, strlen( $basedir ) );
450 } elseif ( 0 === strpos( $file, $baseurl ) ) {
451 $file = substr( $file, strlen( $baseurl ) );
452 }
453 }
454
455 /**
456 * -------------------------------------------------
457 * TYPE: KEY (Cloud / CDN paths or URLs)
458 * -------------------------------------------------
459 */
460 elseif ( $type === 'key' ) {
461
462 // URL → extract path only
463 if ( filter_var( $file, FILTER_VALIDATE_URL ) ) {
464 $parsed = wp_parse_url( $file );
465 $file = $parsed['path'] ?? '';
466 }
467
468 $file = ltrim( $file, '/' );
469
470 $enable_base_path = self::get_settings( 'enable_base_path', true );
471 $base_path = trim( (string) self::get_settings( 'base_path', '' ), '/' );
472
473 /**
474 * If base_path is enabled and exists as a FULL segment,
475 * strip everything before it.
476 */
477 if ( $enable_base_path && $base_path !== '' ) {
478 $pattern = '#(^|/)' . preg_quote( $base_path, '#' ) . '(/|$)#';
479
480 if ( preg_match( $pattern, $file, $m, PREG_OFFSET_CAPTURE ) ) {
481 $file = substr( $file, $m[0][1] );
482 }
483 }
484 }
485
486 // Final cleanup
487 $file = trim( $file, '/' );
488
489 /**
490 * Reject directory-only paths
491 */
492 if ( $file === '' || substr( $file, -1 ) === '/' ) {
493 return false;
494 }
495
496 return apply_filters(
497 'wpmcs_get_relative_file_path_from_upload_directory',
498 $file,
499 $type
500 );
501 }
502
503
504 /**
505 * Whether the file may be synced based on plugin extension settings only.
506 *
507 * Uses `extensions_exclude` to block listed extensions and optional `extensions_include` as an allow-list.
508 * When `extensions_include` is empty, no extension is blocked by the allow-list (only exclude applies).
509 * WordPress MIME / `wp_check_filetype` is not used here.
510 *
511 * @since 1.0.0
512 * @param string $path Absolute or relative file path.
513 * @return bool
514 */
515 public static function is_extension_available( $path ) {
516 $settings = self::get_settings();
517 $path_parts = pathinfo( $path );
518
519 if ( ! isset( $path_parts['basename'] ) || $path_parts['basename'] === '' ) {
520 return false;
521 }
522
523 $ext = isset( $path_parts['extension'] ) ? strtolower( $path_parts['extension'] ) : '';
524
525 $allowed = [];
526 if ( isset( $settings['extensions_include'] ) && is_array( $settings['extensions_include'] ) ) {
527 $allowed = array_map( 'strtolower', array_filter( $settings['extensions_include'], 'strlen' ) );
528 }
529
530 $not_allowed = [];
531 if ( isset( $settings['extensions_exclude'] ) && is_array( $settings['extensions_exclude'] ) ) {
532 $not_allowed = array_map( 'strtolower', array_filter( $settings['extensions_exclude'], 'strlen' ) );
533 }
534
535 if ( in_array( $ext, $not_allowed, true ) ) {
536 return false;
537 }
538
539 if ( ! empty( $allowed ) && ! in_array( $ext, $allowed, true ) ) {
540 return false;
541 }
542
543 return true;
544 }
545
546 /**
547 * Generate prefix for object versioning
548 * @since 1.0.0
549 * @return string
550 */
551 public static function generate_object_versioning_prefix(){
552 $year_month = self::get_settings('year_month');
553 $date_format = $year_month ? 'dHis' : 'YmdHis';
554
555 // Use current time so that object version is unique
556 $time = current_time('timestamp');
557
558 $object_version = date($date_format, $time) . '/';
559 $object_version = apply_filters('wpmcs_object_version_prefix', $object_version);
560
561 return $object_version;
562 }
563
564
565 /**
566 * Generate Key for Objects
567 * @since 1.0.0
568 */
569 public static function generate_object_key($relative_source_path, $prefix) {
570 $upload_path = '';
571 $enable_base_path = self::get_settings('enable_base_path', true);
572 $base_path = self::get_settings('base_path', 'wp-content/uploads');
573 $year_month = self::get_settings('year_month', true);
574 $relative_source_path = ltrim( $relative_source_path, '/' );
575 $file_name = wp_basename( $relative_source_path );
576
577 if(!$enable_base_path) { // If base path is not enabled
578 $base_path = '';
579 }
580
581 $keep_original_folder_structure = apply_filters( 'wpmcs_keep_original_folder_structure', false );
582
583 if(isset($base_path) && !empty($base_path)) {
584 $upload_path.= preg_replace('~/+~', '/',
585 str_replace('\\', '/',
586 trim($base_path," \n\r\t\v\x00\/ ")
587 )
588 );
589 }
590
591 if($keep_original_folder_structure) {
592 $object_key = ltrim($upload_path . '/' . dirname( $relative_source_path ) . '/' . $prefix . $file_name, '/');
593 } else {
594 if(isset($year_month) && $year_month) {
595 $year_month_prefix = self::get_year_month_from_file_path($relative_source_path);
596 if($year_month_prefix) {
597 $upload_path.= '/'.$year_month_prefix;
598 } else {
599 $upload_path.= '/'.date("Y/m");
600 }
601 }
602
603 $object_key = ltrim($upload_path.'/'.$prefix.$file_name, '/');
604 }
605
606 return apply_filters( 'wpmcs_object_key', $object_key, $relative_source_path, $prefix );
607 }
608
609
610 /**
611 * Check if a file path or URL follows the year/month structure and return the year/month as a string.
612 *
613 * @param string $path_or_url The relative path, absolute path, or URL to check.
614 * @return string|false The year/month string if valid, false otherwise.
615 */
616 public static function get_year_month_from_file_path( $path_or_url ) {
617 // Regex pattern to match paths and URLs containing 'YYYY/MM/' at any depth, allowing subdirectories afterward
618 $pattern = '#(?:^|/)(\d{4})/(0[1-9]|1[0-2])/[^/]+(?:/[^/]+)*$#';
619
620 // Check if the input matches the pattern
621 if ( preg_match( $pattern, $path_or_url, $matches ) ) {
622 // Return the year/month as a string in the format 'YYYY/MM'
623 return $matches[1] . '/' . $matches[2];
624 }
625
626 return false; // Invalid format
627 }
628
629
630 /**
631 * Maybe convert size to string
632 *
633 * @param int $attachment_id
634 * @param mixed $size
635 *
636 * @return null|string
637 */
638 public static function maybe_convert_size_to_string( $attachment_id, $size ) {
639 if ( is_array( $size ) ) {
640 $width = ( isset( $size[0] ) && $size[0] > 0 ) ? $size[0] : 1;
641 $height = ( isset( $size[1] ) && $size[1] > 0 ) ? $size[1] : 1;
642 $original_aspect_ratio = $width / $height;
643 $meta = wp_get_attachment_metadata( $attachment_id );
644
645 if ( ! isset( $meta['sizes'] ) || empty( $meta['sizes'] ) ) {
646 return false;
647 }
648
649 $sizes = $meta['sizes'];
650 uasort( $sizes, function ( $a, $b ) {
651 // Order by image area
652 return ( $a['width'] * $a['height'] ) - ( $b['width'] * $b['height'] );
653 } );
654
655 $near_matches = array();
656
657 foreach ( $sizes as $size => $value ) {
658 if ( $width > $value['width'] || $height > $value['height'] ) {
659 continue;
660 }
661 $aspect_ratio = $value['width'] / $value['height'];
662 if ( $aspect_ratio === $original_aspect_ratio ) {
663 return $size;
664 }
665 $near_matches[] = $size;
666 }
667 // Return nearest match
668 if ( ! empty( $near_matches ) ) {
669 return $near_matches[0];
670 }
671 }
672
673 return $size;
674 }
675
676 /**
677 * Reduce the given URL down to the simplest version of itself.
678 *
679 * Useful for matching against the full version of the URL in a full-text search
680 * or saving as a key for dictionary type lookup.
681 *
682 * @param string $url
683 *
684 * @return string
685 */
686 public static function reduce_url( $url ) {
687 $parts = static::parse_url( $url );
688 $host = isset( $parts['host'] ) ? $parts['host'] : '';
689 $port = isset( $parts['port'] ) ? ":{$parts['port']}" : '';
690 $path = isset( $parts['path'] ) ? $parts['path'] : '';
691
692 return '//' . $host . $port . $path;
693 }
694
695 /**
696 * Remove scheme from URL.
697 *
698 * @param string $url
699 * @return string
700 */
701 public static function remove_scheme( $url ) {
702 return preg_replace( '/^(?:http|https):/', '', $url );
703 }
704
705 /**
706 * Remove size from filename (image[-100x100].jpeg).
707 *
708 * @param string $url
709 * @param bool $remove_extension
710 *
711 * @return string
712 */
713 public static function remove_size_from_filename( $url, $remove_extension = false ) {
714 $url = preg_replace( '/^(\S+)-[0-9]{1,4}x[0-9]{1,4}(\.[a-zA-Z0-9\.]{2,})?/', '$1$2', $url );
715
716 $url = apply_filters( 'wpmcs_remove_size_from_filename', $url );
717
718 if ( $remove_extension ) {
719 $ext = pathinfo( $url, PATHINFO_EXTENSION );
720 $url = str_replace( ".$ext", '', $url );
721 }
722
723 return $url;
724 }
725
726 /**
727 * Is the string a URL?
728 *
729 * @param mixed $string
730 *
731 * @return bool
732 */
733 public static function is_url( $string ): bool {
734 if ( empty( $string ) || ! is_string( $string ) ) {
735 return false;
736 }
737
738 if ( preg_match( '@^(?:https?:)?//[a-zA-Z0-9\-]+@', $string ) ) {
739 return true;
740 }
741
742 return false;
743 }
744
745 /**
746 * Parses a URL into its components. Compatible with PHP < 5.4.7.
747 *
748 * @param string $url The URL to parse.
749 *
750 * @param int $component PHP_URL_ constant for URL component to return.
751 *
752 * @return mixed An array of the parsed components, mixed for a requested component, or false on error.
753 */
754 public static function parse_url( $url, $component = -1 ) {
755 $url = trim( $url );
756 $no_scheme = 0 === strpos( $url, '//' );
757
758 if ( $no_scheme ) {
759 $url = 'http:' . $url;
760 }
761
762 $parts = parse_url( $url, $component );
763
764 if ( 0 < $component ) {
765 return $parts;
766 }
767
768 if ( $no_scheme && is_array( $parts ) ) {
769 unset( $parts['scheme'] );
770 }
771
772 return $parts;
773 }
774
775 /**
776 * Is the given string a usable URL?
777 *
778 * We need URLs that include at least a domain and filename with extension
779 * for URL rewriting in either direction.
780 *
781 * @param mixed $url
782 *
783 * @return bool
784 */
785 public static function is_file_url( $url ): bool {
786 if ( ! static::is_url( $url ) ) {
787 return false;
788 }
789
790 $parts = static::parse_url( $url );
791
792 if (
793 empty( $parts['host'] ) ||
794 empty( $parts['path'] ) ||
795 ! pathinfo( $parts['path'], PATHINFO_EXTENSION )
796 ) {
797 return false;
798 }
799
800 return true;
801 }
802
803
804 /**
805 * Remove query strings of services.
806 *
807 * @param string $content
808 * @param string $base_url Optional base URL that must exist within URL for Amazon query strings to be removed.
809 *
810 * @return string
811 */
812 public static function remove_query_strings( $content, $base_url = '' ) {
813 $pattern = '\?[^\s"<\?]*(?:X-Amz-Algorithm|AWSAccessKeyId|Key-Pair-Id|GoogleAccessId)=[^\s"<\?]+';
814 $group = 0;
815
816 if ( ! is_string( $content ) ) {
817 return $content;
818 }
819
820 if ( ! empty( $base_url ) ) {
821 $pattern = preg_quote( $base_url, '/' ) . '[^\s"<\?]+(' . $pattern . ')';
822 $group = 1;
823 }
824 if ( ! preg_match_all( '/' . $pattern . '/', $content, $matches ) || ! isset( $matches[ $group ] ) ) {
825 // No query strings found, return
826 return $content;
827 }
828
829 $matches = array_unique( $matches[ $group ] );
830
831 foreach ( $matches as $match ) {
832 $content = str_replace( $match, '', $content );
833 }
834 return $content;
835 }
836
837 /**
838 * Maybe unserialize data, but not if an object.
839 *
840 * @param mixed $data
841 *
842 * @return mixed
843 */
844 public static function maybe_unserialize( $data ) {
845 if ( is_serialized( $data ) ) {
846 return @unserialize( $data, array( 'allowed_classes' => false ) ); // @phpcs:ignore
847 }
848
849 return $data;
850 }
851
852
853 /**
854 * Serialize data if needed.
855 *
856 * @param mixed $data
857 * @return mixed
858 */
859 public static function maybe_serialize( $data ) {
860 if ( is_array( $data ) || is_object( $data ) ) {
861 return serialize( $data );
862 }
863
864 // If it's not an array or object, don't serialize. If it is already serialized, return as is.
865 if ( is_serialized( $data ) ) {
866 return $data;
867 }
868
869 return $data;
870 }
871
872
873 /**
874 * Validate JSON
875 */
876 public static function is_json( $string ) {
877 json_decode( $string );
878 return ( json_last_error() == JSON_ERROR_NONE );
879 }
880
881 /**
882 * Check whether a specific class::method exists in the current call stack.
883 *
884 * Useful for detecting callers like WooCommerce image regeneration
885 * without hard dependencies.
886 *
887 * @since 1.3.7
888 * @param string $class Fully qualified class name.
889 * @param string|null $function Method name (optional).
890 * @param int $depth Backtrace depth limit.
891 *
892 * @return bool
893 */
894 public static function is_called_from(
895 string $class,
896 ?string $function = null,
897 int $depth = 15
898 ) : bool {
899
900 $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, $depth );
901
902 foreach ( $trace as $frame ) {
903
904 if ( empty( $frame['class'] ) ) {
905 continue;
906 }
907
908 if ( $frame['class'] !== $class ) {
909 continue;
910 }
911
912 // If function not specified, class match is enough
913 if ( $function === null ) {
914 return true;
915 }
916
917 if ( isset( $frame['function'] ) && $frame['function'] === $function ) {
918 return true;
919 }
920 }
921
922 return false;
923 }
924
925
926 /**
927 * Is this an AJAX process?
928 *
929 * @return bool
930 */
931 public static function is_ajax() {
932 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
933 return true;
934 }
935
936 return false;
937 }
938
939 /**
940 * Helper function for filtering super globals. Easily testable.
941 *
942 * @param string $variable
943 * @param int $type
944 * @param int $filter
945 * @param mixed $options
946 *
947 * @return mixed
948 */
949 public static function filter_input( $variable, $type = INPUT_GET, $filter = FILTER_DEFAULT, $options = array() ) {
950 return filter_input( $type, $variable, $filter, $options );
951 }
952
953 /**
954 * Get license data safe for frontend exposure (no raw key).
955 *
956 * @return array
957 */
958 public static function get_safe_license_data() {
959 $data = get_option('wpmcs_pro_license', []);
960 if (empty($data)) {
961 return [];
962 }
963 $key = $data['license_key'] ?? '';
964 $masked = '';
965 if (!empty($key)) {
966 $parts = explode('-', $key);
967 if (count($parts) <= 2) {
968 $masked = str_repeat('*', strlen($key));
969 } else {
970 $first = $parts[0];
971 $last = end($parts);
972 $middle_count = count($parts) - 2;
973 $masked_middle = array_fill(0, $middle_count, '****');
974 $masked = $first . '-' . implode('-', $masked_middle) . '-' . $last;
975 }
976 }
977 return [
978 'masked_key' => $masked,
979 'status' => $data['status'] ?? '',
980 'expiry' => $data['expiry'] ?? '',
981 'is_expired' => $data['is_expired'] ?? false,
982 'is_domain_activated' => $data['is_domain_activated'] ?? false,
983 'can_activate' => $data['can_activate'] ?? false,
984 'message' => $data['message'] ?? '',
985 'last_checked' => $data['last_checked'] ?? 0,
986 ];
987 }
988
989 }
990