PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2
6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 5.2.2 5.3 All 153 releases
mainwp / class / class-mainwp-utility.php

class-mainwp-utility.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2, at class/class-mainwp-utility.php

2,615 lines 81.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Utility Helper.
4 *
5 * @package MainWP/Dashboard
6 */
7
8 namespace MainWP\Dashboard;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors, Generic.Metrics.CyclomaticComplexity -- Using cURL functions.
16
17 /**
18 * Class MainWP_Utility
19 *
20 * @package MainWP\Dashboard
21 */
22 class MainWP_Utility { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
23
24 /**
25 * Prefix for short-term notice options.
26 */
27 const SHORT_TERM_NOTICE_OPTION_PREFIX = '_mainwp_temp_notice_';
28
29
30 /**
31 * Prefix for per-user short-term notice options.
32 */
33 const USER_SHORT_TERM_NOTICE_OPTION_PREFIX = '_mainwp_user_temp_notice_';
34
35
36 /**
37 * New short term notice TTL.
38 */
39 const SHORT_TERM_NOTICE_TTL = MONTH_IN_SECONDS;
40
41
42 /**
43 * Yoast SEO is enabled return true else return null.
44 *
45 * @static
46 * @var boolean $enabled_wp_seo If Yoast SEO is enabled return true else return null.
47 */
48 public static $enabled_wp_seo = null;
49
50 /**
51 * Private static variable.
52 *
53 * @static
54 *
55 * @var mixed Default null
56 */
57 public static $last_deactivated_alerts = null;
58
59 /**
60 * Private static variable to hold the single instance of the class.
61 *
62 * @static
63 *
64 * @var mixed Default null
65 */
66 private static $instance = null;
67
68 /**
69 * Store the disabled php functions.
70 *
71 * @static
72 * @var string $disabled_functions disabled php functions.
73 */
74 public static $disabled_functions = null;
75
76 /**
77 * Method get_class_name()
78 *
79 * Get Class Name.
80 *
81 * @return object __CLASS__
82 */
83 public static function get_class_name() {
84 return __CLASS__;
85 }
86
87 /**
88 * Method instance()
89 *
90 * Create public static instance.
91 *
92 * @static
93 * @return MainWP_Utility
94 */
95 public static function instance() {
96 if ( null === static::$instance ) {
97 static::$instance = new self();
98 }
99
100 return static::$instance;
101 }
102
103 /**
104 * Method starts_with()
105 *
106 * Start of Stack Trace.
107 *
108 * @param mixed $haystack The full stack.
109 * @param mixed $needle The function that is throwing the error.
110 *
111 * @return mixed Needle in the Haystack.
112 */
113 public static function starts_with( $haystack, $needle ) {
114 return ! strncmp( $haystack, $needle, strlen( $needle ) );
115 }
116
117 /**
118 * Method ends_with()
119 *
120 * End of Stack Trace.
121 *
122 * @param mixed $haystack Haystack parameter.
123 * @param mixed $needle Needle parameter.
124 *
125 * @return boolean
126 */
127 public static function ends_with( $haystack, $needle ) {
128 $length = strlen( $needle );
129 if ( 0 === $length ) {
130 return true;
131 }
132
133 return substr( $haystack, - $length ) === $needle;
134 }
135
136 /**
137 * Get a nice URL.
138 *
139 * @param string $pUrl Website URL.
140 * @param bool $showHttp Whether to show HTTP.
141 * @param bool $removeTrailingSlash Whether to removev the trailing slash.
142 *
143 * @return string The formatted URL.
144 */
145 public static function get_nice_url( $pUrl, $showHttp = false, $removeTrailingSlash = false ) {
146 $url = $pUrl;
147
148 if ( static::starts_with( $url, 'http://' ) ) {
149 if ( ! $showHttp ) {
150 $url = substr( $url, 7 );
151 }
152 } elseif ( static::starts_with( $url, 'https://' ) ) {
153 if ( ! $showHttp ) {
154 $url = substr( $url, 8 );
155 }
156 } elseif ( $showHttp ) {
157 $url = 'http://' . $url; // phpcs:ignore -- NOSONAR -compatible.
158 }
159
160 if ( static::ends_with( $url, '/' ) ) {
161 if ( ! $showHttp ) {
162 $url = substr( $url, 0, strlen( $url ) - 1 );
163 }
164 } else {
165 $url .= '/';
166 }
167
168 if ( $removeTrailingSlash ) {
169 $url = rtrim( $url, '/' );
170 }
171
172 return $url;
173 }
174
175 /**
176 * Method is_domain_valid()
177 *
178 * Check $url against FILTER_VALIDATE_URL.
179 *
180 * @param mixed $url Domain to check.
181 *
182 * @return boolean True|False.
183 */
184 public static function is_domain_valid( $url ) {
185 return filter_var( $url, FILTER_VALIDATE_URL );
186 }
187
188 /**
189 * Method ctype_digit()
190 *
191 * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
192 *
193 * @param mixed $str String to check.
194 *
195 * @return boolean Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
196 */
197 public static function ctype_digit( $str ) {
198 return ( is_string( $str ) || is_int( $str ) || is_float( $str ) ) && preg_match( '/^\d+\z/', $str );
199 }
200
201 /**
202 * Method sortmulti()
203 *
204 * Sort the given array, Acending, Decending or by Natural Order.
205 *
206 * @param mixed $arr Array to sort.
207 * @param mixed $index Index of array.
208 * @param mixed $order Acending or Decending order.
209 * @param bool $natsort Sort an array using a "natural order" algorithm. Default: false.
210 * @param bool $case_sensitive If case sensitive return true else return false. Default: false.
211 *
212 * @return array $sorted Return the sorted array.
213 */
214 public static function sortmulti( $arr, $index, $order, $natsort = false, $case_sensitive = false ) { // phpcs:ignore -- NOSONAR - complex.
215 $sorted = array();
216 if ( is_array( $arr ) && ! empty( $arr ) ) {
217 foreach ( array_keys( $arr ) as $key ) {
218 $temp[ $key ] = $arr[ $key ][ $index ];
219 }
220 if ( ! $natsort ) {
221 if ( 'asc' === $order ) {
222 asort( $temp );
223 } else {
224 arsort( $temp );
225 }
226 } else {
227 if ( true === $case_sensitive ) {
228 natsort( $temp );
229 } else {
230 natcasesort( $temp );
231 }
232 if ( 'asc' !== $order ) {
233 $temp = array_reverse( $temp, true );
234 }
235 }
236 foreach ( array_keys( $temp ) as $key ) {
237 if ( is_numeric( $key ) ) {
238 $sorted[] = $arr[ $key ];
239 } else {
240 $sorted[ $key ] = $arr[ $key ];
241 }
242 }
243
244 return $sorted;
245 }
246
247 return $sorted;
248 }
249
250 /**
251 * Method get_sub_array_having()
252 *
253 * Get sub array.
254 *
255 * @param mixed $arr Array to traverse.
256 * @param mixed $index Index of array.
257 * @param mixed $value Array values.
258 *
259 * void array $output Sub array.
260 */
261 public static function get_sub_array_having( $arr, $index, $value ) {
262 $output = array();
263 if ( is_array( $arr ) && ! empty( $arr ) ) {
264 foreach ( $arr as $arrvalue ) {
265 $existed = isset( $arrvalue[ $index ] ) ? $arrvalue[ $index ] : null;
266 if ( $existed === $value ) {
267 $output[] = $arrvalue;
268 }
269 }
270 }
271
272 return $output;
273 }
274
275 /**
276 * Method get_sub_array_with_limit()
277 *
278 * Get sub array.
279 *
280 * @param mixed $arr Array to traverse.
281 * @param mixed $start start index of array.
282 * @param mixed $count count values.
283 *
284 * void array $output Sub array.
285 */
286 public static function get_sub_array_with_limit( $arr, $start, $count ) {
287 $output = array();
288 if ( is_array( $arr ) && ! empty( $arr ) ) {
289 if ( $start > count( $arr ) ) {
290 return array();
291 }
292 $i = 0;
293 foreach ( $arr as $value ) {
294 if ( $i >= $start && $i < $start + $count ) {
295 $output[] = $value;
296 }
297 ++$i;
298 if ( $i > $start + $count ) {
299 break;
300 }
301 }
302 }
303 return $output;
304 }
305
306
307 /**
308 * Method trim_slashes()
309 *
310 * Trim stashes from element.
311 *
312 * @param mixed $elem Element to trim.
313 *
314 * @return string Return string with no slashes.
315 */
316 public static function trim_slashes( $elem ) {
317 return trim( $elem, '/' );
318 }
319
320 /**
321 * Method sanitize()
322 *
323 * Sanitize given string.
324 *
325 * @param mixed $str String to sanitize.
326 *
327 * @return string Sanitized string.
328 */
329 public static function sanitize( $str ) {
330 return preg_replace( '/[\\\\\/\:"\*\?\<\>\|]+/', '', $str );
331 }
332
333 /**
334 * Method sanitize_alphanumeric()
335 *
336 * Sanitize given string.
337 *
338 * @param mixed $str String to sanitize.
339 *
340 * @return string Sanitized string.
341 */
342 public static function sanitize_attr_slug( $str ) {
343 $str = strtolower( $str );
344 $str = str_replace( array( '=', '?', '/' ), '-', $str );
345 $str = preg_replace( '/[^A-Za-z0-9^\-]/', '', $str );
346 return $str;
347 }
348
349 /**
350 * Method end_session()
351 *
352 * End a session.
353 *
354 * @return void
355 */
356 public static function end_session() {
357
358 if ( defined( 'WP_CLI' ) && WP_CLI ) {
359 return;
360 }
361
362 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
363 return;
364 }
365
366 session_write_close();
367 if ( 0 < ob_get_length() ) {
368 ob_end_flush();
369 }
370 }
371
372 /**
373 * Method get_timestamp()
374 *
375 * Get time stamp in gmt_offset.
376 *
377 * @param mixed $timestamp Time stamp to convert.
378 *
379 * @return string Time stamp in general mountain time offset.
380 */
381 public static function get_timestamp( $timestamp = false ) {
382 if ( false === $timestamp ) {
383 $timestamp = time();
384 }
385 $gmtOffset = get_option( 'gmt_offset' );
386
387 return $gmtOffset ? ( $gmtOffset * HOUR_IN_SECONDS ) + $timestamp : $timestamp;
388 }
389
390 /**
391 * Method date()
392 *
393 * Show date in given format.
394 *
395 * @param mixed $format Format to display date in.
396 *
397 * @return string Date.
398 */
399 public static function date( $format ) {
400 // phpcs:ignore -- use local date function.
401 return date( $format, static::get_timestamp() );
402 }
403
404 /**
405 * Method format_timestamp()
406 *
407 * Format the given timestamp.
408 *
409 * @param mixed $timestamp Timestamp to format.
410 *
411 * @return string Formatted timestamp.
412 */
413 public static function format_timestamp( $timestamp ) {
414 return date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp );
415 }
416
417 /**
418 * Method format_timezone()
419 *
420 * Format the given timestamp.
421 *
422 * @param mixed $timestamp Timestamp to format.
423 * @param mixed $with_tz_info Return date time with timezone infor.
424 * @param mixed $use_tzformat Input tz format, to support display tz child site format.
425 *
426 * @return string Formatted timestamp.
427 */
428 public static function format_timezone( $timestamp, $with_tz_info = false, $use_tzformat = false ) { // phpcs:ignore -- NOSONAR - complex.
429 $tzinfo = '';
430 if ( false !== $use_tzformat ) {
431 if ( is_array( $use_tzformat ) && ( isset( $use_tzformat['timezone_string'] ) || isset( $use_tzformat['gmt_offset'] ) || isset( $use_tzformat['date_format'] ) || isset( $use_tzformat['time_format'] ) ) ) {
432 $wp_timezone = ! empty( $use_tzformat['timezone_string'] ) ? $use_tzformat['timezone_string'] : '';
433
434 $format = '';
435 if ( ! empty( $use_tzformat['date_format'] ) ) {
436 $format .= $use_tzformat['date_format'] . ' ';
437 }
438 if ( ! empty( $use_tzformat['time_format'] ) ) {
439 $format .= $use_tzformat['time_format'] . ' ';
440 }
441 $format = rtrim( $format );
442
443 if ( empty( $wp_timezone ) ) {
444 $gmt = ! empty( $use_tzformat['gmt_offset'] ) ? $use_tzformat['gmt_offset'] : 0;
445 return date_i18n( $format, $timestamp, $gmt );
446 }
447
448 $datetime = new \DateTime( '@' . $timestamp );
449 $datetime->setTimezone( new \DateTimeZone( $wp_timezone ) );
450
451 return $datetime->format( $format );
452 }
453 return '';
454 }
455
456 $wp_timezone = static::clean_wp_timezone_string( get_option( 'timezone_string' ) );
457
458 if ( ! $wp_timezone ) {
459 if ( $with_tz_info ) {
460 $tzinfo = ' ( UTC ' . get_option( 'gmt_offset' ) . ' )';
461 }
462 return static::format_timestamp( static::get_timestamp( $timestamp ) ) . $tzinfo;
463 }
464
465 if ( $with_tz_info ) {
466 $tzinfo = ' ( ' . $wp_timezone . ' )';
467 }
468
469 $datetime = new \DateTime( '@' . $timestamp );
470 $datetime->setTimezone( new \DateTimeZone( $wp_timezone ) );
471
472 $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) . $tzinfo;
473 return $datetime->format( $format );
474 }
475
476 /**
477 * Method clean_wp_timezone_string().
478 *
479 * @param mixed $raw Raw tz string to clean.
480 * @return string Clean tz string.
481 */
482 public static function clean_wp_timezone_string( $raw ) {
483 // If it's already a valid timezone, just return it.
484 if ( in_array( $raw, timezone_identifiers_list(), true ) ) {
485 return $raw;
486 }
487 // Try to find a valid timezone inside the messy string.
488 foreach ( timezone_identifiers_list() as $tz ) {
489 if ( strpos( $raw, $tz ) !== false ) {
490 return $tz;
491 }
492 }
493 return '';
494 }
495
496 /**
497 * Method format_timestamp()
498 *
499 * Format the given timestamp.
500 *
501 * @param mixed $timestamp Timestamp to format.
502 *
503 * @return string Formatted timestamp.
504 */
505 public static function format_date( $timestamp ) {
506 return date_i18n( get_option( 'date_format' ), $timestamp );
507 }
508
509 /**
510 * Method format_time()
511 *
512 * Format the given timestamp.
513 *
514 * @param mixed $timestamp Timestamp to format.
515 *
516 * @return string Formatted timestamp.
517 */
518 public static function format_time( $timestamp ) {
519 return date_i18n( get_option( 'time_format' ), $timestamp );
520 }
521
522 /**
523 * Get last sync information.
524 *
525 * Retrieves synchronization timestamp for global view or specific site.
526 * For global view, checks all sites and returns the most recent sync.
527 * For specific site, returns that site's last sync timestamp.
528 *
529 * @param int|null $site_id Optional. Site ID to get sync info for. Null for global view.
530 * @return array {
531 * Last sync information.
532 *
533 * @type int $timestamp Unix timestamp of last sync (0 if never synced).
534 * @type string $formatted Formatted date/time string using WordPress date/time format.
535 * @type string $status Sync status (only for global view): 'all_synced', 'not_synced', or false.
536 * @type string $message Human-readable sync message.
537 * }
538 */
539 public static function get_last_sync_info( $site_id = null ) {
540 $timestamp = 0;
541 $status = false;
542
543 if ( null === $site_id ) {
544 $result = MainWP_DB_Common::instance()->get_last_sync_status();
545 $status = $result['sync_status'];
546 $timestamp = $result['last_sync'];
547
548 if ( 'all_synced' === $status ) {
549 $timestamp = get_option( 'mainwp_last_synced_all_sites', $timestamp );
550 }
551 } else {
552 $site_id = absint( $site_id );
553 if ( $site_id > 0 ) {
554 $website = MainWP_DB::instance()->get_website_by_id( $site_id );
555 if ( $website && ! empty( $website->dtsSync ) ) {
556 $timestamp = $website->dtsSync;
557 }
558 }
559 }
560
561 $formatted = '';
562 $message = '';
563
564 if ( $timestamp ) {
565 $formatted = static::format_timestamp( static::get_timestamp( $timestamp ) );
566 /* translators: %s: formatted date/time */
567 $message = sprintf( esc_html__( 'Last synchronization completed on: %s', 'mainwp' ), $formatted );
568 } else {
569 $message = esc_html__( 'Not yet synchronized', 'mainwp' );
570 }
571
572 return array(
573 'timestamp' => $timestamp,
574 'formatted' => $formatted,
575 'status' => $status,
576 'message' => $message,
577 );
578 }
579
580 /**
581 * Format duration time to show.
582 *
583 * @param float $time timestamp.
584 * @return mixed result.
585 */
586 public static function format_duration_time( $time ) {
587
588 $original_sec = absint( $time );
589 $dura_sec = $original_sec;
590 $days = floor( $dura_sec / 86400 );
591 $dura_sec -= $days * 86400;
592 $dura_hour_sec = $dura_sec;
593 $dura_hours = floor( $dura_sec / 3600 );
594
595 if ( $days > 0 ) {
596 $formatted_dura = ( $days * 24 + $dura_hours ) . gmdate( 'i\m s\s', $dura_hour_sec );
597 } else {
598 $formatted_dura = gmdate( 'H\h i\m s\s', $original_sec );
599 }
600 return '<bdi>' . esc_html( $formatted_dura ) . '</bdi>';
601 }
602
603 /**
604 * Get UTC timestamp by date string.
605 *
606 * @param string $dt_str date.
607 * @param int $add_days Add days.
608 *
609 * @return mixed Local timestamp.
610 */
611 public static function get_utc_timestamp_by_date( $dt_str, $add_days = 0 ) {
612
613 $tz = wp_timezone(); // site timezone.
614 $day = new \DateTimeImmutable( $dt_str, $tz );
615
616 if ( is_numeric( $add_days ) && $add_days > 0 ) {
617 $day = $day->modify( '+' . $add_days . ' day' );
618 }
619
620 return $day->setTimezone( new \DateTimeZone( 'UTC' ) )->getTimestamp();
621 }
622
623
624 /**
625 * Converts a UTC timestamp (integer or float) to a local date string
626 * using the site's timezone (handles DST automatically).
627 *
628 * Supports:
629 * - Integer seconds timestamps (e.g. 1696930123)
630 * - Float seconds with microseconds (e.g. 1696930123.123456)
631 *
632 * Uses WordPress `wp_timezone()` to determine the local timezone.
633 *
634 * @since 6.0.0
635 *
636 * @param int|float|string $utc_time UTC timestamp in seconds. Can be integer (seconds)
637 * or float (seconds with microseconds).
638 * @param string $format_str PHP date format string. Default 'Y-m-d'.
639 * @param int $add_days Optional. Number of days to add (can be negative). Default 0.
640 *
641 * @return string Formatted local date string, or empty string on invalid input.
642 *
643 * @example
644 * // From plain timestamp:
645 * echo MyClass::get_local_date_by_utc_timestamp(1696930123, 'Y-m-d H:i:s');
646 * // → "2023-10-10 15:35:23" (depending on site timezone)
647 *
648 * @example
649 * // From microtime (float seconds):
650 * echo MyClass::get_local_date_by_utc_timestamp(1696930123.123456, 'Y-m-d H:i:s.v');
651 * // → "2023-10-10 15:35:23.123" (microseconds preserved)
652 */
653 public static function get_local_date_by_utc_timestamp( $utc_time, $format_str = 'Y-m-d', $add_days = 0 ) {
654 if ( ! is_numeric( $utc_time ) ) {
655 return '';
656 }
657
658 $tz = wp_timezone();
659 $utc_zone = new \DateTimeZone( 'UTC' );
660
661 // Detect float (has fractional seconds).
662 if ( is_float( $utc_time ) || strpos( (string) $utc_time, '.' ) !== false ) {
663 $dt_utc = \DateTimeImmutable::createFromFormat( 'U.u', sprintf( '%.6F', $utc_time ), $utc_zone );
664 } else {
665 $dt_utc = ( new \DateTimeImmutable( '@' . $utc_time ) )->setTimezone( $utc_zone );
666 }
667
668 if ( $add_days ) {
669 $dt_utc = $dt_utc->modify( sprintf( '%+d day', $add_days ) );
670 }
671
672 return $dt_utc->setTimezone( $tz )->format( $format_str );
673 }
674
675
676 /**
677 * Compute day/offset values and UTC datetime string for a local input time.
678 *
679 * - Automatically uses WordPress site timezone if none provided.
680 * - Converts the local datetime to UTC for MySQL compatibility.
681 * - Returns microsecond constants for daily grouping and offset adjustments.
682 *
683 * @param string $from_date_local 'Y-m-d H:i:s' in local timezone.
684 * @param string|null $local_timezone Optional. PHP timezone ID (e.g. 'Asia/Ho_Chi_Minh').
685 * @return array {
686 * @type int $day_micros Microseconds in one day (86400000000).
687 * @type int $offset_micro Timezone offset in microseconds (e.g. +25200000000).
688 * @type string $from_date_utc UTC datetime string ('Y-m-d H:i:s') for MySQL.
689 * @type string $local_timezone The timezone actually used.
690 * }
691 */
692 public static function get_time_context( $from_date_local, $local_timezone = null ) {
693 if ( empty( $local_timezone ) ) {
694 if ( function_exists( 'wp_timezone' ) ) {
695 $tz_obj = wp_timezone();
696 $local_timezone = $tz_obj->getName();
697 } else {
698 $local_timezone = get_option( 'timezone_string' ) ? get_option( 'timezone_string' ) : 'UTC';
699 }
700 }
701
702 $MICRO = 1000000;
703 $SECONDS_PER_DAY = 86400;
704 $day_micros = $SECONDS_PER_DAY * $MICRO;
705
706 $tz = new \DateTimeZone( $local_timezone );
707
708 // 👇 End of local day instead of start.
709 $dt = new \DateTimeImmutable( $from_date_local . ' 23:59:59', $tz );
710
711 $offset_seconds = $tz->getOffset( $dt );
712 $offset_micro = $offset_seconds * $MICRO;
713
714 $from_date_utc = $dt->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
715
716 return array(
717 'day_micros' => $day_micros,
718 'offset_micro' => $offset_micro,
719 'from_date_utc' => $from_date_utc,
720 'local_timezone' => $local_timezone,
721 );
722 }
723
724
725 /**
726 * Method human_filesize()
727 *
728 * Convert to human readable file size format,
729 * (B|kB|MB|GB|TB|PB|EB|ZB|YB).
730 *
731 * @param mixed $bytes File in bytes.
732 * @param integer $decimals Number of decimals to output.
733 *
734 * @return string Human readable file size.
735 */
736 public static function human_filesize( $bytes, $decimals = 2 ) {
737 $size = array( 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' );
738 $factor = floor( ( strlen( $bytes ) - 1 ) / 3 );
739
740 return sprintf( "%.{$decimals}f", $bytes / pow( 1024, $factor ) ) . @$size[ $factor ];
741 }
742
743 /**
744 * Method map_fields()
745 *
746 * Map Site.
747 *
748 * @param mixed $data data to map.
749 * @param mixed $keys Keys to map.
750 * @param bool $object_output Output format array|object.
751 *
752 * @return mixed Mapped data.
753 */
754 public static function map_fields( &$data, $keys, $object_output = true ) {
755 return static::map_site( $data, $keys, $object_output );
756 }
757
758 /**
759 * Method map_site()
760 *
761 * Map Site.
762 *
763 * @param mixed $website Website to map.
764 * @param mixed $keys Keys to map.
765 * @param bool $object_output Output format array|object.
766 *
767 * @return mixed $outputSite Mapped site.
768 */
769 public static function map_site( &$website, $keys, $object_output = true ) { // phpcs:ignore -- NOSONAR - complex.
770 if ( $object_output ) {
771 $outputSite = new \stdClass();
772 if ( ! empty( $website ) ) {
773 if ( is_object( $website ) ) {
774 foreach ( $keys as $key ) {
775 if ( property_exists( $website, $key ) ) {
776 $outputSite->{$key} = $website->$key;
777 } else {
778 $outputSite->{$key} = '';
779 }
780 }
781 } elseif ( is_array( $website ) ) {
782 foreach ( $keys as $key ) {
783 if ( isset( $website[ $key ] ) ) {
784 $outputSite->{$key} = $website[ $key ];
785 } else {
786 $outputSite->{$key} = '';
787 }
788 }
789 }
790 }
791 } else {
792 $outputSite = array();
793 if ( ! empty( $website ) ) {
794 if ( is_object( $website ) ) {
795 foreach ( $keys as $key ) {
796 if ( property_exists( $website, $key ) ) {
797 $outputSite[ $key ] = $website->$key;
798 } else {
799 $outputSite[ $key ] = '';
800 }
801 }
802 } elseif ( is_array( $website ) ) {
803 foreach ( $keys as $key ) {
804 if ( isset( $website[ $key ] ) ) {
805 $outputSite[ $key ] = $website[ $key ];
806 } else {
807 $outputSite[ $key ] = '';
808 }
809 }
810 }
811 }
812 }
813 return $outputSite;
814 }
815
816 /**
817 * Method array_merge()
818 *
819 * Merge two given arrays into one.
820 *
821 * @param mixed $arr1 First array.
822 * @param mixed $arr2 Second array.
823 *
824 * @return array Merged Array.
825 */
826 public static function array_merge( $arr1, $arr2 ) {
827 if ( ! is_array( $arr1 ) && ! is_array( $arr2 ) ) {
828 return array();
829 }
830 if ( ! is_array( $arr1 ) ) {
831 return $arr2;
832 }
833 if ( ! is_array( $arr2 ) ) {
834 return $arr1;
835 }
836
837 $output = array();
838 foreach ( $arr1 as $el ) {
839 $output[] = $el;
840 }
841 foreach ( $arr2 as $el ) {
842 $output[] = $el;
843 }
844
845 return $output;
846 }
847
848 /**
849 * Method update_option()
850 *
851 * Update option.
852 *
853 * @param mixed $option_name Option name.
854 * @param mixed $option_value Option value.
855 *
856 * @return (boolean) False if value was not updated and true if value was updated.
857 */
858 public static function update_option( $option_name, $option_value ) {
859 $success = add_option( $option_name, $option_value, '', 'no' );
860
861 if ( ! $success ) {
862 $success = update_option( $option_name, $option_value );
863 }
864
865 return $success;
866 }
867
868 /**
869 * Method update_user_option()
870 *
871 * Update option.
872 *
873 * @param mixed $option_name Option name.
874 * @param mixed $option_value Option value.
875 *
876 * @return (boolean) False if value was not updated and true if value was updated.
877 */
878 public static function update_user_option( $option_name, $option_value ) {
879 $user = wp_get_current_user();
880 if ( $user ) {
881 return update_user_option( $user->ID, $option_name, $option_value );
882 }
883 return false;
884 }
885
886 /**
887 * Method delete_user_option()
888 *
889 * @param string $option_name Option name.
890 *
891 * @return void.
892 */
893 public static function delete_user_option( $option_name ) {
894 $user = wp_get_current_user();
895 if ( $user ) {
896 delete_user_option( $user->ID, $option_name );
897 }
898 }
899
900 /**
901 * Method remove_preslash_spaces()
902 *
903 * Remove spaces before slashes.
904 *
905 * @param string $text String to strip.
906 *
907 * @return string $text Cleaned string.
908 */
909 public static function remove_preslash_spaces( $text ) {
910 while ( stristr( $text, ' /' ) ) {
911 $text = str_replace( ' /', '/', $text );
912 }
913
914 return $text;
915 }
916
917 /**
918 * Method remove_http_prefix()
919 *
920 * Remove http prefixes from given url.
921 *
922 * @param mixed $pUrl Given URL.
923 * @param bool $pTrimSlashes Whether or not to trim slashes. Default is false.
924 *
925 * @return string Trimmed URL.
926 */
927 public static function remove_http_prefix( $pUrl, $pTrimSlashes = false ) {
928 return str_replace( array( 'http:' . ( $pTrimSlashes ? '//' : '' ), 'https:' . ( $pTrimSlashes ? '//' : '' ) ), array( '', '' ), $pUrl );
929 }
930
931 /**
932 * Method remove_http_www_prefix()
933 *
934 * Remove 'www.' from given URL.
935 *
936 * @param mixed $pUrl Given URL.
937 *
938 * @return string Cleaned URL.
939 */
940 public static function remove_http_www_prefix( $pUrl ) {
941 $pUrl = static::remove_http_prefix( $pUrl, true );
942 if ( static::starts_with( strtolower( $pUrl ), 'www.' ) ) {
943 $pUrl = substr( $pUrl, 4 );
944 }
945 return $pUrl;
946 }
947
948 /**
949 * Method sanitize_file_name()
950 *
951 * Sanitize file names.
952 *
953 * @param mixed $filename File name to sanitize.
954 *
955 * @return string Sanitized filename.
956 */
957 public static function sanitize_file_name( $filename ) {
958 $filename = str_replace( array( '|', '/', '\\', ' ', ':' ), array( '-', '-', '-', '-', '-' ), $filename );
959 return sanitize_file_name( $filename );
960 }
961
962
963
964 /**
965 * Method esc_content()
966 *
967 * Escape content,
968 * allowed content (a,href,title,br,em,strong,p,hr,ul,ol,li,h1,h2 ... ).
969 *
970 * @param mixed $content Content to escape.
971 * @param string $type Type of content. Default = note.
972 * @param mixed $more_allowed input allowed tags - options.
973 *
974 * @return string Filtered content containing only the allowed HTML.
975 */
976 public static function esc_content( $content, $type = 'note', $more_allowed = array() ) {
977 if ( ! is_string( $content ) ) {
978 return $content;
979 }
980
981 if ( 'note' === $type ) {
982
983 $allowed_html = array(
984 'a' => array(
985 'href' => array(),
986 'title' => array(),
987 ),
988 'br' => array(),
989 'em' => array(),
990 'strong' => array(),
991 'p' => array(),
992 'hr' => array(),
993 'ul' => array(),
994 'ol' => array(),
995 'li' => array(),
996 'h1' => array(),
997 'h2' => array(),
998 );
999
1000 if ( is_array( $more_allowed ) && ! empty( $more_allowed ) ) {
1001 $allowed_html = array_merge( $allowed_html, $more_allowed );
1002 }
1003
1004 $content = wp_kses( $content, $allowed_html );
1005
1006 } elseif ( 'mixed' === $type ) {
1007
1008 $allowed_html = array(
1009 'a' => array(
1010 'href' => array(),
1011 'title' => array(),
1012 'class' => array(),
1013 'onclick' => array(),
1014 ),
1015 'img' => array(
1016 'src' => array(),
1017 'title' => array(),
1018 'class' => array(),
1019 'onclick' => array(),
1020 'alt' => array(),
1021 'width' => array(),
1022 'height' => array(),
1023 'sizes' => array(),
1024 'srcset' => array(),
1025 'usemap' => array(),
1026 ),
1027 'br' => array(),
1028 'em' => array(),
1029 'strong' => array(),
1030 'p' => array(),
1031 'hr' => array(),
1032 'ul' => array(
1033 'style' => array(),
1034 ),
1035 'ol' => array(),
1036 'li' => array(),
1037 'h1' => array(),
1038 'h2' => array(),
1039 'head' => array(),
1040 'html' => array(
1041 'lang' => array(),
1042 ),
1043 'meta' => array(
1044 'name' => array(),
1045 'http-equiv' => array(),
1046 'content' => array(),
1047 'charset' => array(),
1048 ),
1049 'title' => array(),
1050 'body' => array(
1051 'style' => array(),
1052 ),
1053 'span' => array(
1054 'id' => array(),
1055 'style' => array(),
1056 'class' => array(),
1057 ),
1058 'form' => array(
1059 'id' => array(),
1060 'method' => array(),
1061 'action' => array(),
1062 'onsubmit' => array(),
1063 ),
1064 'table' => array(
1065 'class' => array(),
1066 ),
1067 'thead' => array(
1068 'class' => array(),
1069 ),
1070 'tbody' => array(
1071 'class' => array(),
1072 ),
1073 'tr' => array(
1074 'id' => array(),
1075 ),
1076 'td' => array(
1077 'class' => array(),
1078 ),
1079 'div' => array(
1080 'id' => array(),
1081 'style' => array(),
1082 'class' => array(),
1083 ),
1084 'input' => array(
1085 'type' => array(),
1086 'name' => array(),
1087 'class' => array(),
1088 'value' => array(),
1089 'onclick' => array(),
1090 ),
1091 'button' => array(
1092 'type' => array(),
1093 'name' => array(),
1094 'value' => array(),
1095 'class' => array(),
1096 'title' => array(),
1097 'onclick' => array(),
1098 ),
1099 );
1100
1101 if ( is_array( $more_allowed ) && ! empty( $more_allowed ) ) {
1102 $allowed_html = array_merge( $allowed_html, $more_allowed );
1103 }
1104
1105 $content = wp_kses( $content, $allowed_html );
1106 } else {
1107 $content = wp_kses_post( $content );
1108 }
1109
1110 return $content;
1111 }
1112
1113 /**
1114 * Method esc_mixed_content()
1115 *
1116 * Escape mixed content,
1117 * allowed content (a,href,title,br,em,strong,p,hr,ul,ol,li,h1,h2 ... ).
1118 *
1119 * @param mixed $data data to escape.
1120 * @param string $depth Maximum depth to walk through $data. Must be greater than 0.
1121 * @param mixed $more_allowed input allowed tags - options.
1122 *
1123 * @throws \MainWP_Exception Excetpion message.
1124 *
1125 * @return string Filtered content containing only the allowed HTML.
1126 */
1127 public static function esc_mixed_content( $data, $depth, $more_allowed = array() ) { // phpcs:ignore -- NOSONAR - complex.
1128 if ( $depth < 0 ) {
1129 throw new MainWP_Exception( 'Reached depth limit' );
1130 }
1131
1132 if ( is_array( $data ) ) {
1133 $output = array();
1134 foreach ( $data as $id => $el ) {
1135 // Don't forget to sanitize the ID!
1136 if ( is_string( $id ) ) {
1137 $clean_id = static::esc_content( $id, 'mixed', $more_allowed );
1138 } else {
1139 $clean_id = $id;
1140 }
1141
1142 // Check the element type, so that we're only recursing if we really have to.
1143 if ( is_array( $el ) || is_object( $el ) ) {
1144 $output[ $clean_id ] = static::esc_mixed_content( $el, $depth - 1 );
1145 } elseif ( is_string( $el ) ) {
1146 $output[ $clean_id ] = static::esc_content( $el, 'mixed', $more_allowed );
1147 } else {
1148 $output[ $clean_id ] = $el;
1149 }
1150 }
1151 } elseif ( is_object( $data ) ) {
1152 $output = new stdClass();
1153 foreach ( $data as $id => $el ) {
1154 if ( is_string( $id ) ) {
1155 $clean_id = static::esc_content( $id, 'mixed', $more_allowed );
1156 } else {
1157 $clean_id = $id;
1158 }
1159
1160 if ( is_array( $el ) || is_object( $el ) ) {
1161 $output->$clean_id = static::esc_mixed_content( $el, $depth - 1, $more_allowed );
1162 } elseif ( is_string( $el ) ) {
1163 $output->$clean_id = static::esc_content( $el, 'mixed', $more_allowed );
1164 } else {
1165 $output->$clean_id = $el;
1166 }
1167 }
1168 } elseif ( is_string( $data ) ) {
1169 return static::esc_content( $data, 'mixed', $more_allowed );
1170 } else {
1171 return $data;
1172 }
1173
1174 return $output;
1175 }
1176
1177 /**
1178 * Method parse_html_error_message()
1179 *
1180 * @param string $error_msg Error message.
1181 *
1182 * @return mixed array|string.
1183 */
1184 public static function parse_html_error_message( $error_msg ) {
1185 // pasing error message that included link html.
1186 preg_match( '/([^\<]*)(<a[^\>]*>)([^\<]*)(<[^\>]*>)(.*)/', $error_msg, $output_array );
1187 if ( is_array( $output_array ) && 6 === count( $output_array ) ) {
1188 preg_match( '/<a href="([^\"]*)"(.*)/', $output_array[2], $link_array );
1189 $link = '';
1190 if ( is_array( $link_array ) && 3 === count( $link_array ) ) {
1191 $link = $link_array[1];
1192 }
1193 if ( ! empty( $link ) ) {
1194 return array(
1195 'el_before' => esc_html( $output_array[1] ),
1196 'el_link' => esc_html( $link ),
1197 'el_text' => esc_html( $output_array[3] ),
1198 'el_after' => esc_html( $output_array[5] ),
1199 );
1200 }
1201 }
1202 return $error_msg;
1203 }
1204
1205 /**
1206 * Method show_mainwp_message()
1207 *
1208 * Check whenther or not to show the MainWP Message.
1209 *
1210 * @param mixed $type Type of message.
1211 * @param mixed $notice_id Notice ID.
1212 *
1213 * @return boolean true|false.
1214 */
1215 public static function show_mainwp_message( $type, $notice_id ) {
1216 unset( $type );
1217 $status = get_user_option( 'mainwp_notice_saved_status' );
1218 if ( ! is_array( $status ) ) {
1219 $status = array();
1220 }
1221 if ( isset( $status[ $notice_id ] ) ) {
1222 return false;
1223 }
1224 return true;
1225 }
1226
1227 /**
1228 * Method get_hide_notice_status()
1229 *
1230 * Check whenther or not to show the MainWP Message.
1231 *
1232 * @param mixed $notice_id Notice ID.
1233 *
1234 * @return mixed true|false|time.
1235 */
1236 public static function get_hide_notice_status( $notice_id ) {
1237 $notices = get_user_option( 'mainwp_notice_saved_status' );
1238 if ( ! is_array( $notices ) ) {
1239 $notices = array();
1240 }
1241 if ( isset( $notices[ $notice_id ] ) ) {
1242 return $notices[ $notice_id ];
1243 }
1244 return false;
1245 }
1246
1247 /**
1248 * Method get_flash_message()
1249 *
1250 * Get saved flash Message.
1251 *
1252 * @param mixed $message_id Notice ID.
1253 * @param bool $delete True to delete the message after get it.
1254 *
1255 * @return boolean true|false.
1256 */
1257 public static function get_flash_message( $message_id, $delete = true ) {
1258 $flash_messages = get_user_option( 'mainwp_flash_messages' );
1259 if ( ! is_array( $flash_messages ) ) {
1260 $flash_messages = array();
1261 }
1262 if ( ! isset( $flash_messages[ $message_id ] ) ) {
1263 return false;
1264 }
1265 $content = $flash_messages[ $message_id ];
1266 if ( $delete ) {
1267 unset( $flash_messages[ $message_id ] );
1268 static::update_user_option( 'mainwp_flash_messages', $flash_messages );
1269 }
1270 return $content;
1271 }
1272
1273 /**
1274 * Method set_short_term_notice()
1275 *
1276 * Set new temporary notice.
1277 *
1278 * @param string $monitor Monitor.
1279 * @param string $noti_key Notice key.
1280 * @param int $checked_at Notice checked at.
1281 *
1282 * @return void.
1283 */
1284 public static function set_short_term_notice( $monitor, $noti_key, $checked_at ) {
1285 if ( empty( $monitor ) || empty( $noti_key ) ) {
1286 return;
1287 }
1288 $noti_handle_name = static::SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $noti_key;
1289 static::update_option( $noti_handle_name, $checked_at );
1290 }
1291
1292
1293 /**
1294 * Method is_short_term_notice()
1295 *
1296 * Check whether the short-term notice is still active.
1297 *
1298 * @param string $monitor Monitor name.
1299 * @param string $noti_key Notice key.
1300 *
1301 * @return boolean true|false.
1302 */
1303 public static function is_short_term_notice( $monitor, $noti_key ) {
1304
1305 if ( empty( $monitor ) || empty( $noti_key ) ) {
1306 return false;
1307 }
1308
1309 $noti_handle_name = static::SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $noti_key;
1310
1311 $notice_time = get_option( $noti_handle_name );
1312
1313 if ( false === $notice_time ) {
1314 return false;
1315 }
1316
1317 if ( ! is_numeric( $notice_time ) ) {
1318 delete_option( $noti_handle_name );
1319 }
1320
1321 if ( time() - (int) $notice_time < self::SHORT_TERM_NOTICE_TTL ) {
1322 if ( static::is_short_term_dismissed_notice( $monitor, $noti_key ) ) {
1323 return false;
1324 }
1325 return true;
1326 } else {
1327 delete_option( $noti_handle_name );
1328 }
1329 return false;
1330 }
1331
1332
1333 /**
1334 * Method is_short_term_dismissed_notice()
1335 *
1336 * Check whether the short-term notice is user dismissed.
1337 *
1338 * @param string $monitor Monitor.
1339 * @param string $noti_key Notice key.
1340 *
1341 * @return boolean true|false.
1342 */
1343 public static function is_short_term_dismissed_notice( $monitor, $noti_key ) {
1344 if ( empty( $monitor ) || empty( $noti_key ) ) {
1345 return false;
1346 }
1347 $user_noti_handle = static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $noti_key;
1348 $user = wp_get_current_user();
1349 if ( $user && ! empty( $user->ID ) ) {
1350 return 'dismissed' === get_option( $user_noti_handle . '_user_' . $user->ID );
1351 }
1352 return false;
1353 }
1354
1355
1356 /**
1357 * Purge short-term notices.
1358 *
1359 * @param string $monitor Optional monitor name. When provided, only
1360 * notices for the specified monitor are purged.
1361 * @param bool $expired_only Whether to purge only expired notices.
1362 *
1363 * @return void
1364 */
1365 public static function purge_short_term_notices( $monitor = '', $expired_only = true ) { // phpcs:ignore -- NOSONAR - complex.
1366
1367 $opt_prefix = self::SHORT_TERM_NOTICE_OPTION_PREFIX;
1368
1369 $prefix = '' !== $monitor ? $monitor . '_' : '';
1370
1371 $timeout = time() - self::SHORT_TERM_NOTICE_TTL;
1372
1373 $db = MainWP_DB::instance()->get_wpdb_instance();
1374
1375 $sql = $db->prepare(
1376 "
1377 SELECT option_name
1378 FROM {$db->options}
1379 WHERE option_name LIKE %s
1380 ",
1381 $db->esc_like( $opt_prefix . (string) $prefix ) . '%',
1382 );
1383
1384 if ( $expired_only ) {
1385 $sql .= $db->prepare(
1386 ' AND CAST(option_value AS UNSIGNED) <= %d ',
1387 $timeout
1388 );
1389 }
1390
1391 $options = $db->get_col( $sql );
1392
1393 foreach ( $options as $option_name ) {
1394 delete_option( $option_name );
1395 }
1396 }
1397
1398
1399 /**
1400 * Purge short-term notices for a specific issue.
1401 *
1402 * @param string $monitor Monitor name.
1403 * @param string $issue_code Issue code.
1404 *
1405 * @return bool True if the operation completed, false if the input is invalid.
1406 */
1407 public static function delete_short_term_notices_for_issue( $monitor, $issue_code ) {
1408
1409 if ( empty( $monitor ) || empty( $issue_code ) ) {
1410 return false;
1411 }
1412
1413 $opt_prefix = self::SHORT_TERM_NOTICE_OPTION_PREFIX;
1414 $prefix = $monitor . '_' . $issue_code;
1415
1416 $db = MainWP_DB::instance()->get_wpdb_instance();
1417
1418 $sql = $db->prepare(
1419 "
1420 SELECT option_name
1421 FROM {$db->options}
1422 WHERE option_name LIKE %s
1423 ",
1424 $db->esc_like( $opt_prefix . $prefix ) . '%'
1425 );
1426
1427 $options = $db->get_col( $sql );
1428
1429 foreach ( $options as $option_name ) {
1430 delete_option( $option_name ); // Delete options and cache.
1431 }
1432
1433 return true;
1434 }
1435
1436 /**
1437 * Method dismiss_short_term_notice()
1438 *
1439 * Hide short term notice.
1440 *
1441 * @param string $monitor_noti_key Notice key.
1442 *
1443 * @return void.
1444 */
1445 public static function dismiss_short_term_notice( $monitor_noti_key ) {
1446 $noti_handle_name = static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor_noti_key;
1447 $user = wp_get_current_user();
1448 if ( $user && ! empty( $user->ID ) ) {
1449 static::update_option( $noti_handle_name . '_user_' . $user->ID, 'dismissed' );
1450 }
1451 }
1452
1453 /**
1454 * Delete user short-term notice options for the specified monitor.
1455 *
1456 * Optionally preserves the dismissed state for the specified issue codes.
1457 *
1458 * @param string $monitor Monitor name.
1459 * @param array $preserved_issues Issue codes whose dismissed state should be preserved.
1460 *
1461 * @return void
1462 */
1463 public static function delete_short_term_notice_options( $monitor, $preserved_issues = array() ) {
1464
1465 if ( empty( $monitor ) ) {
1466 return;
1467 }
1468
1469 $db = MainWP_DB::instance()->get_wpdb_instance();
1470
1471 $sql = "
1472 SELECT option_name
1473 FROM {$db->options}
1474 WHERE option_name LIKE %s
1475 ";
1476
1477 $args = array(
1478 $db->esc_like(
1479 static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_'
1480 ) . '%',
1481 );
1482
1483 // Preserve the user's dismissed state for the specified issues.
1484 if ( ! empty( $preserved_issues ) && is_array( $preserved_issues ) ) {
1485 foreach ( $preserved_issues as $issue_code ) {
1486 $sql .= ' AND option_name NOT LIKE %s';
1487 $args[] = $db->esc_like(
1488 static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $issue_code . '_user_'
1489 ) . '%';
1490 }
1491 }
1492
1493 $option_names = $db->get_col(
1494 $db->prepare( $sql, ...$args )
1495 );
1496
1497 if ( empty( $option_names ) ) {
1498 return;
1499 }
1500
1501 foreach ( $option_names as $option_name ) {
1502 delete_option( $option_name );
1503 }
1504 }
1505
1506 /**
1507 * Method update_flash_message()
1508 *
1509 * Check whenther or not to show the MainWP Message.
1510 *
1511 * @param mixed $message_id Notice ID.
1512 * @param mixed $content Content of message.
1513 *
1514 * @return boolean true|false.
1515 */
1516 public static function update_flash_message( $message_id, $content ) {
1517 $flash_messages = get_user_option( 'mainwp_flash_messages' );
1518 if ( ! is_array( $flash_messages ) ) {
1519 $flash_messages = array();
1520 }
1521 $current = isset( $flash_messages[ $message_id ] ) ? $flash_messages[ $message_id ] : '';
1522 if ( empty( $current ) ) {
1523 $current = $content;
1524 } else {
1525 $current .= '|' . $content;
1526 }
1527 $flash_messages[ $message_id ] = $current;
1528 return static::update_user_option( 'mainwp_flash_messages', $flash_messages );
1529 }
1530
1531 /**
1532 * Method array_sort()
1533 *
1534 * Sort given array by given flags.
1535 *
1536 * @param mixed $arr Array to sort.
1537 * @param mixed $key Array key.
1538 * @param string $sort_flag Flags to sort by. Default = SORT_STRING.
1539 */
1540 public static function array_sort( &$arr, $key, $sort_flag = SORT_STRING ) {
1541 $sorter = array();
1542 $ret = array();
1543 reset( $arr );
1544 foreach ( $arr as $ii => $val ) {
1545 if ( isset( $val[ $key ] ) ) {
1546 $sorter[ $ii ] = $val[ $key ];
1547 } elseif ( SORT_NUMERIC === $sort_flag ) {
1548 $sorter[ $ii ] = count( $sorter );
1549 }
1550 }
1551 asort( $sorter, $sort_flag );
1552 foreach ( $sorter as $ii => $val ) {
1553 $ret[ $ii ] = $arr[ $ii ];
1554 }
1555 $arr = $ret;
1556 }
1557
1558 /**
1559 * Method array_sort_existed_keys()
1560 *
1561 * Sort given array by given flags.
1562 *
1563 * @param mixed $arr Array to sort.
1564 * @param mixed $key Array key.
1565 * @param string $sort_flag Flags to sort by. Default = SORT_STRING.
1566 */
1567 public static function array_sort_existed_keys( &$arr, $key, $sort_flag = SORT_STRING ) {
1568 $sorter = array();
1569 $ret = array();
1570 reset( $arr );
1571
1572 // get items with $key to sort.
1573 foreach ( $arr as $ii => $val ) {
1574 if ( isset( $val[ $key ] ) ) {
1575 $sorter[ $ii ] = $val[ $key ];
1576 }
1577 }
1578 asort( $sorter, $sort_flag );
1579
1580 foreach ( $sorter as $ii => $val ) {
1581 $ret[ $ii ] = $arr[ $ii ];
1582 }
1583
1584 // asign other items (without $keys).
1585 foreach ( $arr as $ii => $val ) {
1586 if ( ! isset( $val[ $key ] ) ) {
1587 $ret[ $ii ] = $val;
1588 }
1589 }
1590
1591 $arr = $ret;
1592 }
1593
1594 /**
1595 * Method numeric_filter()
1596 *
1597 * Filter given numeric.
1598 *
1599 * @param int $int_num Int number.
1600 * @return array $arr_ints Array filtered.
1601 */
1602 public static function numeric_filter( $int_num ) {
1603 return ( (string) (int) $int_num === (string) $int_num && 0 < $int_num ) ? $int_num : false;
1604 }
1605
1606 /**
1607 * Method array_numeric_filter()
1608 *
1609 * Filter given numeric array.
1610 *
1611 * @param array $arr_ints Array to filter.
1612 * @return array $arr_ints Array filtered.
1613 */
1614 public static function array_numeric_filter( $arr_ints ) {
1615 $arr_ints = array_filter(
1616 $arr_ints,
1617 function ( $e ) {
1618 return ( (string) (int) $e === (string) $e && 0 < $e ) ? true : false;
1619 }
1620 );
1621 return $arr_ints;
1622 }
1623
1624 /**
1625 * Method enabled_wp_seo()
1626 *
1627 * Check if Yoast SEO is enabled.
1628 *
1629 * @return boolean true|false.
1630 */
1631 public static function enabled_wp_seo() {
1632 if ( null === static::$enabled_wp_seo ) {
1633 static::$enabled_wp_seo = is_plugin_active( 'wordpress-seo-extension/wordpress-seo-extension.php' );
1634 }
1635 return static::$enabled_wp_seo;
1636 }
1637
1638 /**
1639 * Method value_to_string()
1640 *
1641 * Value to string.
1642 *
1643 * @param mixed $var_value Value to convert to string.
1644 *
1645 * @return string Value that has been converted into a string.
1646 */
1647 public static function value_to_string( $var_value ) {
1648 if ( is_array( $var_value ) || is_object( $var_value ) ) {
1649 //phpcs:ignore -- for debug only
1650 return print_r( $var_value, true );
1651 } elseif ( is_string( $var_value ) ) {
1652 return $var_value;
1653 }
1654 return '';
1655 }
1656
1657 /**
1658 * Get Health Site value.
1659 *
1660 * @param mixed $issue_counts Health site issues.
1661 *
1662 * @return array Health status value.
1663 */
1664 public static function get_site_health( $issue_counts ) {
1665
1666 // Coerce non-array/empty/partial input to a full set of counts so a scalar
1667 // (e.g. a JSON-decoded string) or a missing key never fatals or warns.
1668 $issue_counts = array_merge(
1669 array(
1670 'good' => 0,
1671 'recommended' => 0,
1672 'critical' => 0,
1673 ),
1674 is_array( $issue_counts ) ? $issue_counts : array()
1675 );
1676
1677 // Normalize each counter to a non-negative integer before arithmetic so a
1678 // non-numeric or negative stored value cannot fatal or skew the score.
1679 // is_numeric() first: intval() maps a non-empty array to 1, which would
1680 // count a malformed nested value as a real issue.
1681 $good = is_numeric( $issue_counts['good'] ) ? max( 0, intval( $issue_counts['good'] ) ) : 0;
1682 $recommended = is_numeric( $issue_counts['recommended'] ) ? max( 0, intval( $issue_counts['recommended'] ) ) : 0;
1683 $critical = is_numeric( $issue_counts['critical'] ) ? max( 0, intval( $issue_counts['critical'] ) ) : 0;
1684
1685 $totalTests = $good + $recommended + $critical * 1.5;
1686 $failedTests = $recommended * 0.5 + $critical * 1.5;
1687
1688 if ( empty( $totalTests ) ) {
1689 $val = 100;
1690 } else {
1691 $val = 100 - ceil( ( $failedTests / $totalTests ) * 100 );
1692 }
1693
1694 if ( 0 > $val ) {
1695 $val = 0;
1696 }
1697
1698 if ( 100 < $val ) {
1699 $val = 100;
1700 }
1701
1702 return array(
1703 'val' => $val,
1704 'critical' => $critical,
1705 );
1706 }
1707
1708
1709 /**
1710 * Get HTTP code.
1711 *
1712 * @param int $code HTTP code.
1713 *
1714 * @return array $http_codes HTTP code.
1715 */
1716 public static function get_http_codes( $code = false ) {
1717
1718 $http_codes = array(
1719 100 => 'Continue',
1720 101 => 'Switching Protocols',
1721 200 => 'OK',
1722 201 => 'Created',
1723 202 => 'Accepted',
1724 203 => 'Non-Authoritative Information',
1725 204 => 'No Content',
1726 205 => 'Reset Content',
1727 206 => 'Partial Content',
1728 300 => 'Multiple Choices',
1729 301 => 'Moved Permanently',
1730 302 => 'Found',
1731 303 => 'See Other',
1732 304 => 'Not Modified',
1733 305 => 'Use Proxy',
1734 306 => '(Unused)',
1735 307 => 'Temporary Redirect',
1736 400 => 'Bad Request',
1737 401 => 'Unauthorized',
1738 402 => 'Payment Required',
1739 403 => 'Forbidden',
1740 404 => 'Not Found',
1741 405 => 'Method Not Allowed',
1742 406 => 'Not Acceptable',
1743 407 => 'Proxy Authentication Required',
1744 408 => 'Request Timeout',
1745 409 => 'Conflict',
1746 410 => 'Gone',
1747 411 => 'Length Required',
1748 412 => 'Precondition Failed',
1749 413 => 'Request Entity Too Large',
1750 414 => 'Request-URI Too Long',
1751 415 => 'Unsupported Media Type',
1752 416 => 'Requested Range Not Satisfiable',
1753 417 => 'Expectation Failed',
1754 500 => 'Internal Server Error',
1755 501 => 'Not Implemented',
1756 502 => 'Bad Gateway',
1757 503 => 'Service Unavailable',
1758 504 => 'Gateway Timeout',
1759 505 => 'HTTP Version Not Supported',
1760 );
1761
1762 if ( false === $code ) {
1763 return $http_codes;
1764 }
1765
1766 return isset( $http_codes[ $code ] ) ? $http_codes[ $code ] : '';
1767 }
1768
1769 /**
1770 * Method valid_input_emails().
1771 *
1772 * @param string $emails Input emails string.
1773 *
1774 * @return string $valid_emails Valid emails string.
1775 */
1776 public static function valid_input_emails( $emails ) {
1777
1778 if ( is_string( $emails ) ) {
1779 $emails = array_filter( explode( ',', $emails ) );
1780 }
1781
1782 $valid_emails = array();
1783 if ( is_array( $emails ) ) {
1784 foreach ( $emails as $email ) {
1785 $email = esc_html( trim( $email ) );
1786 if ( ! empty( $email ) && ! in_array( $email, $valid_emails, true ) ) {
1787 $valid_emails[] = $email;
1788 }
1789 }
1790 }
1791 $valid_emails = implode( ',', $valid_emails );
1792 return $valid_emails;
1793 }
1794
1795 /**
1796 * Method check_image_file_name()
1797 *
1798 * Check if the file image.
1799 *
1800 * @param string $filename Contains image (file) name.
1801 *
1802 * @return true|false valid name or not.
1803 */
1804 public static function check_image_file_name( $filename ) {
1805 if ( validate_file( $filename ) ) {
1806 return false;
1807 }
1808
1809 $allowed_files = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'webp', 'heic' );
1810 $file_ext = array_values( array_slice( explode( '.', $filename ), -1 ) )[0];
1811 $file_ext = strtolower( $file_ext );
1812 if ( ! in_array( $file_ext, $allowed_files ) ) {
1813 return false;
1814 }
1815
1816 return true;
1817 }
1818
1819 /**
1820 * Method check_abandoned()
1821 *
1822 * Get site's icon.
1823 *
1824 * @param mixed $siteId site's id.
1825 * @param string $which to check plugin/theme.
1826 *
1827 * @return array result error or success
1828 * @throws \MainWP_Exception Error message.
1829 */
1830 public static function check_abandoned( $siteId = null, $which = '' ) { // phpcs:ignore -- NOSONAR - complex.
1831 if ( static::ctype_digit( $siteId ) ) {
1832 $website = MainWP_DB::instance()->get_website_by_id( $siteId );
1833 if ( MainWP_System_Utility::can_edit_website( $website ) ) {
1834 $error = '';
1835 try {
1836 $information = MainWP_Connect::fetch_url_authed( $website, 'check_abandoned', array( 'which' => $which ) );
1837 } catch ( MainWP_Exception $e ) {
1838 $error = $e->getMessage();
1839 }
1840
1841 if ( '' !== $error ) {
1842 return array( 'error' => $error );
1843 } elseif ( isset( $information['success'] ) && ! empty( $information['success'] ) ) {
1844 return array( 'result' => 'success' );
1845 } else {
1846 return array( 'undefined_error' => true );
1847 }
1848 }
1849 }
1850 return array( 'result' => 'NOSITE' );
1851 }
1852
1853 /**
1854 * Get directory or slug of plugin.
1855 *
1856 * @param string $slug Plugin slug.
1857 *
1858 * @return string $value directory or slug of plugin.
1859 */
1860 public static function get_dir_slug( $slug ) {
1861 $value = '';
1862 if ( false === strpos( $slug, '/' ) ) {
1863 if ( false !== strpos( $slug, '.' ) ) {
1864 $value = substr( $slug, 0, strpos( $slug, '.' ) );
1865 }
1866 } else {
1867 $value = dirname( $slug );
1868 }
1869 if ( empty( $value ) ) {
1870 return $slug;
1871 }
1872 return $value;
1873 }
1874
1875 /**
1876 * Metho get_siteview_mode().
1877 *
1878 * Get site view mode.
1879 *
1880 * @return string $viewmode Site view mode.
1881 */
1882 public static function get_siteview_mode() {
1883 $viewmode = get_user_option( 'mainwp_sitesviewmode' );
1884 if ( 'grid' !== $viewmode && 'table' !== $viewmode ) {
1885 $viewmode = 'table';
1886 }
1887 return $viewmode;
1888 }
1889
1890
1891 /**
1892 * Metho delete_file().
1893 *
1894 * Delete file.
1895 *
1896 * @param string $file_path File path.
1897 *
1898 * @return bool true|false.
1899 */
1900 public static function delete_file( $file_path ) {
1901
1902 global $wp_filesystem;
1903
1904 if ( ! empty( $file_path ) ) {
1905 if ( $wp_filesystem ) {
1906 if ( $wp_filesystem->exists( $file_path ) ) {
1907 $wp_filesystem->delete( $file_path );
1908 }
1909 } elseif ( file_exists( $file_path ) ) {
1910 wp_delete_file( $file_path );
1911 }
1912 return true;
1913 }
1914
1915 return false;
1916 }
1917
1918 /**
1919 * Method get_disable_functions()
1920 *
1921 * Get disable functions.
1922 *
1923 * @return string
1924 */
1925 public function get_disable_functions() {
1926 if ( null === static::$disabled_functions ) {
1927 static::$disabled_functions = ini_get( 'disable_functions' );
1928 }
1929 return static::$disabled_functions;
1930 }
1931
1932 /**
1933 * Method is_disable_functions()
1934 *
1935 * Check if it is disabled functions.
1936 *
1937 * @param string $func Function name to check.
1938 *
1939 * @return string
1940 */
1941 public function is_disabled_functions( $func ) {
1942 $dis_funcs = $this->get_disable_functions();
1943
1944 if ( ! empty( $dis_funcs ) && ( false !== stristr( $dis_funcs, $func ) ) ) {
1945 return true;
1946 }
1947 return false;
1948 }
1949
1950 /**
1951 * Method hook_verify_ping_nonce()
1952 *
1953 * Verify nonce without session and user id.
1954 *
1955 * @param bool $input_value Boolean value, it should always be FALSE.
1956 * @param string $nonce Nonce to verify.
1957 * @param mixed $siteid Site ID.
1958 *
1959 * @return mixed If verified return 1 or 2, if not return false.
1960 */
1961 public static function hook_verify_ping_nonce( $input_value, $nonce = '', $siteid = false ) {
1962 unset( $input_value );
1963 $action = 'pingnonce';
1964 return static::verify_site_nonce( $nonce, $action, $siteid );
1965 }
1966
1967 /**
1968 * Method create_site_nonce()
1969 *
1970 * Create action nonce for site.
1971 *
1972 * @param mixed $action Action to perform.
1973 * @param mixed $siteid Site ID.
1974 *
1975 * @return string Custom nonce.
1976 */
1977 public static function create_site_nonce( $action = - 1, $siteid = false ) {
1978 if ( empty( $action ) || empty( $siteid || ! is_numeric( $siteid ) ) ) {
1979 return false;
1980 }
1981 return substr( wp_hash( 'site|' . $siteid . '|' . $action, 'nonce' ), - 12, 10 );
1982 }
1983
1984 /**
1985 * Method verify_site_nonce()
1986 *
1987 * Verify nonce without session and user id.
1988 *
1989 * @param string $nonce Nonce to verify.
1990 * @param mixed $action Action to perform.
1991 * @param mixed $siteid Site ID.
1992 *
1993 * @return mixed If verified return 1 or 2, if not return false.
1994 */
1995 public static function verify_site_nonce( $nonce, $action = - 1, $siteid = 0 ) {
1996 $nonce = (string) $nonce;
1997 if ( empty( $nonce ) || empty( $siteid || ! is_numeric( $siteid ) ) ) {
1998 return false;
1999 }
2000
2001 $expected = substr( wp_hash( 'site|' . $siteid . '|' . $action, 'nonce' ), - 12, 10 );
2002 if ( hash_equals( $expected, $nonce ) ) {
2003 return 1;
2004 }
2005 return false;
2006 }
2007
2008
2009 /**
2010 * Find for multi keywords.
2011 *
2012 * @param string $name_str string find on.
2013 * @param array $words Array string input.
2014 * @return bool True|False.
2015 */
2016 public static function multi_find_keywords( $name_str, $words = array() ) {
2017 if ( ! is_array( $words ) ) {
2018 return false;
2019 }
2020 foreach ( $words as $word ) {
2021 if ( stristr( $name_str, $word ) ) {
2022 return true;
2023
2024 }
2025 }
2026 return false;
2027 }
2028
2029 /**
2030 * Merge values from right array to left array.
2031 *
2032 * @param array $left_array left array.
2033 * @param array $right_array right array.
2034 *
2035 * @return array $result result array.
2036 */
2037 public static function right_array_merge( $left_array, $right_array ) {
2038 if ( ! is_array( $left_array ) || ! is_array( $right_array ) ) {
2039 return array();
2040 }
2041 $result = array_intersect_key( $right_array, $left_array );
2042 return array_merge( $left_array, $result );
2043 }
2044
2045
2046 /**
2047 * Method get_set_deactivated_licenses_alerted().
2048 *
2049 * @param string $slug Extension slug.
2050 * @param bool $time_value Time value.
2051 * @param string $act get/set value.
2052 *
2053 * @return array $result result array.
2054 */
2055 public function get_set_deactivated_licenses_alerted( $slug, $time_value = false, $act = 'get' ) {
2056 if ( null === $this->last_deactivated_alerts ) {
2057 $this->last_deactivated_alerts = get_option( 'mainwp_cron_licenses_deactivated_alerted', array() );
2058 if ( ! is_array( $this->last_deactivated_alerts ) ) {
2059 $this->last_deactivated_alerts = array();
2060 }
2061 }
2062 if ( 'get' === $act ) {
2063 return isset( $this->last_deactivated_alerts[ $slug ] ) ? $this->last_deactivated_alerts[ $slug ] : 0;
2064 } elseif ( 'set' === $act ) {
2065 $this->last_deactivated_alerts[ $slug ] = intval( $time_value );
2066 get_option( 'mainwp_cron_licenses_deactivated_alerted', $this->last_deactivated_alerts );
2067 }
2068 }
2069
2070 /**
2071 * Method get_remote_favicon().
2072 *
2073 * @param string $url Url.
2074 * @param string $favi favicon file name.
2075 * @param int $item_id item id.
2076 * @param string $file_prefix favicon file prefix name.
2077 *
2078 * @return mixed result.
2079 */
2080 public static function get_remote_favicon( $url, $favi = '', $item_id = false, $file_prefix = '' ) { // phpcs:ignore -- NOSONAR - complex.
2081
2082 if ( empty( $favi ) ) {
2083 $favi = 'favicon.ico';
2084 }
2085
2086 if ( '/' !== substr( $url, - 1 ) ) {
2087 $url .= '/';
2088 }
2089
2090 $favi_url = $url . $favi;
2091
2092 $content = MainWP_Connect::get_file_content( $favi_url );
2093
2094 if ( empty( $content ) && 'favicon.ico' === $favi ) {
2095 $favi_url = $url . 'favicon.png';
2096 $content = MainWP_Connect::get_file_content( $favi_url ); // try other file.
2097 }
2098
2099 if ( ! empty( $content ) ) {
2100
2101 MainWP_System_Utility::get_wp_file_system();
2102
2103 global $wp_filesystem;
2104
2105 $dirs = MainWP_System_Utility::get_mainwp_dir( 'icons', true );
2106 $iconsDir = $dirs[0];
2107 if ( $favi ) {
2108
2109 $tmp = explode( '.', $favi );
2110 if ( 2 !== count( $tmp ) ) {
2111 return false;
2112 }
2113
2114 $favi_ext = $tmp[1];
2115
2116 if ( empty( $item_id ) ) {
2117 $item_id = time() . '-' . wp_rand( 100, 999 );
2118 }
2119 if ( ! empty( $file_prefix ) ) {
2120 $filename = $file_prefix . $item_id . '.' . $favi_ext;
2121 } else {
2122 $filename = 'favi-' . $item_id . '.' . $favi_ext;
2123 }
2124
2125 $size = $wp_filesystem->put_contents( $iconsDir . $filename, $content ); // phpcs:ignore --
2126 if ( $size ) {
2127 MainWP_Logger::instance()->debug( 'Icon Cost Product size :: ' . $size );
2128 return array(
2129 'result' => 'success',
2130 'file' => $filename,
2131 'dir' => $iconsDir,
2132 );
2133 } else {
2134 return array( 'error' => 'Save icon file failed.' );
2135 }
2136 }
2137 return false;
2138 } else {
2139 return array( 'error' => esc_html__( 'Download icon file failed', 'mainwp' ) );
2140 }
2141 }
2142
2143 /**
2144 * Method get_saved_favicon_url()
2145 *
2146 * @param string $favi Favicon file name.
2147 *
2148 * @return mixed $faviurl Favicon URL.
2149 */
2150 public static function get_saved_favicon_url( $favi ) {
2151 $faviurl = '';
2152 if ( ! empty( $favi ) ) {
2153 $dirs = MainWP_System_Utility::get_icons_dir();
2154 if ( file_exists( $dirs[0] . $favi ) ) {
2155 $faviurl = $dirs[1] . $favi;
2156 } else {
2157 $faviurl = '';
2158 }
2159 }
2160 return $faviurl;
2161 }
2162
2163 /**
2164 * Method delete_saved_favicon()
2165 *
2166 * @param string $favi Favicon file name.
2167 *
2168 * @return bool Success result.
2169 */
2170 public static function delete_saved_favicon( $favi ) {
2171 if ( ! empty( $favi ) ) {
2172 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
2173 global $wp_filesystem;
2174 $dirs = MainWP_System_Utility::get_icons_dir();
2175 if ( $hasWPFileSystem && $wp_filesystem->exists( $dirs[0] . $favi ) ) {
2176 $wp_filesystem->delete( $dirs[0] . $favi );
2177 return true;
2178 }
2179 }
2180 return false;
2181 }
2182
2183 /**
2184 * Delete icon file.
2185 *
2186 * @param string $sub_dir Sub dir file icon.
2187 * @param string $cost_icon file icon.
2188 */
2189 public function delete_uploaded_icon_file( $sub_dir, $cost_icon ) {
2190 $valid_file = 0 === validate_file( $cost_icon ) ? true : false;
2191 if ( $valid_file ) {
2192 $dirs = MainWP_System_Utility::get_mainwp_dir( $sub_dir, true );
2193 $f = $dirs[0] . $cost_icon;
2194 if ( file_exists( $f ) ) {
2195 wp_delete_file( $f );
2196 }
2197 }
2198 }
2199
2200 /**
2201 * Method get_table_orders().
2202 *
2203 * @param array $data table data.
2204 */
2205 public function get_table_orders( $data ) {
2206
2207 $values = array(
2208 'orderby' => null,
2209 'order' => null,
2210 );
2211
2212 if ( isset( $data['order'] ) ) {
2213 $columns = isset( $data['columns'] ) ? wp_unslash( $data['columns'] ) : array();
2214 $ord_col = isset( $data['order'][0]['column'] ) ? sanitize_text_field( wp_unslash( $data['order'][0]['column'] ) ) : '';
2215 if ( isset( $columns[ $ord_col ] ) ) {
2216 $values = array(
2217 'orderby' => isset( $columns[ $ord_col ]['data'] ) ? sanitize_text_field( wp_unslash( $columns[ $ord_col ]['data'] ) ) : '',
2218 'order' => isset( $data['order'][0]['dir'] ) ? sanitize_text_field( wp_unslash( $data['order'][0]['dir'] ) ) : '',
2219 );
2220 }
2221 }
2222
2223 return $values;
2224 }
2225
2226 /**
2227 * Method valid_file_check().
2228 *
2229 * @param string $path file path.
2230 * @param bool $readable readable.
2231 *
2232 * @return bool is valid.
2233 */
2234 public static function valid_file_check( $path, $readable = true ) {
2235 $valid = is_string( $path ) && ! stristr( $path, '..' );
2236 if ( $valid && $readable ) {
2237 $valid = is_readable( $path );
2238 }
2239 return $valid;
2240 }
2241
2242
2243 /**
2244 * Handle sanitize POST data.
2245 *
2246 * @param array $data input data.
2247 *
2248 * @return array
2249 */
2250 public function sanitize_data( $data ) {
2251 if ( ! is_array( $data ) ) {
2252 return array();
2253 }
2254
2255 // Sanitize all record values.
2256 return array_map(
2257 function ( $value ) {
2258 if ( ! is_array( $value ) ) {
2259 return wp_strip_all_tags( $value );
2260 }
2261
2262 return $value;
2263 },
2264 $data
2265 );
2266 }
2267
2268 /**
2269 * String ends by.
2270 *
2271 * @param mixed $str str.
2272 * @param mixed $ends ends.
2273 * @return bool value value.
2274 */
2275 public static function string_ends_by( $str, $ends ) {
2276 if ( function_exists( '\str_ends_with' ) ) {
2277 return \str_ends_with( $str, $ends );
2278 } else {
2279 $ends_len = strlen( $ends );
2280 if ( $ends_len > strlen( $str ) ) {
2281 return false;
2282 }
2283 return substr( $str, -$ends_len ) === $ends;
2284 }
2285 }
2286 /**
2287 * Returns number in shorter format.
2288 *
2289 * @param int $number str.
2290 * @return string $number Shorer number.
2291 */
2292 public static function short_number_format( $number ) {
2293 if ( $number > 999 && $number < 1000000 ) {
2294 // Anything between 1000 and 1000000.
2295 $number = number_format( $number / 1000, 1 ) . 'K';
2296 } elseif ( $number >= 1000000000 ) {
2297 // 1000000 or higher.
2298 $number = number_format( $number / 1000000, 2 ) . 'M';
2299 }
2300 return $number;
2301 }
2302
2303 /**
2304 * Returns date in time ago format
2305 *
2306 * @param mixed $ptime Date stamp.
2307 * @return string $string Time elapsed string.
2308 */
2309 public static function time_elapsed_string( $ptime ) {
2310 $etime = time() - $ptime;
2311
2312 if ( $etime < 1 ) {
2313 return '0 seconds';
2314 }
2315
2316 $a = array(
2317 365 * 24 * 60 * 60 => 'year',
2318 30 * 24 * 60 * 60 => 'month',
2319 24 * 60 * 60 => 'day',
2320 60 * 60 => 'hour',
2321 60 => 'minute',
2322 1 => 'second',
2323 );
2324 $a_plural = array(
2325 'year' => 'years',
2326 'month' => 'months',
2327 'day' => 'days',
2328 'hour' => 'hours',
2329 'minute' => 'minutes',
2330 'second' => 'seconds',
2331 );
2332
2333 foreach ( $a as $secs => $str ) {
2334 $d = $etime / $secs;
2335 if ( $d >= 1 ) {
2336 $r = round( $d );
2337 return $r . ' ' . ( $r > 1 ? $a_plural[ $str ] : $str ) . ' ago';
2338 }
2339 }
2340 }
2341
2342 /**
2343 * Returns language as flag.
2344 *
2345 * @param string $language Language code.
2346 * @return void
2347 */
2348 public static function get_language_code_as_flag( $language ) {
2349 // For flag extraction, remove trailing _formal or _informal if present.
2350 $flag_language = preg_replace( '/_(formal|informal)$/', '', $language );
2351 // Get the last 2 characters of the flag language code.
2352 $last_two_chars = ! empty( $flag_language ) ? substr( $flag_language, -2 ) : '';
2353 // Convert to lowercase.
2354 $lowercase_last_two_chars = strtolower( $last_two_chars );
2355 $lowercase_flag_language = strtolower( $flag_language );
2356
2357 // Get display name using the original language string.
2358 $display_language = function_exists( 'locale_get_display_name' ) ? locale_get_display_name( $language ) : $language;
2359
2360 // Adjust special country codes.
2361 if ( 'et' === $lowercase_last_two_chars ) {
2362 $lowercase_last_two_chars = 'ee';
2363 }
2364 if ( 'sq' === $lowercase_last_two_chars ) {
2365 $lowercase_last_two_chars = 'al';
2366 }
2367 if ( 'ab' === $lowercase_last_two_chars ) {
2368 $lowercase_last_two_chars = 'dz';
2369 }
2370
2371 $stacked_flags = array(
2372 'ca' => array(
2373 'primary' => 'es',
2374 'secondary' => 'ad',
2375 ),
2376 );
2377 // Only stack flags for explicit Catalan locales to avoid affecting Canada.
2378 $stacked_flag_locales = array(
2379 'ca' => array(
2380 'ca',
2381 ),
2382 );
2383
2384 if ( isset( $stacked_flags[ $lowercase_last_two_chars ] ) ) {
2385 $should_stack = true;
2386 if ( isset( $stacked_flag_locales[ $lowercase_last_two_chars ] ) ) {
2387 $should_stack = in_array( $lowercase_flag_language, $stacked_flag_locales[ $lowercase_last_two_chars ], true );
2388 }
2389
2390 if ( $should_stack ) {
2391 $primary_flag = $stacked_flags[ $lowercase_last_two_chars ]['primary'];
2392 $secondary_flag = $stacked_flags[ $lowercase_last_two_chars ]['secondary'];
2393
2394 echo '<span data-tooltip="' . esc_html__( 'Site Language: ', 'mainwp' ) . esc_attr( $display_language ) . '" data-position="left center" data-inverted="">';
2395 echo '<span class="mainwp-flag-stack">';
2396 echo '<i class="small ' . esc_attr( $primary_flag ) . ' flag mainwp-flag-stack__flag mainwp-flag-stack__flag--primary"></i>';
2397 echo '<i class="small ' . esc_attr( $secondary_flag ) . ' flag mainwp-flag-stack__flag mainwp-flag-stack__flag--secondary"></i>';
2398 echo '</span>';
2399 echo '</span>';
2400 return;
2401 }
2402 }
2403
2404 echo '<span data-tooltip="' . esc_html__( 'Site Language: ', 'mainwp' ) . esc_attr( $display_language ) . '" data-position="left center" data-inverted=""><i class="small ' . esc_attr( $lowercase_last_two_chars ) . ' flag"></i></span>';
2405 }
2406
2407 /**
2408 * Returns icon for the site indexability status.
2409 *
2410 * @param int $status Status, 1 or 0.
2411 * @return void.
2412 */
2413 public static function get_site_index_option_icon( $status ) {
2414 $icon = '';
2415 $tooltip = '';
2416 if ( isset( $status ) && '' !== $status ) {
2417 if ( 1 === intval( $status ) ) {
2418 $icon = 'green dot circle outline';
2419 $tooltip = 'Search engines can index this site.';
2420 } elseif ( 0 === intval( $status ) ) {
2421 $icon = 'red ban';
2422 $tooltip = 'This site is blocking search engines.';
2423 }
2424 } else {
2425 $icon = 'grey circle';
2426 $tooltip = 'Indexing status unknown. Resync the site or check manually in WordPress Settings > Reading.';
2427 }
2428 echo '<span data-tooltip="' . $tooltip . '" data-position="left center" data-inverted=""><i class="' . $icon . ' icon"></i></span>'; //phpcs:ignore -- ok.
2429 }
2430
2431 /**
2432 * Returns the appropriate Fomantic UI color class based on number of updates
2433 *
2434 * @param int $update_count Number of available updates.
2435 *
2436 * @return string CSS class for the element
2437 */
2438 public static function mainwp_get_update_count_class( $update_count ) {
2439 // Convert to integer using intval().
2440 $update_count = intval( $update_count );
2441
2442 // Ensure count is not negative.
2443 if ( 0 > $update_count ) {
2444 $update_count = 0;
2445 }
2446
2447 if ( 0 === $update_count ) {
2448 return 'grey';
2449 } elseif ( $update_count >= 1 && $update_count <= 3 ) {
2450 return 'yellow';
2451 } elseif ( $update_count >= 4 && $update_count <= 5 ) {
2452 return 'orange';
2453 } else {
2454 return 'red';
2455 }
2456 }
2457
2458 /**
2459 * Display site name and URL with optional WP Admin link.
2460 *
2461 * @param object|int $site Site object or Site ID.
2462 * @param bool $wp_admin Whether to show WP Admin link.
2463 * @param bool $print_content Whether to print or return the content.
2464 * @return string HTML markup for site display.
2465 */
2466 public static function mainwp_display_site( $site = '', $wp_admin = true, $print_content = false ) {
2467 if ( empty( $site ) ) {
2468 return '';
2469 }
2470
2471 if ( static::ctype_digit( $site ) ) {
2472 $website = MainWP_DB::instance()->get_website_by_id( $site );
2473 } elseif ( is_object( $site ) && isset( $site->id ) ) {
2474 $website = $site;
2475 } else {
2476 return '';
2477 }
2478
2479 if ( ! $website ) {
2480 return '';
2481 }
2482
2483 $site_name = esc_html( stripslashes( $website->name ) );
2484 $site_url = esc_url( $website->url );
2485 $nice_url = esc_html( static::get_nice_url( $website->url ) );
2486
2487 $html = '<div class="mainwp-site-display">';
2488
2489 // WP Admin link (if enabled and user has permission).
2490 if ( $wp_admin && \mainwp_current_user_can( 'dashboard', 'access_wpadmin_on_child_sites' ) ) {
2491 $admin_url = MainWP_Site_Open::get_open_site_url( $website->id, '', false );
2492 $html .= '<a href="' . esc_url( $admin_url ) . '" class="open_newwindow_wpadmin" target="_blank" data-tooltip="' . esc_attr__( 'Go to WP Admin', 'mainwp' ) . '" data-position="top left" data-inverted=""><i class="sign in icon"></i></a> ';
2493 } elseif ( $wp_admin ) {
2494 $html .= '<i class="sign in icon"></i> ';
2495 }
2496
2497 // Site name with dashboard link.
2498 $html .= '<a href="' . esc_url( admin_url( 'admin.php?page=managesites&dashboard=' . intval( $website->id ) ) ) . '">' . $site_name . '</a>';
2499
2500 // Site URL.
2501 $html .= '<div><span class="ui small text">';
2502 $html .= '<a href="' . $site_url . '" class="mainwp-may-hide-referrer open_site_url ui grey text" target="_blank">' . $nice_url . '</a>';
2503 $html .= '</span></div>';
2504
2505 $html .= '</div>';
2506
2507 if ( $print_content ) {
2508 echo $html; // phpcs:ignore -- ok.
2509 return '';
2510 }
2511
2512 return $html;
2513 }
2514
2515 /**
2516 * Generate a sortable BIGINT for WordPress versions.
2517 * Newer versions produce larger numbers.
2518 * Ordering matches WP core version_compare().
2519 *
2520 * @param string $version Version value.
2521 */
2522 public function wp_versions_order_num( $version ) {
2523
2524 $v = strtolower( trim( $version ) );
2525
2526 // 1) Normalize WordPress aliases.
2527 $v = str_replace(
2528 array( '-alpha', '-beta', '-rc' ),
2529 array( '-a', '-b', '-rc' ),
2530 $v
2531 );
2532
2533 // nightly / dev / trunk → dev.
2534 if ( preg_match( '/-(nightly|dev|trunk)/', $v ) ) {
2535 $v = preg_replace( '/-.+$/', '-dev', $v );
2536 }
2537
2538 // unknown tags → dev.
2539 if ( preg_match( '/-[a-z]+/', $v ) && ! preg_match( '/-(a|b|rc|dev)/', $v ) ) {
2540 $v = preg_replace( '/-.+$/', '-dev', $v );
2541 }
2542
2543 // 2) Extract numeric base version.
2544 $base = explode( '-', $v, 2 )[0];
2545 $parts = array_map( 'intval', explode( '.', $base ) );
2546
2547 if ( count( $parts ) < 4 ) {
2548 $parts = array_pad( $parts, 4, 0 );
2549 }
2550
2551 $versionNum =
2552 ( $parts[0] << 24 ) |
2553 ( $parts[1] << 16 ) |
2554 ( $parts[2] << 8 ) |
2555 $parts[3];
2556
2557 // 3) Release channel rank (lower = newer).
2558 $rank = 1; // stable.
2559 if ( strpos( $v, '-rc' ) !== false ) {
2560 $rank = 2;
2561 } elseif ( strpos( $v, '-b' ) !== false ) {
2562 $rank = 3;
2563 } elseif ( strpos( $v, '-a' ) !== false ) {
2564 $rank = 4;
2565 } elseif ( strpos( $v, '-dev' ) !== false ) {
2566 $rank = 5;
2567 }
2568
2569 // 4) Prerelease / build number.
2570 $build = 0;
2571 if ( preg_match( '/rc(\d+)/', $v, $m ) ) {
2572 $build = (int) $m[1];
2573 } elseif ( preg_match( '/b(\d+)/', $v, $m ) ) { // NOSONAR - same above ok.
2574 $build = (int) $m[1];
2575 } elseif ( preg_match( '/a(\d+)/', $v, $m ) ) { // NOSONAR - same above ok.
2576 $build = (int) $m[1];
2577 }
2578
2579 // 5) Compose sortable BIGINT.
2580 return ( $versionNum << 32 )
2581 | ( $rank << 29 )
2582 | min( $build, ( 1 << 29 ) - 1 );
2583 }
2584
2585 /**
2586 * Method handle gen_rand_id().
2587 *
2588 * @param string $str Input string.
2589 * @param bool $more_entropy More entropy False if a robust prefix is required, default false.
2590 * @return string Random ID 8 characters.
2591 */
2592 public static function gen_rand_id( $str = '', $more_entropy = false ) {
2593 return hash( 'crc32b', uniqid( $str, $more_entropy ? true : false ) );
2594 }
2595
2596 /**
2597 * Decode and validate JSON array.
2598 *
2599 * @param mixed $data Data to decode.
2600 */
2601 public static function get_decoded_array( $data ) {
2602 $decoded = ! empty( $data ) ? json_decode( $data, true ) : array();
2603 return is_array( $decoded ) ? $decoded : array();
2604 }
2605
2606 /**
2607 * Method get_use_cron()
2608 *
2609 * @return bool Whether use cron.
2610 */
2611 public static function get_use_cron() {
2612 return ( get_option( 'mainwp_wp_cron' ) === false ) || ( (int) get_option( 'mainwp_wp_cron' ) === 1 ) ? 1 : 0;
2613 }
2614 }
2615