PluginProbe
Media Cloud Sync / 1.3.11
Media Cloud Sync v1.3.11
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.11, at includes/config/utils.php

1,141 lines 35.3 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 * @param string|false $meta_name
152 * @param string $meta_table
153 * @param bool $flush_cache Whether to flush the plugin object cache afterward.
154 */
155 public static function clear_all_meta($meta_name = false, $meta_table = 'all', $flush_cache = true) {
156 global $wpdb;
157
158 $meta_name = $meta_name == false || empty($meta_name) ? Schema::getConstant('META_KEY') : $meta_name;
159
160 if ( empty( $meta_name ) ) {
161 return false; // Avoid accidental deletions if the meta_key is empty
162 }
163
164 $meta_tables = $meta_table == 'all' ? ['postmeta', 'usermeta', 'options'] : [$meta_table];
165
166 if( in_array('postmeta', $meta_tables) ) {
167 // Clear post meta
168 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s", $meta_name ) );
169 }
170
171 if( in_array('usermeta', $meta_tables) ) {
172 // Clear user meta
173 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE meta_key = %s", $meta_name ) );
174 }
175
176 if( in_array('options', $meta_tables) ) {
177 // Clear options
178 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name = %s", $meta_name ) );
179 }
180
181 self::invalidate_core_meta_cache($meta_tables, $meta_name);
182
183 if ($flush_cache) {
184 Cache::flush_object_cache();
185 }
186
187 return true;
188 }
189
190 /**
191 * Clears all content meta from the database
192 *
193 * @param string|false $meta_name Optional. The meta key to clear. Defaults to the constant CONTENT_META_KEY.
194 * @param bool $flush_cache Whether to flush the plugin object cache afterward.
195 */
196 public static function clear_all_content_meta($meta_name = false, $flush_cache = true) {
197 global $wpdb;
198 $meta_name = $meta_name == false || empty($meta_name) ? Schema::getConstant('CONTENT_META_KEY') : $meta_name;
199
200 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s", $meta_name ) );
201
202 self::invalidate_core_meta_cache(['postmeta'], $meta_name);
203
204 if ($flush_cache) {
205 Cache::flush_object_cache();
206 }
207
208 return true;
209 }
210
211 /**
212 * Invalidate WordPress core object caches after direct SQL meta deletes.
213 */
214 private static function invalidate_core_meta_cache($meta_tables, $meta_name) {
215 if (!function_exists('wp_cache_delete')) {
216 return;
217 }
218
219 if (in_array('options', $meta_tables, true)) {
220 wp_cache_delete('alloptions', 'options');
221 wp_cache_delete($meta_name, 'options');
222 }
223
224 if (in_array('postmeta', $meta_tables, true)) {
225 if (function_exists('wp_cache_set_last_changed')) {
226 wp_cache_set_last_changed('posts');
227 } elseif (function_exists('wp_cache_delete')) {
228 wp_cache_delete('last_changed', 'posts');
229 }
230 }
231 }
232
233
234
235 /**
236 * Function To get Current credentials
237 * @since 1.0.0
238 * @return array|boolean|string|integer|float|double
239 */
240 public static function get_credentials($option='', $default=false, $masked_config = false){
241 $current_setttings = self::get_option('credentials',[], Schema::getConstant('GLOBAL_SETTINGS_KEY'));
242 if(isset($current_setttings) && !empty($current_setttings)){
243 // Resolve the credential source. Defaults to 'database' for backward compatibility.
244 $source = isset($current_setttings['configSource']) ? $current_setttings['configSource'] : 'database';
245
246 if($source === 'config') {
247 // Credentials live in the WPMCS_CONFIG constant (wp-config.php).
248 // Server-side consumers receive the real values; REST-facing (masked) callers
249 // receive nothing so the constant contents are never exposed to the browser.
250 $current_setttings['config'] = $masked_config ? [] : self::get_wp_config_credentials();
251 } elseif($masked_config && isset($current_setttings['config'])) {
252 $current_setttings['config'] = self::mask_config($current_setttings['config']);
253 }
254
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 * Check whether credentials are defined via the WPMCS_CONFIG constant in wp-config.php.
271 * @since 1.3.11
272 * @return boolean
273 */
274 public static function is_wp_config_credentials_defined(){
275 return defined('WPMCS_CONFIG');
276 }
277
278 /**
279 * Get credentials defined via the WPMCS_CONFIG constant in wp-config.php.
280 * Accepts either a PHP array or a serialized string.
281 * @since 1.3.11
282 * @return array
283 */
284 public static function get_wp_config_credentials(){
285 if(!defined('WPMCS_CONFIG')) {
286 return [];
287 }
288 $config = constant('WPMCS_CONFIG');
289 if(is_string($config)) {
290 $config = self::maybe_unserialize($config);
291 }
292 return is_array($config) ? $config : [];
293 }
294
295 /**
296 * Get the current credential source ('database' | 'config').
297 * Defaults to 'database' for backward compatibility.
298 * @since 1.3.11
299 * @return string
300 */
301 public static function get_credentials_source(){
302 $current_setttings = self::get_option('credentials',[], Schema::getConstant('GLOBAL_SETTINGS_KEY'));
303 return isset($current_setttings['configSource']) ? $current_setttings['configSource'] : 'database';
304 }
305
306 /**
307 * Mask Config
308 * @since 1.2.13
309 * @return array|boolean|string|integer|float|double
310 */
311 public static function mask_config($config){
312 foreach ($config as $key => $value) {
313 if (in_array($key, ['config_json', 'secret_key'])) {
314 $config[$key] = substr($value, 0, 4) . self::mask_string(substr($value, 4));
315 }
316 }
317 return $config;
318 }
319
320 /**
321 * Mask String
322 * @since 1.2.13
323 * @return array|boolean|string|integer|float|double
324 */
325 public static function mask_string($string){
326 return str_repeat('*', strlen($string));
327 }
328
329 /**
330 * Function To get Current settings
331 * @since 1.0.0
332 * @return array|boolean|string|integer|float|double
333 */
334 public static function get_settings($option='', $default=false){
335 $current_setttings = self::get_option('settings',[], Schema::getConstant('GLOBAL_SETTINGS_KEY'));
336 if(isset($current_setttings) && !empty($current_setttings)){
337 if(isset($option) && !empty($option)){
338 if(isset($current_setttings[$option])) {
339 return $current_setttings[$option];
340 } else {
341 return $default;
342 }
343 } else {
344 return $current_setttings;
345 }
346 } else {
347 return $default;
348 }
349 }
350
351 /**
352 * Function To get Current statuses
353 * @since 1.0.0
354 * @return array|boolean|string|integer|float|double
355 */
356 public static function get_status($option='', $default=false){
357 $current_setttings = self::get_option('status',[], Schema::getConstant('STATUS_KEY'));
358 if(isset($current_setttings) && !empty($current_setttings)){
359 if(isset($option) && !empty($option)){
360 if(isset($current_setttings[$option])) {
361 return $current_setttings[$option];
362 } else {
363 return $default;
364 }
365 } else {
366 return $current_setttings;
367 }
368 } else {
369 return $default;
370 }
371 }
372
373 /**
374 * Function To set statuses
375 * @since 1.0.0
376 *
377 */
378 public static function set_status($option='', $data=[]){
379 if(!isset($option) || empty($option)){
380 return false;
381 }
382
383 $meta_name = Schema::getConstant('STATUS_KEY');
384 $current_setttings = get_option($meta_name, []);
385
386 if(!is_array($current_setttings)) {
387 $current_setttings = [];
388 }
389
390 $current_setttings[$option] = $data;
391
392 return self::update_option('status', $current_setttings, $meta_name);
393 }
394
395 /**
396 * Function To get Current Service
397 * @since 1.0.0
398 * @return array|boolean|string|integer|float|double
399 */
400 public static function get_service(){
401 $current_service = self::get_credentials('service', '');
402 if(isset($current_service) && !empty($current_service)){
403 return $current_service;
404 } else {
405 return false;
406 }
407 }
408
409 /**
410 * Function To get Current config
411 * @since 1.0.0
412 * @return array|boolean|string|integer|float|double
413 */
414 public static function get_config($option='', $default=false){
415 $current_setttings = self::get_credentials('config',[]);
416 if(isset($current_setttings) && !empty($current_setttings)){
417 if(isset($option) && !empty($option)){
418 if(isset($current_setttings[$option])) {
419 return $current_setttings[$option];
420 } else {
421 return $default;
422 }
423 } else {
424 return $current_setttings;
425 }
426 } else {
427 return $default;
428 }
429 }
430
431 /**
432 * Function to check serving media environment is ok
433 * @since 1.0.0
434 * @return boolean
435 */
436 public static function is_ok_to_serve($attachment_id = false, $check_id = true){
437 return (
438 self::is_service_enabled() &&
439 self::get_settings('rewrite_url') &&
440 ( $check_id ? isset($attachment_id) && !empty($attachment_id) : true )
441 );
442 }
443
444 /**
445 * Function to check uploading media environment is ok
446 * @since 1.0.0
447 * @return boolean
448 */
449 public static function is_ok_to_upload($attachment_id = false){
450 return (
451 self::is_service_enabled() &&
452 self::get_settings('copy_to_bucket') &&
453 isset($attachment_id) && !empty($attachment_id)
454 );
455 }
456
457 /**
458 * Whether stored credentials are complete for the configured service.
459 * @since 1.3.11
460 * @return boolean
461 */
462 private static function has_valid_storage_credentials() {
463 return self::get_service_configuration_error() === '';
464 }
465
466 /**
467 * Human-readable error when storage credentials are incomplete.
468 * @since 1.3.11
469 * @return string Empty when valid.
470 */
471 public static function get_service_configuration_error() {
472 $service = self::get_service();
473 if(!$service) {
474 return '';
475 }
476
477 $credentials = self::get_credentials('', [], false);
478 $bucketConfig = isset($credentials['bucketConfig']) ? $credentials['bucketConfig'] : [];
479
480 if(empty($bucketConfig['bucket_name'])) {
481 return esc_html__('Bucket name is not configured.', 'media-cloud-sync');
482 }
483
484 $configSource = self::get_credentials_source();
485
486 if($configSource === 'config') {
487 if(!self::is_wp_config_credentials_defined()) {
488 return esc_html__('WPMCS_CONFIG is not defined in wp-config.php', 'media-cloud-sync');
489 }
490
491 $config = self::get_wp_config_credentials();
492 $missing = [];
493 foreach(Service::get_required_config_keys($service) as $key) {
494 if(!isset($config[$key]) || $config[$key] === '') {
495 $missing[] = $key;
496 }
497 }
498 if(!empty($missing)) {
499 /* translators: %s: comma separated list of missing configuration keys */
500 return sprintf(esc_html__('WPMCS_CONFIG is missing key(s): %s', 'media-cloud-sync'), implode(', ', $missing));
501 }
502 } else {
503 $config = isset($credentials['config']) ? $credentials['config'] : [];
504 $missing = [];
505 foreach(Service::get_required_config_keys($service) as $key) {
506 if(!isset($config[$key]) || $config[$key] === '') {
507 $missing[] = $key;
508 }
509 }
510 if(!empty($missing)) {
511 /* translators: %s: comma separated list of missing configuration keys */
512 return sprintf(esc_html__('Storage credentials are incomplete. Missing key(s): %s', 'media-cloud-sync'), implode(', ', $missing));
513 }
514 }
515
516 return '';
517 }
518
519 /**
520 * Function To check service is enabled
521 * @since 1.0.0
522 * @return array|boolean|string|integer|float|double
523 */
524 public static function is_service_enabled(){
525 return !!self::get_service() && self::has_valid_storage_credentials();
526 }
527
528 /**
529 * Check whether a file exist in a list of files
530 * @since 1.0.1
531 * @return boolean
532 */
533 public static function check_existing_file_names( $filename, $files ) {
534 $fname = pathinfo( $filename, PATHINFO_FILENAME );
535 $ext = pathinfo( $filename, PATHINFO_EXTENSION );
536
537 // Edge case, file names like `.ext`.
538 if ( empty( $fname ) ) {
539 return false;
540 }
541
542 if ( $ext ) {
543 $ext = ".$ext";
544 }
545
546 $regex = '/^' . preg_quote( $fname ) . '-(?:\d+x\d+|scaled|rotated)' . preg_quote( $ext ) . '$/i';
547
548 foreach ( $files as $file ) {
549 if (
550 preg_match( $regex, wp_basename($file) ) ||
551 $filename == $file
552 ) {
553 return true;
554 }
555 }
556
557 return false;
558 }
559
560
561 /**
562 * Get relative attachment path for local source or remote object key.
563 *
564 * @param string $file File path, URL, or object key
565 * @param string $type 'source' (local WP) or 'key' (cloud / CDN)
566 *
567 * @return string|false
568 */
569 public static function get_attachment_source_path( $file, $type = 'source' ) {
570 if ( empty( $file ) || ! is_string( $file ) ) {
571 return false;
572 }
573
574 // Normalize slashes early
575 $file = str_replace( '\\', '/', $file );
576
577 /**
578 * -------------------------------------------------
579 * TYPE: SOURCE (WordPress local paths / URLs)
580 * -------------------------------------------------
581 */
582 if ( $type === 'source' ) {
583
584 $uploads = wp_get_upload_dir();
585 if ( empty( $uploads ) || ! empty( $uploads['error'] ) ) {
586 return false;
587 }
588
589 $basedir = str_replace( '\\', '/', $uploads['basedir'] );
590 $baseurl = str_replace( '\\', '/', $uploads['baseurl'] );
591
592 // If URL → extract path
593 if ( filter_var( $file, FILTER_VALIDATE_URL ) ) {
594 $parsed = wp_parse_url( $file );
595 $file = $parsed['path'] ?? '';
596 }
597
598 // Strip WordPress upload root
599 if ( 0 === strpos( $file, $basedir ) ) {
600 $file = substr( $file, strlen( $basedir ) );
601 } elseif ( 0 === strpos( $file, $baseurl ) ) {
602 $file = substr( $file, strlen( $baseurl ) );
603 }
604 }
605
606 /**
607 * -------------------------------------------------
608 * TYPE: KEY (Cloud / CDN paths or URLs)
609 * -------------------------------------------------
610 */
611 elseif ( $type === 'key' ) {
612
613 // URL → extract path only
614 if ( filter_var( $file, FILTER_VALIDATE_URL ) ) {
615 $parsed = wp_parse_url( $file );
616 $file = $parsed['path'] ?? '';
617 }
618
619 $file = ltrim( $file, '/' );
620
621 $enable_base_path = self::get_settings( 'enable_base_path', true );
622 $base_path = trim( (string) self::get_settings( 'base_path', '' ), '/' );
623
624 /**
625 * If base_path is enabled and exists as a FULL segment,
626 * strip everything before it.
627 */
628 if ( $enable_base_path && $base_path !== '' ) {
629 $pattern = '#(^|/)' . preg_quote( $base_path, '#' ) . '(/|$)#';
630
631 if ( preg_match( $pattern, $file, $m, PREG_OFFSET_CAPTURE ) ) {
632 $file = substr( $file, $m[0][1] );
633 }
634 }
635 }
636
637 // Final cleanup
638 $file = trim( $file, '/' );
639
640 /**
641 * Reject directory-only paths
642 */
643 if ( $file === '' || substr( $file, -1 ) === '/' ) {
644 return false;
645 }
646
647 return apply_filters(
648 'wpmcs_get_relative_file_path_from_upload_directory',
649 $file,
650 $type
651 );
652 }
653
654
655 /**
656 * Whether the file may be synced based on plugin extension settings only.
657 *
658 * Uses `extensions_exclude` to block listed extensions and optional `extensions_include` as an allow-list.
659 * When `extensions_include` is empty, no extension is blocked by the allow-list (only exclude applies).
660 * WordPress MIME / `wp_check_filetype` is not used here.
661 *
662 * @since 1.0.0
663 * @param string $path Absolute or relative file path.
664 * @return bool
665 */
666 public static function is_extension_available( $path ) {
667 $settings = self::get_settings();
668 $path_parts = pathinfo( $path );
669
670 if ( ! isset( $path_parts['basename'] ) || $path_parts['basename'] === '' ) {
671 return false;
672 }
673
674 $ext = isset( $path_parts['extension'] ) ? strtolower( $path_parts['extension'] ) : '';
675
676 $allowed = [];
677 if ( isset( $settings['extensions_include'] ) && is_array( $settings['extensions_include'] ) ) {
678 $allowed = array_map( 'strtolower', array_filter( $settings['extensions_include'], 'strlen' ) );
679 }
680
681 $not_allowed = [];
682 if ( isset( $settings['extensions_exclude'] ) && is_array( $settings['extensions_exclude'] ) ) {
683 $not_allowed = array_map( 'strtolower', array_filter( $settings['extensions_exclude'], 'strlen' ) );
684 }
685
686 if ( in_array( $ext, $not_allowed, true ) ) {
687 return false;
688 }
689
690 if ( ! empty( $allowed ) && ! in_array( $ext, $allowed, true ) ) {
691 return false;
692 }
693
694 return true;
695 }
696
697 /**
698 * Generate prefix for object versioning
699 * @since 1.0.0
700 * @return string
701 */
702 public static function generate_object_versioning_prefix(){
703 $year_month = self::get_settings('year_month');
704 $date_format = $year_month ? 'dHis' : 'YmdHis';
705
706 // Use current time so that object version is unique
707 $time = current_time('timestamp');
708
709 $object_version = date($date_format, $time) . '/';
710 $object_version = apply_filters('wpmcs_object_version_prefix', $object_version);
711
712 return $object_version;
713 }
714
715
716 /**
717 * Generate Key for Objects
718 * @since 1.0.0
719 */
720 public static function generate_object_key($relative_source_path, $prefix) {
721 $upload_path = '';
722 $enable_base_path = self::get_settings('enable_base_path', true);
723 $base_path = self::get_settings('base_path', 'wp-content/uploads');
724 $year_month = self::get_settings('year_month', true);
725 $relative_source_path = ltrim( $relative_source_path, '/' );
726 $file_name = wp_basename( $relative_source_path );
727
728 if(!$enable_base_path) { // If base path is not enabled
729 $base_path = '';
730 }
731
732 $keep_original_folder_structure = apply_filters( 'wpmcs_keep_original_folder_structure', false );
733
734 if(isset($base_path) && !empty($base_path)) {
735 $upload_path.= preg_replace('~/+~', '/',
736 str_replace('\\', '/',
737 trim($base_path," \n\r\t\v\x00\/ ")
738 )
739 );
740 }
741
742 if($keep_original_folder_structure) {
743 $object_key = ltrim($upload_path . '/' . dirname( $relative_source_path ) . '/' . $prefix . $file_name, '/');
744 } else {
745 if(isset($year_month) && $year_month) {
746 $year_month_prefix = self::get_year_month_from_file_path($relative_source_path);
747 if($year_month_prefix) {
748 $upload_path.= '/'.$year_month_prefix;
749 } else {
750 $upload_path.= '/'.date("Y/m");
751 }
752 }
753
754 $object_key = ltrim($upload_path.'/'.$prefix.$file_name, '/');
755 }
756
757 return apply_filters( 'wpmcs_object_key', $object_key, $relative_source_path, $prefix );
758 }
759
760
761 /**
762 * Check if a file path or URL follows the year/month structure and return the year/month as a string.
763 *
764 * @param string $path_or_url The relative path, absolute path, or URL to check.
765 * @return string|false The year/month string if valid, false otherwise.
766 */
767 public static function get_year_month_from_file_path( $path_or_url ) {
768 // Regex pattern to match paths and URLs containing 'YYYY/MM/' at any depth, allowing subdirectories afterward
769 $pattern = '#(?:^|/)(\d{4})/(0[1-9]|1[0-2])/[^/]+(?:/[^/]+)*$#';
770
771 // Check if the input matches the pattern
772 if ( preg_match( $pattern, $path_or_url, $matches ) ) {
773 // Return the year/month as a string in the format 'YYYY/MM'
774 return $matches[1] . '/' . $matches[2];
775 }
776
777 return false; // Invalid format
778 }
779
780
781 /**
782 * Maybe convert size to string
783 *
784 * @param int $attachment_id
785 * @param mixed $size
786 *
787 * @return null|string
788 */
789 public static function maybe_convert_size_to_string( $attachment_id, $size ) {
790 if ( is_array( $size ) ) {
791 $width = ( isset( $size[0] ) && $size[0] > 0 ) ? $size[0] : 1;
792 $height = ( isset( $size[1] ) && $size[1] > 0 ) ? $size[1] : 1;
793 $original_aspect_ratio = $width / $height;
794 $meta = wp_get_attachment_metadata( $attachment_id );
795
796 if ( ! isset( $meta['sizes'] ) || empty( $meta['sizes'] ) ) {
797 return false;
798 }
799
800 $sizes = $meta['sizes'];
801 uasort( $sizes, function ( $a, $b ) {
802 // Order by image area
803 return ( $a['width'] * $a['height'] ) - ( $b['width'] * $b['height'] );
804 } );
805
806 $near_matches = array();
807
808 foreach ( $sizes as $size => $value ) {
809 if ( $width > $value['width'] || $height > $value['height'] ) {
810 continue;
811 }
812 $aspect_ratio = $value['width'] / $value['height'];
813 if ( $aspect_ratio === $original_aspect_ratio ) {
814 return $size;
815 }
816 $near_matches[] = $size;
817 }
818 // Return nearest match
819 if ( ! empty( $near_matches ) ) {
820 return $near_matches[0];
821 }
822 }
823
824 return $size;
825 }
826
827 /**
828 * Reduce the given URL down to the simplest version of itself.
829 *
830 * Useful for matching against the full version of the URL in a full-text search
831 * or saving as a key for dictionary type lookup.
832 *
833 * @param string $url
834 *
835 * @return string
836 */
837 public static function reduce_url( $url ) {
838 $parts = static::parse_url( $url );
839 $host = isset( $parts['host'] ) ? $parts['host'] : '';
840 $port = isset( $parts['port'] ) ? ":{$parts['port']}" : '';
841 $path = isset( $parts['path'] ) ? $parts['path'] : '';
842
843 return '//' . $host . $port . $path;
844 }
845
846 /**
847 * Remove scheme from URL.
848 *
849 * @param string $url
850 * @return string
851 */
852 public static function remove_scheme( $url ) {
853 return preg_replace( '/^(?:http|https):/', '', $url );
854 }
855
856 /**
857 * Remove size from filename (image[-100x100].jpeg).
858 *
859 * @param string $url
860 * @param bool $remove_extension
861 *
862 * @return string
863 */
864 public static function remove_size_from_filename( $url, $remove_extension = false ) {
865 $url = preg_replace( '/^(\S+)-[0-9]{1,4}x[0-9]{1,4}(\.[a-zA-Z0-9\.]{2,})?/', '$1$2', $url );
866
867 $url = apply_filters( 'wpmcs_remove_size_from_filename', $url );
868
869 if ( $remove_extension ) {
870 $ext = pathinfo( $url, PATHINFO_EXTENSION );
871 $url = str_replace( ".$ext", '', $url );
872 }
873
874 return $url;
875 }
876
877 /**
878 * Is the string a URL?
879 *
880 * @param mixed $string
881 *
882 * @return bool
883 */
884 public static function is_url( $string ): bool {
885 if ( empty( $string ) || ! is_string( $string ) ) {
886 return false;
887 }
888
889 if ( preg_match( '@^(?:https?:)?//[a-zA-Z0-9\-]+@', $string ) ) {
890 return true;
891 }
892
893 return false;
894 }
895
896 /**
897 * Parses a URL into its components. Compatible with PHP < 5.4.7.
898 *
899 * @param string $url The URL to parse.
900 *
901 * @param int $component PHP_URL_ constant for URL component to return.
902 *
903 * @return mixed An array of the parsed components, mixed for a requested component, or false on error.
904 */
905 public static function parse_url( $url, $component = -1 ) {
906 $url = trim( $url );
907 $no_scheme = 0 === strpos( $url, '//' );
908
909 if ( $no_scheme ) {
910 $url = 'http:' . $url;
911 }
912
913 $parts = parse_url( $url, $component );
914
915 if ( 0 < $component ) {
916 return $parts;
917 }
918
919 if ( $no_scheme && is_array( $parts ) ) {
920 unset( $parts['scheme'] );
921 }
922
923 return $parts;
924 }
925
926 /**
927 * Is the given string a usable URL?
928 *
929 * We need URLs that include at least a domain and filename with extension
930 * for URL rewriting in either direction.
931 *
932 * @param mixed $url
933 *
934 * @return bool
935 */
936 public static function is_file_url( $url ): bool {
937 if ( ! static::is_url( $url ) ) {
938 return false;
939 }
940
941 $parts = static::parse_url( $url );
942
943 if (
944 empty( $parts['host'] ) ||
945 empty( $parts['path'] ) ||
946 ! pathinfo( $parts['path'], PATHINFO_EXTENSION )
947 ) {
948 return false;
949 }
950
951 return true;
952 }
953
954
955 /**
956 * Remove query strings of services.
957 *
958 * @param string $content
959 * @param string $base_url Optional base URL that must exist within URL for Amazon query strings to be removed.
960 *
961 * @return string
962 */
963 public static function remove_query_strings( $content, $base_url = '' ) {
964 $pattern = '\?[^\s"<\?]*(?:X-Amz-Algorithm|AWSAccessKeyId|Key-Pair-Id|GoogleAccessId)=[^\s"<\?]+';
965 $group = 0;
966
967 if ( ! is_string( $content ) ) {
968 return $content;
969 }
970
971 if ( ! empty( $base_url ) ) {
972 $pattern = preg_quote( $base_url, '/' ) . '[^\s"<\?]+(' . $pattern . ')';
973 $group = 1;
974 }
975 if ( ! preg_match_all( '/' . $pattern . '/', $content, $matches ) || ! isset( $matches[ $group ] ) ) {
976 // No query strings found, return
977 return $content;
978 }
979
980 $matches = array_unique( $matches[ $group ] );
981
982 foreach ( $matches as $match ) {
983 $content = str_replace( $match, '', $content );
984 }
985 return $content;
986 }
987
988 /**
989 * Maybe unserialize data, but not if an object.
990 *
991 * @param mixed $data
992 *
993 * @return mixed
994 */
995 public static function maybe_unserialize( $data ) {
996 if ( is_serialized( $data ) ) {
997 return @unserialize( $data, array( 'allowed_classes' => false ) ); // @phpcs:ignore
998 }
999
1000 return $data;
1001 }
1002
1003
1004 /**
1005 * Serialize data if needed.
1006 *
1007 * @param mixed $data
1008 * @return mixed
1009 */
1010 public static function maybe_serialize( $data ) {
1011 if ( is_array( $data ) || is_object( $data ) ) {
1012 return serialize( $data );
1013 }
1014
1015 // If it's not an array or object, don't serialize. If it is already serialized, return as is.
1016 if ( is_serialized( $data ) ) {
1017 return $data;
1018 }
1019
1020 return $data;
1021 }
1022
1023
1024 /**
1025 * Validate JSON
1026 */
1027 public static function is_json( $string ) {
1028 json_decode( $string );
1029 return ( json_last_error() == JSON_ERROR_NONE );
1030 }
1031
1032 /**
1033 * Check whether a specific class::method exists in the current call stack.
1034 *
1035 * Useful for detecting callers like WooCommerce image regeneration
1036 * without hard dependencies.
1037 *
1038 * @since 1.3.7
1039 * @param string $class Fully qualified class name.
1040 * @param string|null $function Method name (optional).
1041 * @param int $depth Backtrace depth limit.
1042 *
1043 * @return bool
1044 */
1045 public static function is_called_from(
1046 string $class,
1047 ?string $function = null,
1048 int $depth = 15
1049 ) : bool {
1050
1051 $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, $depth );
1052
1053 foreach ( $trace as $frame ) {
1054
1055 if ( empty( $frame['class'] ) ) {
1056 continue;
1057 }
1058
1059 if ( $frame['class'] !== $class ) {
1060 continue;
1061 }
1062
1063 // If function not specified, class match is enough
1064 if ( $function === null ) {
1065 return true;
1066 }
1067
1068 if ( isset( $frame['function'] ) && $frame['function'] === $function ) {
1069 return true;
1070 }
1071 }
1072
1073 return false;
1074 }
1075
1076
1077 /**
1078 * Is this an AJAX process?
1079 *
1080 * @return bool
1081 */
1082 public static function is_ajax() {
1083 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
1084 return true;
1085 }
1086
1087 return false;
1088 }
1089
1090 /**
1091 * Helper function for filtering super globals. Easily testable.
1092 *
1093 * @param string $variable
1094 * @param int $type
1095 * @param int $filter
1096 * @param mixed $options
1097 *
1098 * @return mixed
1099 */
1100 public static function filter_input( $variable, $type = INPUT_GET, $filter = FILTER_DEFAULT, $options = array() ) {
1101 return filter_input( $type, $variable, $filter, $options );
1102 }
1103
1104 /**
1105 * Get license data safe for frontend exposure (no raw key).
1106 *
1107 * @return array
1108 */
1109 public static function get_safe_license_data() {
1110 $data = get_option('wpmcs_pro_license', []);
1111 if (empty($data)) {
1112 return [];
1113 }
1114 $key = $data['license_key'] ?? '';
1115 $masked = '';
1116 if (!empty($key)) {
1117 $parts = explode('-', $key);
1118 if (count($parts) <= 2) {
1119 $masked = str_repeat('*', strlen($key));
1120 } else {
1121 $first = $parts[0];
1122 $last = end($parts);
1123 $middle_count = count($parts) - 2;
1124 $masked_middle = array_fill(0, $middle_count, '****');
1125 $masked = $first . '-' . implode('-', $masked_middle) . '-' . $last;
1126 }
1127 }
1128 return [
1129 'masked_key' => $masked,
1130 'status' => $data['status'] ?? '',
1131 'expiry' => $data['expiry'] ?? '',
1132 'is_expired' => $data['is_expired'] ?? false,
1133 'is_domain_activated' => $data['is_domain_activated'] ?? false,
1134 'can_activate' => $data['can_activate'] ?? false,
1135 'message' => $data['message'] ?? '',
1136 'last_checked' => $data['last_checked'] ?? 0,
1137 ];
1138 }
1139
1140 }
1141