PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 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 All 155 releases
mainwp / class / class-mainwp-utility.php

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

2,645 lines 82.1 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 * Un-hide a MainWP message for the current user.
1229 *
1230 * Removes the notice ID from the saved notice status so the message
1231 * can be displayed again.
1232 *
1233 * @param mixed $notice_id Notice ID.
1234 *
1235 * @return bool True if the notice was un-hidden, otherwise false.
1236 */
1237 public static function unhide_mainwp_message( $notice_id ) {
1238 $status = get_user_option( 'mainwp_notice_saved_status' );
1239
1240 if ( ! is_array( $status ) || ! isset( $status[ $notice_id ] ) ) {
1241 return false;
1242 }
1243
1244 unset( $status[ $notice_id ] );
1245
1246 if ( empty( $status ) ) {
1247 return delete_user_option( get_current_user_id(), 'mainwp_notice_saved_status' );
1248 }
1249
1250 return update_user_option(
1251 get_current_user_id(),
1252 'mainwp_notice_saved_status',
1253 $status
1254 );
1255 }
1256
1257 /**
1258 * Method get_hide_notice_status()
1259 *
1260 * Check whenther or not to show the MainWP Message.
1261 *
1262 * @param mixed $notice_id Notice ID.
1263 *
1264 * @return mixed true|false|time.
1265 */
1266 public static function get_hide_notice_status( $notice_id ) {
1267 $notices = get_user_option( 'mainwp_notice_saved_status' );
1268 if ( ! is_array( $notices ) ) {
1269 $notices = array();
1270 }
1271 if ( isset( $notices[ $notice_id ] ) ) {
1272 return $notices[ $notice_id ];
1273 }
1274 return false;
1275 }
1276
1277 /**
1278 * Method get_flash_message()
1279 *
1280 * Get saved flash Message.
1281 *
1282 * @param mixed $message_id Notice ID.
1283 * @param bool $delete True to delete the message after get it.
1284 *
1285 * @return boolean true|false.
1286 */
1287 public static function get_flash_message( $message_id, $delete = true ) {
1288 $flash_messages = get_user_option( 'mainwp_flash_messages' );
1289 if ( ! is_array( $flash_messages ) ) {
1290 $flash_messages = array();
1291 }
1292 if ( ! isset( $flash_messages[ $message_id ] ) ) {
1293 return false;
1294 }
1295 $content = $flash_messages[ $message_id ];
1296 if ( $delete ) {
1297 unset( $flash_messages[ $message_id ] );
1298 static::update_user_option( 'mainwp_flash_messages', $flash_messages );
1299 }
1300 return $content;
1301 }
1302
1303 /**
1304 * Method set_short_term_notice()
1305 *
1306 * Set new temporary notice.
1307 *
1308 * @param string $monitor Monitor.
1309 * @param string $noti_key Notice key.
1310 * @param int $checked_at Notice checked at.
1311 *
1312 * @return void.
1313 */
1314 public static function set_short_term_notice( $monitor, $noti_key, $checked_at ) {
1315 if ( empty( $monitor ) || empty( $noti_key ) ) {
1316 return;
1317 }
1318 $noti_handle_name = static::SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $noti_key;
1319 static::update_option( $noti_handle_name, $checked_at );
1320 }
1321
1322
1323 /**
1324 * Method is_short_term_notice()
1325 *
1326 * Check whether the short-term notice is still active.
1327 *
1328 * @param string $monitor Monitor name.
1329 * @param string $noti_key Notice key.
1330 *
1331 * @return boolean true|false.
1332 */
1333 public static function is_short_term_notice( $monitor, $noti_key ) {
1334
1335 if ( empty( $monitor ) || empty( $noti_key ) ) {
1336 return false;
1337 }
1338
1339 $noti_handle_name = static::SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $noti_key;
1340
1341 $notice_time = get_option( $noti_handle_name );
1342
1343 if ( false === $notice_time ) {
1344 return false;
1345 }
1346
1347 if ( ! is_numeric( $notice_time ) ) {
1348 delete_option( $noti_handle_name );
1349 }
1350
1351 if ( time() - (int) $notice_time < self::SHORT_TERM_NOTICE_TTL ) {
1352 if ( static::is_short_term_dismissed_notice( $monitor, $noti_key ) ) {
1353 return false;
1354 }
1355 return true;
1356 } else {
1357 delete_option( $noti_handle_name );
1358 }
1359 return false;
1360 }
1361
1362
1363 /**
1364 * Method is_short_term_dismissed_notice()
1365 *
1366 * Check whether the short-term notice is user dismissed.
1367 *
1368 * @param string $monitor Monitor.
1369 * @param string $noti_key Notice key.
1370 *
1371 * @return boolean true|false.
1372 */
1373 public static function is_short_term_dismissed_notice( $monitor, $noti_key ) {
1374 if ( empty( $monitor ) || empty( $noti_key ) ) {
1375 return false;
1376 }
1377 $user_noti_handle = static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $noti_key;
1378 $user = wp_get_current_user();
1379 if ( $user && ! empty( $user->ID ) ) {
1380 return 'dismissed' === get_option( $user_noti_handle . '_user_' . $user->ID );
1381 }
1382 return false;
1383 }
1384
1385
1386 /**
1387 * Purge short-term notices.
1388 *
1389 * @param string $monitor Optional monitor name. When provided, only
1390 * notices for the specified monitor are purged.
1391 * @param bool $expired_only Whether to purge only expired notices.
1392 *
1393 * @return void
1394 */
1395 public static function purge_short_term_notices( $monitor = '', $expired_only = true ) { // phpcs:ignore -- NOSONAR - complex.
1396
1397 $opt_prefix = self::SHORT_TERM_NOTICE_OPTION_PREFIX;
1398
1399 $prefix = '' !== $monitor ? $monitor . '_' : '';
1400
1401 $timeout = time() - self::SHORT_TERM_NOTICE_TTL;
1402
1403 $db = MainWP_DB::instance()->get_wpdb_instance();
1404
1405 $sql = $db->prepare(
1406 "
1407 SELECT option_name
1408 FROM {$db->options}
1409 WHERE option_name LIKE %s
1410 ",
1411 $db->esc_like( $opt_prefix . (string) $prefix ) . '%',
1412 );
1413
1414 if ( $expired_only ) {
1415 $sql .= $db->prepare(
1416 ' AND CAST(option_value AS UNSIGNED) <= %d ',
1417 $timeout
1418 );
1419 }
1420
1421 $options = $db->get_col( $sql );
1422
1423 foreach ( $options as $option_name ) {
1424 delete_option( $option_name );
1425 }
1426 }
1427
1428
1429 /**
1430 * Purge short-term notices for a specific issue.
1431 *
1432 * @param string $monitor Monitor name.
1433 * @param string $issue_code Issue code.
1434 *
1435 * @return bool True if the operation completed, false if the input is invalid.
1436 */
1437 public static function delete_short_term_notices_for_issue( $monitor, $issue_code ) {
1438
1439 if ( empty( $monitor ) || empty( $issue_code ) ) {
1440 return false;
1441 }
1442
1443 $opt_prefix = self::SHORT_TERM_NOTICE_OPTION_PREFIX;
1444 $prefix = $monitor . '_' . $issue_code;
1445
1446 $db = MainWP_DB::instance()->get_wpdb_instance();
1447
1448 $sql = $db->prepare(
1449 "
1450 SELECT option_name
1451 FROM {$db->options}
1452 WHERE option_name LIKE %s
1453 ",
1454 $db->esc_like( $opt_prefix . $prefix ) . '%'
1455 );
1456
1457 $options = $db->get_col( $sql );
1458
1459 foreach ( $options as $option_name ) {
1460 delete_option( $option_name ); // Delete options and cache.
1461 }
1462
1463 return true;
1464 }
1465
1466 /**
1467 * Method dismiss_short_term_notice()
1468 *
1469 * Hide short term notice.
1470 *
1471 * @param string $monitor_noti_key Notice key.
1472 *
1473 * @return void.
1474 */
1475 public static function dismiss_short_term_notice( $monitor_noti_key ) {
1476 $noti_handle_name = static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor_noti_key;
1477 $user = wp_get_current_user();
1478 if ( $user && ! empty( $user->ID ) ) {
1479 static::update_option( $noti_handle_name . '_user_' . $user->ID, 'dismissed' );
1480 }
1481 }
1482
1483 /**
1484 * Delete user short-term notice options for the specified monitor.
1485 *
1486 * Optionally preserves the dismissed state for the specified issue codes.
1487 *
1488 * @param string $monitor Monitor name.
1489 * @param array $preserved_issues Issue codes whose dismissed state should be preserved.
1490 *
1491 * @return void
1492 */
1493 public static function delete_short_term_notice_options( $monitor, $preserved_issues = array() ) {
1494
1495 if ( empty( $monitor ) ) {
1496 return;
1497 }
1498
1499 $db = MainWP_DB::instance()->get_wpdb_instance();
1500
1501 $sql = "
1502 SELECT option_name
1503 FROM {$db->options}
1504 WHERE option_name LIKE %s
1505 ";
1506
1507 $args = array(
1508 $db->esc_like(
1509 static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_'
1510 ) . '%',
1511 );
1512
1513 // Preserve the user's dismissed state for the specified issues.
1514 if ( ! empty( $preserved_issues ) && is_array( $preserved_issues ) ) {
1515 foreach ( $preserved_issues as $issue_code ) {
1516 $sql .= ' AND option_name NOT LIKE %s';
1517 $args[] = $db->esc_like(
1518 static::USER_SHORT_TERM_NOTICE_OPTION_PREFIX . $monitor . '_' . $issue_code . '_user_'
1519 ) . '%';
1520 }
1521 }
1522
1523 $option_names = $db->get_col(
1524 $db->prepare( $sql, ...$args )
1525 );
1526
1527 if ( empty( $option_names ) ) {
1528 return;
1529 }
1530
1531 foreach ( $option_names as $option_name ) {
1532 delete_option( $option_name );
1533 }
1534 }
1535
1536 /**
1537 * Method update_flash_message()
1538 *
1539 * Check whenther or not to show the MainWP Message.
1540 *
1541 * @param mixed $message_id Notice ID.
1542 * @param mixed $content Content of message.
1543 *
1544 * @return boolean true|false.
1545 */
1546 public static function update_flash_message( $message_id, $content ) {
1547 $flash_messages = get_user_option( 'mainwp_flash_messages' );
1548 if ( ! is_array( $flash_messages ) ) {
1549 $flash_messages = array();
1550 }
1551 $current = isset( $flash_messages[ $message_id ] ) ? $flash_messages[ $message_id ] : '';
1552 if ( empty( $current ) ) {
1553 $current = $content;
1554 } else {
1555 $current .= '|' . $content;
1556 }
1557 $flash_messages[ $message_id ] = $current;
1558 return static::update_user_option( 'mainwp_flash_messages', $flash_messages );
1559 }
1560
1561 /**
1562 * Method array_sort()
1563 *
1564 * Sort given array by given flags.
1565 *
1566 * @param mixed $arr Array to sort.
1567 * @param mixed $key Array key.
1568 * @param string $sort_flag Flags to sort by. Default = SORT_STRING.
1569 */
1570 public static function array_sort( &$arr, $key, $sort_flag = SORT_STRING ) {
1571 $sorter = array();
1572 $ret = array();
1573 reset( $arr );
1574 foreach ( $arr as $ii => $val ) {
1575 if ( isset( $val[ $key ] ) ) {
1576 $sorter[ $ii ] = $val[ $key ];
1577 } elseif ( SORT_NUMERIC === $sort_flag ) {
1578 $sorter[ $ii ] = count( $sorter );
1579 }
1580 }
1581 asort( $sorter, $sort_flag );
1582 foreach ( $sorter as $ii => $val ) {
1583 $ret[ $ii ] = $arr[ $ii ];
1584 }
1585 $arr = $ret;
1586 }
1587
1588 /**
1589 * Method array_sort_existed_keys()
1590 *
1591 * Sort given array by given flags.
1592 *
1593 * @param mixed $arr Array to sort.
1594 * @param mixed $key Array key.
1595 * @param string $sort_flag Flags to sort by. Default = SORT_STRING.
1596 */
1597 public static function array_sort_existed_keys( &$arr, $key, $sort_flag = SORT_STRING ) {
1598 $sorter = array();
1599 $ret = array();
1600 reset( $arr );
1601
1602 // get items with $key to sort.
1603 foreach ( $arr as $ii => $val ) {
1604 if ( isset( $val[ $key ] ) ) {
1605 $sorter[ $ii ] = $val[ $key ];
1606 }
1607 }
1608 asort( $sorter, $sort_flag );
1609
1610 foreach ( $sorter as $ii => $val ) {
1611 $ret[ $ii ] = $arr[ $ii ];
1612 }
1613
1614 // asign other items (without $keys).
1615 foreach ( $arr as $ii => $val ) {
1616 if ( ! isset( $val[ $key ] ) ) {
1617 $ret[ $ii ] = $val;
1618 }
1619 }
1620
1621 $arr = $ret;
1622 }
1623
1624 /**
1625 * Method numeric_filter()
1626 *
1627 * Filter given numeric.
1628 *
1629 * @param int $int_num Int number.
1630 * @return array $arr_ints Array filtered.
1631 */
1632 public static function numeric_filter( $int_num ) {
1633 return ( (string) (int) $int_num === (string) $int_num && 0 < $int_num ) ? $int_num : false;
1634 }
1635
1636 /**
1637 * Method array_numeric_filter()
1638 *
1639 * Filter given numeric array.
1640 *
1641 * @param array $arr_ints Array to filter.
1642 * @return array $arr_ints Array filtered.
1643 */
1644 public static function array_numeric_filter( $arr_ints ) {
1645 $arr_ints = array_filter(
1646 $arr_ints,
1647 function ( $e ) {
1648 return ( (string) (int) $e === (string) $e && 0 < $e ) ? true : false;
1649 }
1650 );
1651 return $arr_ints;
1652 }
1653
1654 /**
1655 * Method enabled_wp_seo()
1656 *
1657 * Check if Yoast SEO is enabled.
1658 *
1659 * @return boolean true|false.
1660 */
1661 public static function enabled_wp_seo() {
1662 if ( null === static::$enabled_wp_seo ) {
1663 static::$enabled_wp_seo = is_plugin_active( 'wordpress-seo-extension/wordpress-seo-extension.php' );
1664 }
1665 return static::$enabled_wp_seo;
1666 }
1667
1668 /**
1669 * Method value_to_string()
1670 *
1671 * Value to string.
1672 *
1673 * @param mixed $var_value Value to convert to string.
1674 *
1675 * @return string Value that has been converted into a string.
1676 */
1677 public static function value_to_string( $var_value ) {
1678 if ( is_array( $var_value ) || is_object( $var_value ) ) {
1679 //phpcs:ignore -- for debug only
1680 return print_r( $var_value, true );
1681 } elseif ( is_string( $var_value ) ) {
1682 return $var_value;
1683 }
1684 return '';
1685 }
1686
1687 /**
1688 * Get Health Site value.
1689 *
1690 * @param mixed $issue_counts Health site issues.
1691 *
1692 * @return array Health status value.
1693 */
1694 public static function get_site_health( $issue_counts ) {
1695
1696 // Coerce non-array/empty/partial input to a full set of counts so a scalar
1697 // (e.g. a JSON-decoded string) or a missing key never fatals or warns.
1698 $issue_counts = array_merge(
1699 array(
1700 'good' => 0,
1701 'recommended' => 0,
1702 'critical' => 0,
1703 ),
1704 is_array( $issue_counts ) ? $issue_counts : array()
1705 );
1706
1707 // Normalize each counter to a non-negative integer before arithmetic so a
1708 // non-numeric or negative stored value cannot fatal or skew the score.
1709 // is_numeric() first: intval() maps a non-empty array to 1, which would
1710 // count a malformed nested value as a real issue.
1711 $good = is_numeric( $issue_counts['good'] ) ? max( 0, intval( $issue_counts['good'] ) ) : 0;
1712 $recommended = is_numeric( $issue_counts['recommended'] ) ? max( 0, intval( $issue_counts['recommended'] ) ) : 0;
1713 $critical = is_numeric( $issue_counts['critical'] ) ? max( 0, intval( $issue_counts['critical'] ) ) : 0;
1714
1715 $totalTests = $good + $recommended + $critical * 1.5;
1716 $failedTests = $recommended * 0.5 + $critical * 1.5;
1717
1718 if ( empty( $totalTests ) ) {
1719 $val = 100;
1720 } else {
1721 $val = 100 - ceil( ( $failedTests / $totalTests ) * 100 );
1722 }
1723
1724 if ( 0 > $val ) {
1725 $val = 0;
1726 }
1727
1728 if ( 100 < $val ) {
1729 $val = 100;
1730 }
1731
1732 return array(
1733 'val' => $val,
1734 'critical' => $critical,
1735 );
1736 }
1737
1738
1739 /**
1740 * Get HTTP code.
1741 *
1742 * @param int $code HTTP code.
1743 *
1744 * @return array $http_codes HTTP code.
1745 */
1746 public static function get_http_codes( $code = false ) {
1747
1748 $http_codes = array(
1749 100 => 'Continue',
1750 101 => 'Switching Protocols',
1751 200 => 'OK',
1752 201 => 'Created',
1753 202 => 'Accepted',
1754 203 => 'Non-Authoritative Information',
1755 204 => 'No Content',
1756 205 => 'Reset Content',
1757 206 => 'Partial Content',
1758 300 => 'Multiple Choices',
1759 301 => 'Moved Permanently',
1760 302 => 'Found',
1761 303 => 'See Other',
1762 304 => 'Not Modified',
1763 305 => 'Use Proxy',
1764 306 => '(Unused)',
1765 307 => 'Temporary Redirect',
1766 400 => 'Bad Request',
1767 401 => 'Unauthorized',
1768 402 => 'Payment Required',
1769 403 => 'Forbidden',
1770 404 => 'Not Found',
1771 405 => 'Method Not Allowed',
1772 406 => 'Not Acceptable',
1773 407 => 'Proxy Authentication Required',
1774 408 => 'Request Timeout',
1775 409 => 'Conflict',
1776 410 => 'Gone',
1777 411 => 'Length Required',
1778 412 => 'Precondition Failed',
1779 413 => 'Request Entity Too Large',
1780 414 => 'Request-URI Too Long',
1781 415 => 'Unsupported Media Type',
1782 416 => 'Requested Range Not Satisfiable',
1783 417 => 'Expectation Failed',
1784 500 => 'Internal Server Error',
1785 501 => 'Not Implemented',
1786 502 => 'Bad Gateway',
1787 503 => 'Service Unavailable',
1788 504 => 'Gateway Timeout',
1789 505 => 'HTTP Version Not Supported',
1790 );
1791
1792 if ( false === $code ) {
1793 return $http_codes;
1794 }
1795
1796 return isset( $http_codes[ $code ] ) ? $http_codes[ $code ] : '';
1797 }
1798
1799 /**
1800 * Method valid_input_emails().
1801 *
1802 * @param string $emails Input emails string.
1803 *
1804 * @return string $valid_emails Valid emails string.
1805 */
1806 public static function valid_input_emails( $emails ) {
1807
1808 if ( is_string( $emails ) ) {
1809 $emails = array_filter( explode( ',', $emails ) );
1810 }
1811
1812 $valid_emails = array();
1813 if ( is_array( $emails ) ) {
1814 foreach ( $emails as $email ) {
1815 $email = esc_html( trim( $email ) );
1816 if ( ! empty( $email ) && ! in_array( $email, $valid_emails, true ) ) {
1817 $valid_emails[] = $email;
1818 }
1819 }
1820 }
1821 $valid_emails = implode( ',', $valid_emails );
1822 return $valid_emails;
1823 }
1824
1825 /**
1826 * Method check_image_file_name()
1827 *
1828 * Check if the file image.
1829 *
1830 * @param string $filename Contains image (file) name.
1831 *
1832 * @return true|false valid name or not.
1833 */
1834 public static function check_image_file_name( $filename ) {
1835 if ( validate_file( $filename ) ) {
1836 return false;
1837 }
1838
1839 $allowed_files = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'webp', 'heic' );
1840 $file_ext = array_values( array_slice( explode( '.', $filename ), -1 ) )[0];
1841 $file_ext = strtolower( $file_ext );
1842 if ( ! in_array( $file_ext, $allowed_files ) ) {
1843 return false;
1844 }
1845
1846 return true;
1847 }
1848
1849 /**
1850 * Method check_abandoned()
1851 *
1852 * Get site's icon.
1853 *
1854 * @param mixed $siteId site's id.
1855 * @param string $which to check plugin/theme.
1856 *
1857 * @return array result error or success
1858 * @throws \MainWP_Exception Error message.
1859 */
1860 public static function check_abandoned( $siteId = null, $which = '' ) { // phpcs:ignore -- NOSONAR - complex.
1861 if ( static::ctype_digit( $siteId ) ) {
1862 $website = MainWP_DB::instance()->get_website_by_id( $siteId );
1863 if ( MainWP_System_Utility::can_edit_website( $website ) ) {
1864 $error = '';
1865 try {
1866 $information = MainWP_Connect::fetch_url_authed( $website, 'check_abandoned', array( 'which' => $which ) );
1867 } catch ( MainWP_Exception $e ) {
1868 $error = $e->getMessage();
1869 }
1870
1871 if ( '' !== $error ) {
1872 return array( 'error' => $error );
1873 } elseif ( isset( $information['success'] ) && ! empty( $information['success'] ) ) {
1874 return array( 'result' => 'success' );
1875 } else {
1876 return array( 'undefined_error' => true );
1877 }
1878 }
1879 }
1880 return array( 'result' => 'NOSITE' );
1881 }
1882
1883 /**
1884 * Get directory or slug of plugin.
1885 *
1886 * @param string $slug Plugin slug.
1887 *
1888 * @return string $value directory or slug of plugin.
1889 */
1890 public static function get_dir_slug( $slug ) {
1891 $value = '';
1892 if ( false === strpos( $slug, '/' ) ) {
1893 if ( false !== strpos( $slug, '.' ) ) {
1894 $value = substr( $slug, 0, strpos( $slug, '.' ) );
1895 }
1896 } else {
1897 $value = dirname( $slug );
1898 }
1899 if ( empty( $value ) ) {
1900 return $slug;
1901 }
1902 return $value;
1903 }
1904
1905 /**
1906 * Metho get_siteview_mode().
1907 *
1908 * Get site view mode.
1909 *
1910 * @return string $viewmode Site view mode.
1911 */
1912 public static function get_siteview_mode() {
1913 $viewmode = get_user_option( 'mainwp_sitesviewmode' );
1914 if ( 'grid' !== $viewmode && 'table' !== $viewmode ) {
1915 $viewmode = 'table';
1916 }
1917 return $viewmode;
1918 }
1919
1920
1921 /**
1922 * Metho delete_file().
1923 *
1924 * Delete file.
1925 *
1926 * @param string $file_path File path.
1927 *
1928 * @return bool true|false.
1929 */
1930 public static function delete_file( $file_path ) {
1931
1932 global $wp_filesystem;
1933
1934 if ( ! empty( $file_path ) ) {
1935 if ( $wp_filesystem ) {
1936 if ( $wp_filesystem->exists( $file_path ) ) {
1937 $wp_filesystem->delete( $file_path );
1938 }
1939 } elseif ( file_exists( $file_path ) ) {
1940 wp_delete_file( $file_path );
1941 }
1942 return true;
1943 }
1944
1945 return false;
1946 }
1947
1948 /**
1949 * Method get_disable_functions()
1950 *
1951 * Get disable functions.
1952 *
1953 * @return string
1954 */
1955 public function get_disable_functions() {
1956 if ( null === static::$disabled_functions ) {
1957 static::$disabled_functions = ini_get( 'disable_functions' );
1958 }
1959 return static::$disabled_functions;
1960 }
1961
1962 /**
1963 * Method is_disable_functions()
1964 *
1965 * Check if it is disabled functions.
1966 *
1967 * @param string $func Function name to check.
1968 *
1969 * @return string
1970 */
1971 public function is_disabled_functions( $func ) {
1972 $dis_funcs = $this->get_disable_functions();
1973
1974 if ( ! empty( $dis_funcs ) && ( false !== stristr( $dis_funcs, $func ) ) ) {
1975 return true;
1976 }
1977 return false;
1978 }
1979
1980 /**
1981 * Method hook_verify_ping_nonce()
1982 *
1983 * Verify nonce without session and user id.
1984 *
1985 * @param bool $input_value Boolean value, it should always be FALSE.
1986 * @param string $nonce Nonce to verify.
1987 * @param mixed $siteid Site ID.
1988 *
1989 * @return mixed If verified return 1 or 2, if not return false.
1990 */
1991 public static function hook_verify_ping_nonce( $input_value, $nonce = '', $siteid = false ) {
1992 unset( $input_value );
1993 $action = 'pingnonce';
1994 return static::verify_site_nonce( $nonce, $action, $siteid );
1995 }
1996
1997 /**
1998 * Method create_site_nonce()
1999 *
2000 * Create action nonce for site.
2001 *
2002 * @param mixed $action Action to perform.
2003 * @param mixed $siteid Site ID.
2004 *
2005 * @return string Custom nonce.
2006 */
2007 public static function create_site_nonce( $action = - 1, $siteid = false ) {
2008 if ( empty( $action ) || empty( $siteid || ! is_numeric( $siteid ) ) ) {
2009 return false;
2010 }
2011 return substr( wp_hash( 'site|' . $siteid . '|' . $action, 'nonce' ), - 12, 10 );
2012 }
2013
2014 /**
2015 * Method verify_site_nonce()
2016 *
2017 * Verify nonce without session and user id.
2018 *
2019 * @param string $nonce Nonce to verify.
2020 * @param mixed $action Action to perform.
2021 * @param mixed $siteid Site ID.
2022 *
2023 * @return mixed If verified return 1 or 2, if not return false.
2024 */
2025 public static function verify_site_nonce( $nonce, $action = - 1, $siteid = 0 ) {
2026 $nonce = (string) $nonce;
2027 if ( empty( $nonce ) || empty( $siteid || ! is_numeric( $siteid ) ) ) {
2028 return false;
2029 }
2030
2031 $expected = substr( wp_hash( 'site|' . $siteid . '|' . $action, 'nonce' ), - 12, 10 );
2032 if ( hash_equals( $expected, $nonce ) ) {
2033 return 1;
2034 }
2035 return false;
2036 }
2037
2038
2039 /**
2040 * Find for multi keywords.
2041 *
2042 * @param string $name_str string find on.
2043 * @param array $words Array string input.
2044 * @return bool True|False.
2045 */
2046 public static function multi_find_keywords( $name_str, $words = array() ) {
2047 if ( ! is_array( $words ) ) {
2048 return false;
2049 }
2050 foreach ( $words as $word ) {
2051 if ( stristr( $name_str, $word ) ) {
2052 return true;
2053
2054 }
2055 }
2056 return false;
2057 }
2058
2059 /**
2060 * Merge values from right array to left array.
2061 *
2062 * @param array $left_array left array.
2063 * @param array $right_array right array.
2064 *
2065 * @return array $result result array.
2066 */
2067 public static function right_array_merge( $left_array, $right_array ) {
2068 if ( ! is_array( $left_array ) || ! is_array( $right_array ) ) {
2069 return array();
2070 }
2071 $result = array_intersect_key( $right_array, $left_array );
2072 return array_merge( $left_array, $result );
2073 }
2074
2075
2076 /**
2077 * Method get_set_deactivated_licenses_alerted().
2078 *
2079 * @param string $slug Extension slug.
2080 * @param bool $time_value Time value.
2081 * @param string $act get/set value.
2082 *
2083 * @return array $result result array.
2084 */
2085 public function get_set_deactivated_licenses_alerted( $slug, $time_value = false, $act = 'get' ) {
2086 if ( null === $this->last_deactivated_alerts ) {
2087 $this->last_deactivated_alerts = get_option( 'mainwp_cron_licenses_deactivated_alerted', array() );
2088 if ( ! is_array( $this->last_deactivated_alerts ) ) {
2089 $this->last_deactivated_alerts = array();
2090 }
2091 }
2092 if ( 'get' === $act ) {
2093 return isset( $this->last_deactivated_alerts[ $slug ] ) ? $this->last_deactivated_alerts[ $slug ] : 0;
2094 } elseif ( 'set' === $act ) {
2095 $this->last_deactivated_alerts[ $slug ] = intval( $time_value );
2096 get_option( 'mainwp_cron_licenses_deactivated_alerted', $this->last_deactivated_alerts );
2097 }
2098 }
2099
2100 /**
2101 * Method get_remote_favicon().
2102 *
2103 * @param string $url Url.
2104 * @param string $favi favicon file name.
2105 * @param int $item_id item id.
2106 * @param string $file_prefix favicon file prefix name.
2107 *
2108 * @return mixed result.
2109 */
2110 public static function get_remote_favicon( $url, $favi = '', $item_id = false, $file_prefix = '' ) { // phpcs:ignore -- NOSONAR - complex.
2111
2112 if ( empty( $favi ) ) {
2113 $favi = 'favicon.ico';
2114 }
2115
2116 if ( '/' !== substr( $url, - 1 ) ) {
2117 $url .= '/';
2118 }
2119
2120 $favi_url = $url . $favi;
2121
2122 $content = MainWP_Connect::get_file_content( $favi_url );
2123
2124 if ( empty( $content ) && 'favicon.ico' === $favi ) {
2125 $favi_url = $url . 'favicon.png';
2126 $content = MainWP_Connect::get_file_content( $favi_url ); // try other file.
2127 }
2128
2129 if ( ! empty( $content ) ) {
2130
2131 MainWP_System_Utility::get_wp_file_system();
2132
2133 global $wp_filesystem;
2134
2135 $dirs = MainWP_System_Utility::get_mainwp_dir( 'icons', true );
2136 $iconsDir = $dirs[0];
2137 if ( $favi ) {
2138
2139 $tmp = explode( '.', $favi );
2140 if ( 2 !== count( $tmp ) ) {
2141 return false;
2142 }
2143
2144 $favi_ext = $tmp[1];
2145
2146 if ( empty( $item_id ) ) {
2147 $item_id = time() . '-' . wp_rand( 100, 999 );
2148 }
2149 if ( ! empty( $file_prefix ) ) {
2150 $filename = $file_prefix . $item_id . '.' . $favi_ext;
2151 } else {
2152 $filename = 'favi-' . $item_id . '.' . $favi_ext;
2153 }
2154
2155 $size = $wp_filesystem->put_contents( $iconsDir . $filename, $content ); // phpcs:ignore --
2156 if ( $size ) {
2157 MainWP_Logger::instance()->debug( 'Icon Cost Product size :: ' . $size );
2158 return array(
2159 'result' => 'success',
2160 'file' => $filename,
2161 'dir' => $iconsDir,
2162 );
2163 } else {
2164 return array( 'error' => 'Save icon file failed.' );
2165 }
2166 }
2167 return false;
2168 } else {
2169 return array( 'error' => esc_html__( 'Download icon file failed', 'mainwp' ) );
2170 }
2171 }
2172
2173 /**
2174 * Method get_saved_favicon_url()
2175 *
2176 * @param string $favi Favicon file name.
2177 *
2178 * @return mixed $faviurl Favicon URL.
2179 */
2180 public static function get_saved_favicon_url( $favi ) {
2181 $faviurl = '';
2182 if ( ! empty( $favi ) ) {
2183 $dirs = MainWP_System_Utility::get_icons_dir();
2184 if ( file_exists( $dirs[0] . $favi ) ) {
2185 $faviurl = $dirs[1] . $favi;
2186 } else {
2187 $faviurl = '';
2188 }
2189 }
2190 return $faviurl;
2191 }
2192
2193 /**
2194 * Method delete_saved_favicon()
2195 *
2196 * @param string $favi Favicon file name.
2197 *
2198 * @return bool Success result.
2199 */
2200 public static function delete_saved_favicon( $favi ) {
2201 if ( ! empty( $favi ) ) {
2202 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
2203 global $wp_filesystem;
2204 $dirs = MainWP_System_Utility::get_icons_dir();
2205 if ( $hasWPFileSystem && $wp_filesystem->exists( $dirs[0] . $favi ) ) {
2206 $wp_filesystem->delete( $dirs[0] . $favi );
2207 return true;
2208 }
2209 }
2210 return false;
2211 }
2212
2213 /**
2214 * Delete icon file.
2215 *
2216 * @param string $sub_dir Sub dir file icon.
2217 * @param string $cost_icon file icon.
2218 */
2219 public function delete_uploaded_icon_file( $sub_dir, $cost_icon ) {
2220 $valid_file = 0 === validate_file( $cost_icon ) ? true : false;
2221 if ( $valid_file ) {
2222 $dirs = MainWP_System_Utility::get_mainwp_dir( $sub_dir, true );
2223 $f = $dirs[0] . $cost_icon;
2224 if ( file_exists( $f ) ) {
2225 wp_delete_file( $f );
2226 }
2227 }
2228 }
2229
2230 /**
2231 * Method get_table_orders().
2232 *
2233 * @param array $data table data.
2234 */
2235 public function get_table_orders( $data ) {
2236
2237 $values = array(
2238 'orderby' => null,
2239 'order' => null,
2240 );
2241
2242 if ( isset( $data['order'] ) ) {
2243 $columns = isset( $data['columns'] ) ? wp_unslash( $data['columns'] ) : array();
2244 $ord_col = isset( $data['order'][0]['column'] ) ? sanitize_text_field( wp_unslash( $data['order'][0]['column'] ) ) : '';
2245 if ( isset( $columns[ $ord_col ] ) ) {
2246 $values = array(
2247 'orderby' => isset( $columns[ $ord_col ]['data'] ) ? sanitize_text_field( wp_unslash( $columns[ $ord_col ]['data'] ) ) : '',
2248 'order' => isset( $data['order'][0]['dir'] ) ? sanitize_text_field( wp_unslash( $data['order'][0]['dir'] ) ) : '',
2249 );
2250 }
2251 }
2252
2253 return $values;
2254 }
2255
2256 /**
2257 * Method valid_file_check().
2258 *
2259 * @param string $path file path.
2260 * @param bool $readable readable.
2261 *
2262 * @return bool is valid.
2263 */
2264 public static function valid_file_check( $path, $readable = true ) {
2265 $valid = is_string( $path ) && ! stristr( $path, '..' );
2266 if ( $valid && $readable ) {
2267 $valid = is_readable( $path );
2268 }
2269 return $valid;
2270 }
2271
2272
2273 /**
2274 * Handle sanitize POST data.
2275 *
2276 * @param array $data input data.
2277 *
2278 * @return array
2279 */
2280 public function sanitize_data( $data ) {
2281 if ( ! is_array( $data ) ) {
2282 return array();
2283 }
2284
2285 // Sanitize all record values.
2286 return array_map(
2287 function ( $value ) {
2288 if ( ! is_array( $value ) ) {
2289 return wp_strip_all_tags( $value );
2290 }
2291
2292 return $value;
2293 },
2294 $data
2295 );
2296 }
2297
2298 /**
2299 * String ends by.
2300 *
2301 * @param mixed $str str.
2302 * @param mixed $ends ends.
2303 * @return bool value value.
2304 */
2305 public static function string_ends_by( $str, $ends ) {
2306 if ( function_exists( '\str_ends_with' ) ) {
2307 return \str_ends_with( $str, $ends );
2308 } else {
2309 $ends_len = strlen( $ends );
2310 if ( $ends_len > strlen( $str ) ) {
2311 return false;
2312 }
2313 return substr( $str, -$ends_len ) === $ends;
2314 }
2315 }
2316 /**
2317 * Returns number in shorter format.
2318 *
2319 * @param int $number str.
2320 * @return string $number Shorer number.
2321 */
2322 public static function short_number_format( $number ) {
2323 if ( $number > 999 && $number < 1000000 ) {
2324 // Anything between 1000 and 1000000.
2325 $number = number_format( $number / 1000, 1 ) . 'K';
2326 } elseif ( $number >= 1000000000 ) {
2327 // 1000000 or higher.
2328 $number = number_format( $number / 1000000, 2 ) . 'M';
2329 }
2330 return $number;
2331 }
2332
2333 /**
2334 * Returns date in time ago format
2335 *
2336 * @param mixed $ptime Date stamp.
2337 * @return string $string Time elapsed string.
2338 */
2339 public static function time_elapsed_string( $ptime ) {
2340 $etime = time() - $ptime;
2341
2342 if ( $etime < 1 ) {
2343 return '0 seconds';
2344 }
2345
2346 $a = array(
2347 365 * 24 * 60 * 60 => 'year',
2348 30 * 24 * 60 * 60 => 'month',
2349 24 * 60 * 60 => 'day',
2350 60 * 60 => 'hour',
2351 60 => 'minute',
2352 1 => 'second',
2353 );
2354 $a_plural = array(
2355 'year' => 'years',
2356 'month' => 'months',
2357 'day' => 'days',
2358 'hour' => 'hours',
2359 'minute' => 'minutes',
2360 'second' => 'seconds',
2361 );
2362
2363 foreach ( $a as $secs => $str ) {
2364 $d = $etime / $secs;
2365 if ( $d >= 1 ) {
2366 $r = round( $d );
2367 return $r . ' ' . ( $r > 1 ? $a_plural[ $str ] : $str ) . ' ago';
2368 }
2369 }
2370 }
2371
2372 /**
2373 * Returns language as flag.
2374 *
2375 * @param string $language Language code.
2376 * @return void
2377 */
2378 public static function get_language_code_as_flag( $language ) {
2379 // For flag extraction, remove trailing _formal or _informal if present.
2380 $flag_language = preg_replace( '/_(formal|informal)$/', '', $language );
2381 // Get the last 2 characters of the flag language code.
2382 $last_two_chars = ! empty( $flag_language ) ? substr( $flag_language, -2 ) : '';
2383 // Convert to lowercase.
2384 $lowercase_last_two_chars = strtolower( $last_two_chars );
2385 $lowercase_flag_language = strtolower( $flag_language );
2386
2387 // Get display name using the original language string.
2388 $display_language = function_exists( 'locale_get_display_name' ) ? locale_get_display_name( $language ) : $language;
2389
2390 // Adjust special country codes.
2391 if ( 'et' === $lowercase_last_two_chars ) {
2392 $lowercase_last_two_chars = 'ee';
2393 }
2394 if ( 'sq' === $lowercase_last_two_chars ) {
2395 $lowercase_last_two_chars = 'al';
2396 }
2397 if ( 'ab' === $lowercase_last_two_chars ) {
2398 $lowercase_last_two_chars = 'dz';
2399 }
2400
2401 $stacked_flags = array(
2402 'ca' => array(
2403 'primary' => 'es',
2404 'secondary' => 'ad',
2405 ),
2406 );
2407 // Only stack flags for explicit Catalan locales to avoid affecting Canada.
2408 $stacked_flag_locales = array(
2409 'ca' => array(
2410 'ca',
2411 ),
2412 );
2413
2414 if ( isset( $stacked_flags[ $lowercase_last_two_chars ] ) ) {
2415 $should_stack = true;
2416 if ( isset( $stacked_flag_locales[ $lowercase_last_two_chars ] ) ) {
2417 $should_stack = in_array( $lowercase_flag_language, $stacked_flag_locales[ $lowercase_last_two_chars ], true );
2418 }
2419
2420 if ( $should_stack ) {
2421 $primary_flag = $stacked_flags[ $lowercase_last_two_chars ]['primary'];
2422 $secondary_flag = $stacked_flags[ $lowercase_last_two_chars ]['secondary'];
2423
2424 echo '<span data-tooltip="' . esc_html__( 'Site Language: ', 'mainwp' ) . esc_attr( $display_language ) . '" data-position="left center" data-inverted="">';
2425 echo '<span class="mainwp-flag-stack">';
2426 echo '<i class="small ' . esc_attr( $primary_flag ) . ' flag mainwp-flag-stack__flag mainwp-flag-stack__flag--primary"></i>';
2427 echo '<i class="small ' . esc_attr( $secondary_flag ) . ' flag mainwp-flag-stack__flag mainwp-flag-stack__flag--secondary"></i>';
2428 echo '</span>';
2429 echo '</span>';
2430 return;
2431 }
2432 }
2433
2434 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>';
2435 }
2436
2437 /**
2438 * Returns icon for the site indexability status.
2439 *
2440 * @param int $status Status, 1 or 0.
2441 * @return void.
2442 */
2443 public static function get_site_index_option_icon( $status ) {
2444 $icon = '';
2445 $tooltip = '';
2446 if ( isset( $status ) && '' !== $status ) {
2447 if ( 1 === intval( $status ) ) {
2448 $icon = 'green dot circle outline';
2449 $tooltip = 'Search engines can index this site.';
2450 } elseif ( 0 === intval( $status ) ) {
2451 $icon = 'red ban';
2452 $tooltip = 'This site is blocking search engines.';
2453 }
2454 } else {
2455 $icon = 'grey circle';
2456 $tooltip = 'Indexing status unknown. Resync the site or check manually in WordPress Settings > Reading.';
2457 }
2458 echo '<span data-tooltip="' . $tooltip . '" data-position="left center" data-inverted=""><i class="' . $icon . ' icon"></i></span>'; //phpcs:ignore -- ok.
2459 }
2460
2461 /**
2462 * Returns the appropriate Fomantic UI color class based on number of updates
2463 *
2464 * @param int $update_count Number of available updates.
2465 *
2466 * @return string CSS class for the element
2467 */
2468 public static function mainwp_get_update_count_class( $update_count ) {
2469 // Convert to integer using intval().
2470 $update_count = intval( $update_count );
2471
2472 // Ensure count is not negative.
2473 if ( 0 > $update_count ) {
2474 $update_count = 0;
2475 }
2476
2477 if ( 0 === $update_count ) {
2478 return 'grey';
2479 } elseif ( $update_count >= 1 && $update_count <= 3 ) {
2480 return 'yellow';
2481 } elseif ( $update_count >= 4 && $update_count <= 5 ) {
2482 return 'orange';
2483 } else {
2484 return 'red';
2485 }
2486 }
2487
2488 /**
2489 * Display site name and URL with optional WP Admin link.
2490 *
2491 * @param object|int $site Site object or Site ID.
2492 * @param bool $wp_admin Whether to show WP Admin link.
2493 * @param bool $print_content Whether to print or return the content.
2494 * @return string HTML markup for site display.
2495 */
2496 public static function mainwp_display_site( $site = '', $wp_admin = true, $print_content = false ) {
2497 if ( empty( $site ) ) {
2498 return '';
2499 }
2500
2501 if ( static::ctype_digit( $site ) ) {
2502 $website = MainWP_DB::instance()->get_website_by_id( $site );
2503 } elseif ( is_object( $site ) && isset( $site->id ) ) {
2504 $website = $site;
2505 } else {
2506 return '';
2507 }
2508
2509 if ( ! $website ) {
2510 return '';
2511 }
2512
2513 $site_name = esc_html( stripslashes( $website->name ) );
2514 $site_url = esc_url( $website->url );
2515 $nice_url = esc_html( static::get_nice_url( $website->url ) );
2516
2517 $html = '<div class="mainwp-site-display">';
2518
2519 // WP Admin link (if enabled and user has permission).
2520 if ( $wp_admin && \mainwp_current_user_can( 'dashboard', 'access_wpadmin_on_child_sites' ) ) {
2521 $admin_url = MainWP_Site_Open::get_open_site_url( $website->id, '', false );
2522 $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> ';
2523 } elseif ( $wp_admin ) {
2524 $html .= '<i class="sign in icon"></i> ';
2525 }
2526
2527 // Site name with dashboard link.
2528 $html .= '<a href="' . esc_url( admin_url( 'admin.php?page=managesites&dashboard=' . intval( $website->id ) ) ) . '">' . $site_name . '</a>';
2529
2530 // Site URL.
2531 $html .= '<div><span class="ui small text">';
2532 $html .= '<a href="' . $site_url . '" class="mainwp-may-hide-referrer open_site_url ui grey text" target="_blank">' . $nice_url . '</a>';
2533 $html .= '</span></div>';
2534
2535 $html .= '</div>';
2536
2537 if ( $print_content ) {
2538 echo $html; // phpcs:ignore -- ok.
2539 return '';
2540 }
2541
2542 return $html;
2543 }
2544
2545 /**
2546 * Generate a sortable BIGINT for WordPress versions.
2547 * Newer versions produce larger numbers.
2548 * Ordering matches WP core version_compare().
2549 *
2550 * @param string $version Version value.
2551 */
2552 public function wp_versions_order_num( $version ) {
2553
2554 $v = strtolower( trim( $version ) );
2555
2556 // 1) Normalize WordPress aliases.
2557 $v = str_replace(
2558 array( '-alpha', '-beta', '-rc' ),
2559 array( '-a', '-b', '-rc' ),
2560 $v
2561 );
2562
2563 // nightly / dev / trunk → dev.
2564 if ( preg_match( '/-(nightly|dev|trunk)/', $v ) ) {
2565 $v = preg_replace( '/-.+$/', '-dev', $v );
2566 }
2567
2568 // unknown tags → dev.
2569 if ( preg_match( '/-[a-z]+/', $v ) && ! preg_match( '/-(a|b|rc|dev)/', $v ) ) {
2570 $v = preg_replace( '/-.+$/', '-dev', $v );
2571 }
2572
2573 // 2) Extract numeric base version.
2574 $base = explode( '-', $v, 2 )[0];
2575 $parts = array_map( 'intval', explode( '.', $base ) );
2576
2577 if ( count( $parts ) < 4 ) {
2578 $parts = array_pad( $parts, 4, 0 );
2579 }
2580
2581 $versionNum =
2582 ( $parts[0] << 24 ) |
2583 ( $parts[1] << 16 ) |
2584 ( $parts[2] << 8 ) |
2585 $parts[3];
2586
2587 // 3) Release channel rank (lower = newer).
2588 $rank = 1; // stable.
2589 if ( strpos( $v, '-rc' ) !== false ) {
2590 $rank = 2;
2591 } elseif ( strpos( $v, '-b' ) !== false ) {
2592 $rank = 3;
2593 } elseif ( strpos( $v, '-a' ) !== false ) {
2594 $rank = 4;
2595 } elseif ( strpos( $v, '-dev' ) !== false ) {
2596 $rank = 5;
2597 }
2598
2599 // 4) Prerelease / build number.
2600 $build = 0;
2601 if ( preg_match( '/rc(\d+)/', $v, $m ) ) {
2602 $build = (int) $m[1];
2603 } elseif ( preg_match( '/b(\d+)/', $v, $m ) ) { // NOSONAR - same above ok.
2604 $build = (int) $m[1];
2605 } elseif ( preg_match( '/a(\d+)/', $v, $m ) ) { // NOSONAR - same above ok.
2606 $build = (int) $m[1];
2607 }
2608
2609 // 5) Compose sortable BIGINT.
2610 return ( $versionNum << 32 )
2611 | ( $rank << 29 )
2612 | min( $build, ( 1 << 29 ) - 1 );
2613 }
2614
2615 /**
2616 * Method handle gen_rand_id().
2617 *
2618 * @param string $str Input string.
2619 * @param bool $more_entropy More entropy False if a robust prefix is required, default false.
2620 * @return string Random ID 8 characters.
2621 */
2622 public static function gen_rand_id( $str = '', $more_entropy = false ) {
2623 return hash( 'crc32b', uniqid( $str, $more_entropy ? true : false ) );
2624 }
2625
2626 /**
2627 * Decode and validate JSON array.
2628 *
2629 * @param mixed $data Data to decode.
2630 */
2631 public static function get_decoded_array( $data ) {
2632 $decoded = ! empty( $data ) ? json_decode( $data, true ) : array();
2633 return is_array( $decoded ) ? $decoded : array();
2634 }
2635
2636 /**
2637 * Method get_use_cron()
2638 *
2639 * @return bool Whether use cron.
2640 */
2641 public static function get_use_cron() {
2642 return ( get_option( 'mainwp_wp_cron' ) === false ) || ( (int) get_option( 'mainwp_wp_cron' ) === 1 ) ? 1 : 0;
2643 }
2644 }
2645