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
← All changes | class/class-mainwp-utility.php +2516 -1193 4.6.0.1 → 6.2.2 View file →
@@ -6,8 +6,13 @@
6 6 */
7 7
8 8 namespace MainWP\Dashboard;
9 9
10 +// Exit if accessed directly.
11 +if ( ! defined( 'ABSPATH' ) ) {
12 + exit;
13 +}
14 +
10 15 // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors, Generic.Metrics.CyclomaticComplexity -- Using cURL functions.
11 16
12 17 /**
13 18 * Class MainWP_Utility
@@ -13,1309 +18,2627 @@
13 18 * Class MainWP_Utility
14 19 *
15 20 * @package MainWP\Dashboard
16 21 */
17 -class MainWP_Utility {
22 +class MainWP_Utility { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
18 23
19 - /**
20 - * Yoast SEO is enabled return true else return null.
21 - *
22 - * @static
23 - * @var boolean $enabled_wp_seo If Yoast SEO is enabled return true else return null.
24 - */
25 - public static $enabled_wp_seo = null;
24 + /**
25 + * Prefix for short-term notice options.
26 + */
27 + const SHORT_TERM_NOTICE_OPTION_PREFIX = '_mainwp_temp_notice_';
26 28
27 - /**
28 - * Private static variable to hold the single instance of the class.
29 - *
30 - * @static
31 - *
32 - * @var mixed Default null
33 - */
34 - private static $instance = null;
35 29
36 - /**
37 - * Store the disabled php functions.
38 - *
39 - * @static
40 - * @var string $disabled_functions disabled php functions.
41 - */
42 - public static $disabled_functions = null;
30 + /**
31 + * Prefix for per-user short-term notice options.
32 + */
33 + const USER_SHORT_TERM_NOTICE_OPTION_PREFIX = '_mainwp_user_temp_notice_';
43 34
44 - /**
45 - * Method get_class_name()
46 - *
47 - * Get Class Name.
48 - *
49 - * @return object __CLASS__
50 - */
51 - public static function get_class_name() {
52 - return __CLASS__;
53 - }
54 35
55 - /**
56 - * Method instance()
57 - *
58 - * Create public static instance.
59 - *
60 - * @static
61 - * @return MainWP_Utility
62 - */
63 - public static function instance() {
64 - if ( null === self::$instance ) {
65 - self::$instance = new self();
66 - }
36 + /**
37 + * New short term notice TTL.
38 + */
39 + const SHORT_TERM_NOTICE_TTL = MONTH_IN_SECONDS;
67 40
68 - return self::$instance;
69 - }
70 41
71 - /**
72 - * Method starts_with()
73 - *
74 - * Start of Stack Trace.
75 - *
76 - * @param mixed $haystack The full stack.
77 - * @param mixed $needle The function that is throwing the error.
78 - *
79 - * @return mixed Needle in the Haystack.
80 - */
81 - public static function starts_with( $haystack, $needle ) {
82 - return ! strncmp( $haystack, $needle, strlen( $needle ) );
83 - }
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;
84 49
85 - /**
86 - * Method ends_with()
87 - *
88 - * End of Stack Trace.
89 - *
90 - * @param mixed $haystack Haystack parameter.
91 - * @param mixed $needle Needle parameter.
92 - *
93 - * @return boolean
94 - */
95 - public static function ends_with( $haystack, $needle ) {
96 - $length = strlen( $needle );
97 - if ( 0 === $length ) {
98 - return true;
99 - }
50 + /**
51 + * Private static variable.
52 + *
53 + * @static
54 + *
55 + * @var mixed Default null
56 + */
57 + public static $last_deactivated_alerts = null;
100 58
101 - return ( substr( $haystack, - $length ) === $needle );
102 - }
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;
103 67
104 - /**
105 - * Method get_nice_url()
106 - *
107 - * Grab url.
108 - *
109 - * @param string $pUrl Website URL.
110 - * @param bool $showHttp Show HTTP.
111 - *
112 - * @return string $url.
113 - */
114 - public static function get_nice_url( $pUrl, $showHttp = false ) {
115 - $url = $pUrl;
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;
116 75
117 - if ( self::starts_with( $url, 'http://' ) ) {
118 - if ( ! $showHttp ) {
119 - $url = substr( $url, 7 );
120 - }
121 - } elseif ( self::starts_with( $pUrl, 'https://' ) ) {
122 - if ( ! $showHttp ) {
123 - $url = substr( $url, 8 );
124 - }
125 - } elseif ( $showHttp ) {
126 - $url = 'http://' . $url;
127 - }
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 + }
128 86
129 - if ( self::ends_with( $url, '/' ) ) {
130 - if ( ! $showHttp ) {
131 - $url = substr( $url, 0, strlen( $url ) - 1 );
132 - }
133 - } else {
134 - $url = $url . '/';
135 - }
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 + }
136 99
137 - return $url;
138 - }
100 + return static::$instance;
101 + }
139 102
140 - /**
141 - * Method is_domain_valid()
142 - *
143 - * Check $url against FILTER_VALIDATE_URL.
144 - *
145 - * @param mixed $url Domain to check.
146 - *
147 - * @return boolean True|False.
148 - */
149 - public static function is_domain_valid( $url ) {
150 - return filter_var( $url, FILTER_VALIDATE_URL );
151 - }
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 + }
152 116
153 - /**
154 - * Method ctype_digit()
155 - *
156 - * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
157 - *
158 - * @param mixed $str String to check.
159 - *
160 - * @return boolean Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise.
161 - */
162 - public static function ctype_digit( $str ) {
163 - return ( is_string( $str ) || is_int( $str ) || is_float( $str ) ) && preg_match( '/^\d+\z/', $str );
164 - }
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 + }
165 132
166 - /**
167 - * Method sortmulti()
168 - *
169 - * Sort the given array, Acending, Decending or by Natural Order.
170 - *
171 - * @param mixed $arr Array to sort.
172 - * @param mixed $index Index of array.
173 - * @param mixed $order Acending or Decending order.
174 - * @param bool $natsort Sort an array using a "natural order" algorithm. Default: false.
175 - * @param bool $case_sensitive If case sensitive return true else return false. Default: false.
176 - *
177 - * @return array $sorted Return the sorted array.
178 - */
179 - public static function sortmulti( $arr, $index, $order, $natsort = false, $case_sensitive = false ) {
180 - $sorted = array();
181 - if ( is_array( $arr ) && 0 < count( $arr ) ) {
182 - foreach ( array_keys( $arr ) as $key ) {
183 - $temp[ $key ] = $arr[ $key ][ $index ];
184 - }
185 - if ( ! $natsort ) {
186 - if ( 'asc' === $order ) {
187 - asort( $temp );
188 - } else {
189 - arsort( $temp );
190 - }
191 - } else {
192 - if ( true === $case_sensitive ) {
193 - natsort( $temp );
194 - } else {
195 - natcasesort( $temp );
196 - }
197 - if ( 'asc' !== $order ) {
198 - $temp = array_reverse( $temp, true );
199 - }
200 - }
201 - foreach ( array_keys( $temp ) as $key ) {
202 - if ( is_numeric( $key ) ) {
203 - $sorted[] = $arr[ $key ];
204 - } else {
205 - $sorted[ $key ] = $arr[ $key ];
206 - }
207 - }
133 + return substr( $haystack, - $length ) === $needle;
134 + }
208 135
209 - return $sorted;
210 - }
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;
211 147
212 - return $sorted;
213 - }
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 + }
214 159
215 - /**
216 - * Method get_sub_array_having()
217 - *
218 - * Get sub array.
219 - *
220 - * @param mixed $arr Array to traverse.
221 - * @param mixed $index Index of array.
222 - * @param mixed $value Array values.
223 - *
224 - * void array $output Sub array.
225 - */
226 - public static function get_sub_array_having( $arr, $index, $value ) {
227 - $output = array();
228 - if ( is_array( $arr ) && 0 < count( $arr ) ) {
229 - foreach ( $arr as $arrvalue ) {
230 - $existed = isset( $arrvalue[ $index ] ) ? $arrvalue[ $index ] : null;
231 - if ( $existed === $value ) {
232 - $output[] = $arrvalue;
233 - }
234 - }
235 - }
160 + if ( static::ends_with( $url, '/' ) ) {
161 + if ( ! $showHttp ) {
162 + $url = substr( $url, 0, strlen( $url ) - 1 );
163 + }
164 + } else {
165 + $url .= '/';
166 + }
236 167
237 - return $output;
238 - }
168 + if ( $removeTrailingSlash ) {
169 + $url = rtrim( $url, '/' );
170 + }
239 171
240 - /**
241 - * Method trim_slashes()
242 - *
243 - * Trim stashes from element.
244 - *
245 - * @param mixed $elem Element to trim.
246 - *
247 - * @return string Return string with no slashes.
248 - */
249 - public static function trim_slashes( $elem ) {
250 - return trim( $elem, '/' );
251 - }
172 + return $url;
173 + }
252 174
253 - /**
254 - * Method sanitize()
255 - *
256 - * Sanitize given string.
257 - *
258 - * @param mixed $str String to sanitize.
259 - *
260 - * @return string Sanitized string.
261 - */
262 - public static function sanitize( $str ) {
263 - return preg_replace( '/[\\\\\/\:"\*\?\<\>\|]+/', '', $str );
264 - }
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 + }
265 187
266 - /**
267 - * Method sanitize_alphanumeric()
268 - *
269 - * Sanitize given string.
270 - *
271 - * @param mixed $str String to sanitize.
272 - *
273 - * @return string Sanitized string.
274 - */
275 - public static function sanitize_attr_slug( $str ) {
276 - $str = strtolower( $str );
277 - $str = str_replace( array( '=', '?', '/' ), '-', $str );
278 - $str = preg_replace( '/[^A-Za-z0-9^\-]/', '', $str );
279 - return $str;
280 - }
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 + }
281 200
282 - /**
283 - * Method end_session()
284 - *
285 - * End a session.
286 - *
287 - * @return void
288 - */
289 - public static function end_session() {
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 + }
290 243
291 - if ( defined( 'WP_CLI' ) && WP_CLI ) {
292 - return;
293 - }
244 + return $sorted;
245 + }
294 246
295 - if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
296 - return;
297 - }
247 + return $sorted;
248 + }
298 249
299 - session_write_close();
300 - if ( 0 < ob_get_length() ) {
301 - ob_end_flush();
302 - }
303 - }
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 + }
304 271
305 - /**
306 - * Method get_timestamp()
307 - *
308 - * Get time stamp in gmt_offset.
309 - *
310 - * @param mixed $timestamp Time stamp to convert.
311 - *
312 - * @return string Time stamp in general mountain time offset.
313 - */
314 - public static function get_timestamp( $timestamp = false ) {
315 - if ( false === $timestamp ) {
316 - $timestamp = time();
317 - }
318 - $gmtOffset = get_option( 'gmt_offset' );
272 + return $output;
273 + }
319 274
320 - return ( $gmtOffset ? ( $gmtOffset * HOUR_IN_SECONDS ) + $timestamp : $timestamp );
321 - }
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 + }
322 305
323 - /**
324 - * Method date()
325 - *
326 - * Show date in given format.
327 - *
328 - * @param mixed $format Format to display date in.
329 - *
330 - * @return string Date.
331 - */
332 - public static function date( $format ) {
333 - // phpcs:ignore -- use local date function.
334 - return date( $format, self::get_timestamp() );
335 - }
336 306
337 - /**
338 - * Method format_timestamp()
339 - *
340 - * Format the given timestamp.
341 - *
342 - * @param mixed $timestamp Timestamp to format.
343 - *
344 - * @return string Formatted timestamp.
345 - */
346 - public static function format_timestamp( $timestamp ) {
347 - return date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp );
348 - }
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 + }
349 319
350 - /**
351 - * Method format_timestamp()
352 - *
353 - * Format the given timestamp.
354 - *
355 - * @param mixed $timestamp Timestamp to format.
356 - *
357 - * @return string Formatted timestamp.
358 - */
359 - public static function format_date( $timestamp ) {
360 - return date_i18n( get_option( 'date_format' ), $timestamp );
361 - }
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 + }
362 332
363 - /**
364 - * Method human_filesize()
365 - *
366 - * Convert to human readable file size format,
367 - * (B|kB|MB|GB|TB|PB|EB|ZB|YB).
368 - *
369 - * @param mixed $bytes File in bytes.
370 - * @param integer $decimals Number of decimals to output.
371 - *
372 - * @return string Human readable file size.
373 - */
374 - public static function human_filesize( $bytes, $decimals = 2 ) {
375 - $size = array( 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' );
376 - $factor = floor( ( strlen( $bytes ) - 1 ) / 3 );
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 + }
377 348
378 - return sprintf( "%.{$decimals}f", $bytes / pow( 1024, $factor ) ) . @$size[ $factor ];
379 - }
349 + /**
350 + * Method end_session()
351 + *
352 + * End a session.
353 + *
354 + * @return void
355 + */
356 + public static function end_session() {
380 357
381 - /**
382 - * Method map_fields()
383 - *
384 - * Map Site.
385 - *
386 - * @param mixed $data data to map.
387 - * @param mixed $keys Keys to map.
388 - * @param bool $object_output Output format array|object.
389 - *
390 - * @return mixed Mapped data.
391 - */
392 - public static function map_fields( &$data, $keys, $object_output = true ) {
393 - return self::map_site( $data, $keys, $object_output );
394 - }
358 + if ( defined( 'WP_CLI' ) && WP_CLI ) {
359 + return;
360 + }
395 361
396 - /**
397 - * Method map_site()
398 - *
399 - * Map Site.
400 - *
401 - * @param mixed $website Website to map.
402 - * @param mixed $keys Keys to map.
403 - * @param bool $object_output Output format array|object.
404 - *
405 - * @return mixed $outputSite Mapped site.
406 - */
407 - public static function map_site( &$website, $keys, $object_output = true ) {
408 - $outputSite = array();
409 - if ( ! empty( $website ) ) {
410 - if ( is_object( $website ) ) {
411 - foreach ( $keys as $key ) {
412 - $outputSite[ $key ] = $website->$key;
413 - }
414 - } elseif ( is_array( $website ) ) {
415 - foreach ( $keys as $key ) {
416 - $outputSite[ $key ] = $website[ $key ];
417 - }
418 - }
419 - }
362 + if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
363 + return;
364 + }
420 365
421 - if ( $object_output ) {
422 - return (object) $outputSite;
423 - } else {
424 - return $outputSite;
425 - }
426 - }
366 + session_write_close();
367 + if ( 0 < ob_get_length() ) {
368 + ob_end_flush();
369 + }
370 + }
427 371
428 - /**
429 - * Method array_merge()
430 - *
431 - * Merge two given arrays into one.
432 - *
433 - * @param mixed $arr1 First array.
434 - * @param mixed $arr2 Second array.
435 - *
436 - * @return array Merged Array.
437 - */
438 - public static function array_merge( $arr1, $arr2 ) {
439 - if ( ! is_array( $arr1 ) && ! is_array( $arr2 ) ) {
440 - return array();
441 - }
442 - if ( ! is_array( $arr1 ) ) {
443 - return $arr2;
444 - }
445 - if ( ! is_array( $arr2 ) ) {
446 - return $arr1;
447 - }
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' );
448 386
449 - $output = array();
450 - foreach ( $arr1 as $el ) {
451 - $output[] = $el;
452 - }
453 - foreach ( $arr2 as $el ) {
454 - $output[] = $el;
455 - }
387 + return $gmtOffset ? ( $gmtOffset * HOUR_IN_SECONDS ) + $timestamp : $timestamp;
388 + }
456 389
457 - return $output;
458 - }
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 + }
459 403
460 - /**
461 - * Method update_option()
462 - *
463 - * Update option.
464 - *
465 - * @param mixed $option_name Option name.
466 - * @param mixed $option_value Option value.
467 - *
468 - * @return (boolean) False if value was not updated and true if value was updated.
469 - */
470 - public static function update_option( $option_name, $option_value ) {
471 - $success = add_option( $option_name, $option_value, '', 'no' );
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 + }
472 416
473 - if ( ! $success ) {
474 - $success = update_option( $option_name, $option_value );
475 - }
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'] : '';
476 433
477 - return $success;
478 - }
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 );
479 442
480 - /**
481 - * Method update_user_option()
482 - *
483 - * Update option.
484 - *
485 - * @param mixed $option_name Option name.
486 - * @param mixed $option_value Option value.
487 - *
488 - * @return (boolean) False if value was not updated and true if value was updated.
489 - */
490 - public static function update_user_option( $option_name, $option_value ) {
491 - $user = wp_get_current_user();
492 - if ( $user ) {
493 - return update_user_option( $user->ID, $option_name, $option_value );
494 - }
495 - return false;
496 - }
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 + }
497 447
498 - /**
499 - * Method remove_preslash_spaces()
500 - *
501 - * Remove spaces before slashes.
502 - *
503 - * @param string $text String to strip.
504 - *
505 - * @return string $text Cleaned string.
506 - */
507 - public static function remove_preslash_spaces( $text ) {
508 - while ( stristr( $text, ' /' ) ) {
509 - $text = str_replace( ' /', '/', $text );
510 - }
448 + $datetime = new \DateTime( '@' . $timestamp );
449 + $datetime->setTimezone( new \DateTimeZone( $wp_timezone ) );
511 450
512 - return $text;
513 - }
451 + return $datetime->format( $format );
452 + }
453 + return '';
454 + }
514 455
515 - /**
516 - * Method remove_http_prefix()
517 - *
518 - * Remove http prefixes from given url.
519 - *
520 - * @param mixed $pUrl Given URL.
521 - * @param bool $pTrimSlashes Whether or not to trim slashes. Default is false.
522 - *
523 - * @return string Trimmed URL.
524 - */
525 - public static function remove_http_prefix( $pUrl, $pTrimSlashes = false ) {
526 - return str_replace( array( 'http:' . ( $pTrimSlashes ? '//' : '' ), 'https:' . ( $pTrimSlashes ? '//' : '' ) ), array( '', '' ), $pUrl );
527 - }
456 + $wp_timezone = static::clean_wp_timezone_string( get_option( 'timezone_string' ) );
528 457
529 - /**
530 - * Method remove_http_www_prefix()
531 - *
532 - * Remove 'www.' from given URL.
533 - *
534 - * @param mixed $pUrl Given URL.
535 - *
536 - * @return string Cleaned URL.
537 - */
538 - public static function remove_http_www_prefix( $pUrl ) {
539 - $pUrl = self::remove_http_prefix( $pUrl, true );
540 - return str_replace( 'www.', '', $pUrl );
541 - }
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 + }
542 464
543 - /**
544 - * Method sanitize_file_name()
545 - *
546 - * Sanitize file names.
547 - *
548 - * @param mixed $filename File name to sanitize.
549 - *
550 - * @return string Sanitized filename.
551 - */
552 - public static function sanitize_file_name( $filename ) {
553 - $filename = str_replace( array( '|', '/', '\\', ' ', ':' ), array( '-', '-', '-', '-', '-' ), $filename );
554 - return sanitize_file_name( $filename );
555 - }
465 + if ( $with_tz_info ) {
466 + $tzinfo = ' ( ' . $wp_timezone . ' )';
467 + }
556 468
557 - /**
558 - * Method normalize_filename()
559 - *
560 - * Normalize filename.
561 - *
562 - * @param mixed $s Filename to normalize.
563 - *
564 - * @return string $s Normalised filename.
565 - */
566 - public static function normalize_filename( $s ) {
567 - $s = preg_replace( '@\x{00c4}@u', 'A', $s );
568 - $s = preg_replace( '@\x{00d6}@u', 'O', $s );
569 - $s = preg_replace( '@\x{00dc}@u', 'U', $s );
570 - $s = preg_replace( '@\x{00cb}@u', 'E', $s );
571 - $s = preg_replace( '@\x{00e4}@u', 'a', $s );
572 - $s = preg_replace( '@\x{00f6}@u', 'o', $s );
573 - $s = preg_replace( '@\x{00fc}@u', 'u', $s );
574 - $s = preg_replace( '@\x{00eb}@u', 'e', $s );
575 - $s = preg_replace( '@\x{00f1}@u', 'n', $s );
576 - $s = preg_replace( '@\x{00ff}@u', 'y', $s );
577 - return $s;
578 - }
469 + $datetime = new \DateTime( '@' . $timestamp );
470 + $datetime->setTimezone( new \DateTimeZone( $wp_timezone ) );
579 471
472 + $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) . $tzinfo;
473 + return $datetime->format( $format );
474 + }
580 475
581 - /**
582 - * Method esc_content()
583 - *
584 - * Escape content,
585 - * allowed content (a,href,title,br,em,strong,p,hr,ul,ol,li,h1,h2 ... ).
586 - *
587 - * @param mixed $content Content to escape.
588 - * @param string $type Type of content. Default = note.
589 - * @param mixed $more_allowed input allowed tags - options.
590 - *
591 - * @return string Filtered content containing only the allowed HTML.
592 - */
593 - public static function esc_content( $content, $type = 'note', $more_allowed = array() ) {
594 - if ( ! is_string( $content ) ) {
595 - return $content;
596 - }
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 + }
597 495
598 - if ( 'note' === $type ) {
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 + }
599 508
600 - $allowed_html = array(
601 - 'a' => array(
602 - 'href' => array(),
603 - 'title' => array(),
604 - ),
605 - 'br' => array(),
606 - 'em' => array(),
607 - 'strong' => array(),
608 - 'p' => array(),
609 - 'hr' => array(),
610 - 'ul' => array(),
611 - 'ol' => array(),
612 - 'li' => array(),
613 - 'h1' => array(),
614 - 'h2' => array(),
615 - );
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 + }
616 521
617 - if ( is_array( $more_allowed ) && ! empty( $more_allowed ) ) {
618 - $allowed_html = array_merge( $allowed_html, $more_allowed );
619 - }
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;
620 542
621 - $content = wp_kses( $content, $allowed_html );
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'];
622 547
623 - } elseif ( 'mixed' === $type ) {
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 + }
624 560
625 - $allowed_html = array(
626 - 'a' => array(
627 - 'href' => array(),
628 - 'title' => array(),
629 - 'class' => array(),
630 - 'onclick' => array(),
631 - ),
632 - 'img' => array(
633 - 'src' => array(),
634 - 'title' => array(),
635 - 'class' => array(),
636 - 'onclick' => array(),
637 - 'alt' => array(),
638 - 'width' => array(),
639 - 'height' => array(),
640 - 'sizes' => array(),
641 - 'srcset' => array(),
642 - 'usemap' => array(),
643 - ),
644 - 'br' => array(),
645 - 'em' => array(),
646 - 'strong' => array(),
647 - 'p' => array(),
648 - 'hr' => array(),
649 - 'ul' => array(
650 - 'style' => array(),
651 - ),
652 - 'ol' => array(),
653 - 'li' => array(),
654 - 'h1' => array(),
655 - 'h2' => array(),
656 - 'head' => array(),
657 - 'html' => array(
658 - 'lang' => array(),
659 - ),
660 - 'meta' => array(
661 - 'name' => array(),
662 - 'http-equiv' => array(),
663 - 'content' => array(),
664 - 'charset' => array(),
665 - ),
666 - 'title' => array(),
667 - 'body' => array(
668 - 'style' => array(),
669 - ),
670 - 'span' => array(
671 - 'id' => array(),
672 - 'style' => array(),
673 - 'class' => array(),
674 - ),
675 - 'form' => array(
676 - 'id' => array(),
677 - 'method' => array(),
678 - 'action' => array(),
679 - 'onsubmit' => array(),
680 - ),
681 - 'table' => array(
682 - 'class' => array(),
683 - ),
684 - 'thead' => array(
685 - 'class' => array(),
686 - ),
687 - 'tbody' => array(
688 - 'class' => array(),
689 - ),
690 - 'tr' => array(
691 - 'id' => array(),
692 - ),
693 - 'td' => array(
694 - 'class' => array(),
695 - ),
696 - 'div' => array(
697 - 'id' => array(),
698 - 'style' => array(),
699 - 'class' => array(),
700 - ),
701 - 'input' => array(
702 - 'type' => array(),
703 - 'name' => array(),
704 - 'class' => array(),
705 - 'value' => array(),
706 - 'onclick' => array(),
707 - ),
708 - 'button' => array(
709 - 'type' => array(),
710 - 'name' => array(),
711 - 'value' => array(),
712 - 'class' => array(),
713 - 'title' => array(),
714 - 'onclick' => array(),
715 - ),
716 - );
561 + $formatted = '';
562 + $message = '';
717 563
718 - if ( is_array( $more_allowed ) && ! empty( $more_allowed ) ) {
719 - $allowed_html = array_merge( $allowed_html, $more_allowed );
720 - }
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 + }
721 571
722 - $content = wp_kses( $content, $allowed_html );
723 - } else {
724 - $content = wp_kses_post( $content );
725 - }
572 + return array(
573 + 'timestamp' => $timestamp,
574 + 'formatted' => $formatted,
575 + 'status' => $status,
576 + 'message' => $message,
577 + );
578 + }
726 579
727 - return $content;
728 - }
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 ) {
729 587
730 - /**
731 - * Method esc_mixed_content()
732 - *
733 - * Escape mixed content,
734 - * allowed content (a,href,title,br,em,strong,p,hr,ul,ol,li,h1,h2 ... ).
735 - *
736 - * @param mixed $data data to escape.
737 - * @param string $depth Maximum depth to walk through $data. Must be greater than 0.
738 - * @param mixed $more_allowed input allowed tags - options.
739 - *
740 - * @throws \Exception Excetpion message.
741 - *
742 - * @return string Filtered content containing only the allowed HTML.
743 - */
744 - public static function esc_mixed_content( $data, $depth, $more_allowed = array() ) {
745 - if ( $depth < 0 ) {
746 - throw new \Exception( 'Reached depth limit' );
747 - }
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 );
748 594
749 - if ( is_array( $data ) ) {
750 - $output = array();
751 - foreach ( $data as $id => $el ) {
752 - // Don't forget to sanitize the ID!
753 - if ( is_string( $id ) ) {
754 - $clean_id = self::esc_content( $id, 'mixed', $more_allowed );
755 - } else {
756 - $clean_id = $id;
757 - }
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 + }
758 602
759 - // Check the element type, so that we're only recursing if we really have to.
760 - if ( is_array( $el ) || is_object( $el ) ) {
761 - $output[ $clean_id ] = self::esc_mixed_content( $el, $depth - 1 );
762 - } elseif ( is_string( $el ) ) {
763 - $output[ $clean_id ] = self::esc_content( $el, 'mixed', $more_allowed );
764 - } else {
765 - $output[ $clean_id ] = $el;
766 - }
767 - }
768 - } elseif ( is_object( $data ) ) {
769 - $output = new stdClass();
770 - foreach ( $data as $id => $el ) {
771 - if ( is_string( $id ) ) {
772 - $clean_id = self::esc_content( $id, 'mixed', $more_allowed );
773 - } else {
774 - $clean_id = $id;
775 - }
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 ) {
776 612
777 - if ( is_array( $el ) || is_object( $el ) ) {
778 - $output->$clean_id = self::esc_mixed_content( $el, $depth - 1, $more_allowed );
779 - } elseif ( is_string( $el ) ) {
780 - $output->$clean_id = self::esc_content( $el, 'mixed', $more_allowed );
781 - } else {
782 - $output->$clean_id = $el;
783 - }
784 - }
785 - } elseif ( is_string( $data ) ) {
786 - return self::esc_content( $data, 'mixed', $more_allowed );
787 - } else {
788 - return $data;
789 - }
613 + $tz = wp_timezone(); // site timezone.
614 + $day = new \DateTimeImmutable( $dt_str, $tz );
790 615
791 - return $output;
792 - }
616 + if ( is_numeric( $add_days ) && $add_days > 0 ) {
617 + $day = $day->modify( '+' . $add_days . ' day' );
618 + }
793 619
794 - /**
795 - * Method show_mainwp_message()
796 - *
797 - * Check whenther or not to show the MainWP Message.
798 - *
799 - * @param mixed $type Type of message.
800 - * @param mixed $notice_id Notice ID.
801 - *
802 - * @return boolean true|false.
803 - */
804 - public static function show_mainwp_message( $type, $notice_id ) {
805 - $status = get_user_option( 'mainwp_notice_saved_status' );
806 - if ( ! is_array( $status ) ) {
807 - $status = array();
808 - }
809 - if ( isset( $status[ $notice_id ] ) ) {
810 - return false;
811 - }
812 - return true;
813 - }
620 + return $day->setTimezone( new \DateTimeZone( 'UTC' ) )->getTimestamp();
621 + }
814 622
815 - /**
816 - * Method get_hide_notice_status()
817 - *
818 - * Check whenther or not to show the MainWP Message.
819 - *
820 - * @param mixed $notice_id Notice ID.
821 - *
822 - * @return mixed true|false|time.
823 - */
824 - public static function get_hide_notice_status( $notice_id ) {
825 - $notices = get_user_option( 'mainwp_notice_saved_status' );
826 - if ( ! is_array( $notices ) ) {
827 - $notices = array();
828 - }
829 - if ( isset( $notices[ $notice_id ] ) ) {
830 - return $notices[ $notice_id ];
831 - }
832 - return false;
833 - }
834 623
835 - /**
836 - * Method get_flash_message()
837 - *
838 - * Get saved flash Message.
839 - *
840 - * @param mixed $message_id Notice ID.
841 - * @param bool $delete True to delete the message after get it.
842 - *
843 - * @return boolean true|false.
844 - */
845 - public static function get_flash_message( $message_id, $delete = true ) {
846 - $flash_messages = get_user_option( 'mainwp_flash_messages' );
847 - if ( ! is_array( $flash_messages ) ) {
848 - $flash_messages = array();
849 - }
850 - if ( ! isset( $flash_messages[ $message_id ] ) ) {
851 - return false;
852 - }
853 - $content = $flash_messages[ $message_id ];
854 - if ( $delete ) {
855 - unset( $flash_messages[ $message_id ] );
856 - self::update_user_option( 'mainwp_flash_messages', $flash_messages );
857 - }
858 - return $content;
859 - }
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 + }
860 657
861 - /**
862 - * Method update_flash_message()
863 - *
864 - * Check whenther or not to show the MainWP Message.
865 - *
866 - * @param mixed $message_id Notice ID.
867 - * @param mixed $content Content of message.
868 - *
869 - * @return boolean true|false.
870 - */
871 - public static function update_flash_message( $message_id, $content ) {
872 - $flash_messages = get_user_option( 'mainwp_flash_messages' );
873 - if ( ! is_array( $flash_messages ) ) {
874 - $flash_messages = array();
875 - }
876 - $current = isset( $flash_messages[ $message_id ] ) ? $flash_messages[ $message_id ] : '';
877 - if ( empty( $current ) ) {
878 - $current = $content;
879 - } else {
880 - $current .= '|' . $content;
881 - }
882 - $flash_messages[ $message_id ] = $current;
883 - return self::update_user_option( 'mainwp_flash_messages', $flash_messages );
884 - }
658 + $tz = wp_timezone();
659 + $utc_zone = new \DateTimeZone( 'UTC' );
885 660
886 - /**
887 - * Method array_sort()
888 - *
889 - * Sort given array by given flags.
890 - *
891 - * @param mixed $arr Array to sort.
892 - * @param mixed $key Array key.
893 - * @param string $sort_flag Flags to sort by. Default = SORT_STRING.
894 - */
895 - public static function array_sort( &$arr, $key, $sort_flag = SORT_STRING ) {
896 - $sorter = array();
897 - $ret = array();
898 - reset( $arr );
899 - foreach ( $arr as $ii => $val ) {
900 - $sorter[ $ii ] = $val[ $key ];
901 - }
902 - asort( $sorter, $sort_flag );
903 - foreach ( $sorter as $ii => $val ) {
904 - $ret[ $ii ] = $arr[ $ii ];
905 - }
906 - $arr = $ret;
907 - }
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 + }
908 667
909 - /**
910 - * Method enabled_wp_seo()
911 - *
912 - * Check if Yoast SEO is enabled.
913 - *
914 - * @return boolean true|false.
915 - */
916 - public static function enabled_wp_seo() {
917 - if ( null === self::$enabled_wp_seo ) {
918 - self::$enabled_wp_seo = is_plugin_active( 'wordpress-seo-extension/wordpress-seo-extension.php' );
919 - }
920 - return self::$enabled_wp_seo;
921 - }
668 + if ( $add_days ) {
669 + $dt_utc = $dt_utc->modify( sprintf( '%+d day', $add_days ) );
670 + }
922 671
923 - /**
924 - * Method value_to_string()
925 - *
926 - * Value to string.
927 - *
928 - * @param mixed $var_value Value to convert to string.
929 - *
930 - * @return string Value that has been converted into a string.
931 - */
932 - public static function value_to_string( $var_value ) {
933 - if ( is_array( $var_value ) || is_object( $var_value ) ) {
934 - //phpcs:ignore -- for debug only
935 - return print_r( $var_value, true );
936 - } elseif ( is_string( $var_value ) ) {
937 - return $var_value;
938 - }
939 - return '';
940 - }
672 + return $dt_utc->setTimezone( $tz )->format( $format_str );
673 + }
941 674
942 - /**
943 - * Get Health Site value.
944 - *
945 - * @param mixed $issue_counts Health site issues.
946 - *
947 - * @return array Health status value.
948 - */
949 - public static function get_site_health( $issue_counts ) {
950 675
951 - if ( empty( $issue_counts ) ) {
952 - $issue_counts = array(
953 - 'good' => 0,
954 - 'recommended' => 0,
955 - 'critical' => 0,
956 - );
957 - }
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 + }
958 701
959 - $totalTests = intval( $issue_counts['good'] ) + intval( $issue_counts['recommended'] ) + intval( $issue_counts['critical'] ) * 1.5;
960 - $failedTests = intval( $issue_counts['recommended'] ) * 0.5 + $issue_counts['critical'] * 1.5;
702 + $MICRO = 1000000;
703 + $SECONDS_PER_DAY = 86400;
704 + $day_micros = $SECONDS_PER_DAY * $MICRO;
961 705
962 - if ( empty( $totalTests ) ) {
963 - $val = 100;
964 - } else {
965 - $val = 100 - ceil( ( $failedTests / $totalTests ) * 100 );
966 - }
706 + $tz = new \DateTimeZone( $local_timezone );
967 707
968 - if ( 0 > $val ) {
969 - $val = 0;
970 - }
708 + // 👇 End of local day instead of start.
709 + $dt = new \DateTimeImmutable( $from_date_local . ' 23:59:59', $tz );
971 710
972 - if ( 100 < $val ) {
973 - $val = 100;
974 - }
711 + $offset_seconds = $tz->getOffset( $dt );
712 + $offset_micro = $offset_seconds * $MICRO;
975 713
976 - return array(
977 - 'val' => $val,
978 - 'critical' => $issue_counts['critical'],
979 - );
980 - }
714 + $from_date_utc = $dt->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
981 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 + }
982 723
983 - /**
984 - * Get HTTP code.
985 - *
986 - * @param int $code HTTP code.
987 - *
988 - * @return array $http_codes HTTP code.
989 - */
990 - public static function get_http_codes( $code = false ) {
991 724
992 - $http_codes = array(
993 - 100 => 'Continue',
994 - 101 => 'Switching Protocols',
995 - 200 => 'OK',
996 - 201 => 'Created',
997 - 202 => 'Accepted',
998 - 203 => 'Non-Authoritative Information',
999 - 204 => 'No Content',
1000 - 205 => 'Reset Content',
1001 - 206 => 'Partial Content',
1002 - 300 => 'Multiple Choices',
1003 - 301 => 'Moved Permanently',
1004 - 302 => 'Found',
1005 - 303 => 'See Other',
1006 - 304 => 'Not Modified',
1007 - 305 => 'Use Proxy',
1008 - 306 => '(Unused)',
1009 - 307 => 'Temporary Redirect',
1010 - 400 => 'Bad Request',
1011 - 401 => 'Unauthorized',
1012 - 402 => 'Payment Required',
1013 - 403 => 'Forbidden',
1014 - 404 => 'Not Found',
1015 - 405 => 'Method Not Allowed',
1016 - 406 => 'Not Acceptable',
1017 - 407 => 'Proxy Authentication Required',
1018 - 408 => 'Request Timeout',
1019 - 409 => 'Conflict',
1020 - 410 => 'Gone',
1021 - 411 => 'Length Required',
1022 - 412 => 'Precondition Failed',
1023 - 413 => 'Request Entity Too Large',
1024 - 414 => 'Request-URI Too Long',
1025 - 415 => 'Unsupported Media Type',
1026 - 416 => 'Requested Range Not Satisfiable',
1027 - 417 => 'Expectation Failed',
1028 - 500 => 'Internal Server Error',
1029 - 501 => 'Not Implemented',
1030 - 502 => 'Bad Gateway',
1031 - 503 => 'Service Unavailable',
1032 - 504 => 'Gateway Timeout',
1033 - 505 => 'HTTP Version Not Supported',
1034 - );
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 );
1035 739
1036 - if ( false === $code ) {
1037 - return $http_codes;
1038 - }
740 + return sprintf( "%.{$decimals}f", $bytes / pow( 1024, $factor ) ) . @$size[ $factor ];
741 + }
1039 742
1040 - return isset( $http_codes[ $code ] ) ? $http_codes[ $code ] : '';
1041 - }
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 + }
1042 757
1043 - /**
1044 - * Method valid_input_emails().
1045 - *
1046 - * @param string $emails Input emails string.
1047 - *
1048 - * @return string $valid_emails Valid emails string.
1049 - */
1050 - public static function valid_input_emails( $emails ) {
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 + }
1051 815
1052 - if ( is_string( $emails ) ) {
1053 - $emails = array_filter( explode( ',', $emails ) );
1054 - }
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 + }
1055 836
1056 - $valid_emails = array();
1057 - if ( is_array( $emails ) ) {
1058 - foreach ( $emails as $email ) {
1059 - $email = esc_html( trim( $email ) );
1060 - if ( ! empty( $email ) && ! in_array( $email, $valid_emails, true ) ) {
1061 - $valid_emails[] = $email;
1062 - }
1063 - }
1064 - }
1065 - $valid_emails = implode( ',', $valid_emails );
1066 - return $valid_emails;
1067 - }
837 + $output = array();
838 + foreach ( $arr1 as $el ) {
839 + $output[] = $el;
840 + }
841 + foreach ( $arr2 as $el ) {
842 + $output[] = $el;
843 + }
1068 844
1069 - /**
1070 - * Method check_image_file_name()
1071 - *
1072 - * Check if the file image.
1073 - *
1074 - * @param string $filename Contains image (file) name.
1075 - *
1076 - * @return true|false valid name or not.
1077 - */
1078 - public static function check_image_file_name( $filename ) {
1079 - if ( validate_file( $filename ) ) {
1080 - return false;
1081 - }
845 + return $output;
846 + }
1082 847
1083 - $allowed_files = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico' );
1084 - $file_ext = array_values( array_slice( explode( '.', $filename ), -1 ) )[0];
1085 - $file_ext = strtolower( $file_ext );
1086 - if ( ! in_array( $file_ext, $allowed_files ) ) {
1087 - return false;
1088 - }
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' );
1089 860
1090 - return true;
1091 - }
861 + if ( ! $success ) {
862 + $success = update_option( $option_name, $option_value );
863 + }
1092 864
1093 - /**
1094 - * Method check_abandoned()
1095 - *
1096 - * Get site's icon.
1097 - *
1098 - * @param mixed $siteId site's id.
1099 - * @param string $which to check plugin/theme.
1100 - *
1101 - * @return array result error or success
1102 - * @throws \Exception Error message.
1103 - */
1104 - public static function check_abandoned( $siteId = null, $which = '' ) {
1105 - if ( self::ctype_digit( $siteId ) ) {
1106 - $website = MainWP_DB::instance()->get_website_by_id( $siteId );
1107 - if ( MainWP_System_Utility::can_edit_website( $website ) ) {
1108 - $error = '';
1109 - try {
1110 - $information = MainWP_Connect::fetch_url_authed( $website, 'check_abandoned', array( 'which' => $which ) );
1111 - if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1112 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
1113 - unset( $information['sync'] );
1114 - }
1115 - } catch ( MainWP_Exception $e ) {
1116 - $error = $e->getMessage();
1117 - }
865 + return $success;
866 + }
1118 867
1119 - if ( '' !== $error ) {
1120 - return array( 'error' => $error );
1121 - } elseif ( isset( $information['success'] ) && ! empty( $information['success'] ) ) {
1122 - return array( 'result' => 'success' );
1123 - } else {
1124 - return array( 'undefined_error' => true );
1125 - }
1126 - }
1127 - }
1128 - return array( 'result' => 'NOSITE' );
1129 - }
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 + }
1130 885
1131 - /**
1132 - * Get directory or slug of plugin.
1133 - *
1134 - * @param string $slug Plugin slug.
1135 - *
1136 - * @return string $value directory or slug of plugin.
1137 - */
1138 - public static function get_dir_slug( $slug ) {
1139 - $value = '';
1140 - if ( false === strpos( $slug, '/' ) ) {
1141 - if ( false !== strpos( $slug, '.' ) ) {
1142 - $value = substr( $slug, 0, strpos( $slug, '.' ) );
1143 - }
1144 - } else {
1145 - $value = dirname( $slug );
1146 - }
1147 - if ( empty( $value ) ) {
1148 - return $slug;
1149 - }
1150 - return $value;
1151 - }
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 + }
1152 899
1153 - /**
1154 - * Metho get_siteview_mode().
1155 - *
1156 - * Get site view mode.
1157 - *
1158 - * @return string $viewmode Site view mode.
1159 - */
1160 - public static function get_siteview_mode() {
1161 - $viewmode = get_user_option( 'mainwp_sitesviewmode' );
1162 - if ( 'grid' !== $viewmode && 'table' !== $viewmode ) {
1163 - $viewmode = 'grid';
1164 - }
1165 - return $viewmode;
1166 - }
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 + }
1167 913
914 + return $text;
915 + }
1168 916
1169 - /**
1170 - * Metho delete_file().
1171 - *
1172 - * Delete file.
1173 - *
1174 - * @param string $file_path File path.
1175 - *
1176 - * @return bool true|false.
1177 - */
1178 - public static function delete_file( $file_path ) {
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 + }
1179 930
1180 - global $wp_filesystem;
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 + }
1181 947
1182 - if ( ! empty( $file_path ) ) {
1183 - if ( $wp_filesystem ) {
1184 - if ( $wp_filesystem->exists( $file_path ) ) {
1185 - $wp_filesystem->delete( $file_path );
1186 - }
1187 - } elseif ( file_exists( $file_path ) ) {
1188 - wp_delete_file( $file_path );
1189 - }
1190 - return true;
1191 - }
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 + }
1192 961
1193 - return false;
1194 - }
1195 962
1196 - /**
1197 - * Method get_disable_functions()
1198 - *
1199 - * Get disable functions.
1200 - *
1201 - * @return string
1202 - */
1203 - public function get_disable_functions() {
1204 - if ( null === self::$disabled_functions ) {
1205 - self::$disabled_functions = ini_get( 'disable_functions' );
1206 - }
1207 - return self::$disabled_functions;
1208 - }
1209 963
1210 - /**
1211 - * Method is_disable_functions()
1212 - *
1213 - * Check if it is disabled functions.
1214 - *
1215 - * @param string $func Function name to check.
1216 - *
1217 - * @return string
1218 - */
1219 - public function is_disabled_functions( $func ) {
1220 - $dis_funcs = $this->get_disable_functions();
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 + }
1221 980
1222 - if ( ! empty( $dis_funcs ) && ( false !== stristr( $dis_funcs, $func ) ) ) {
1223 - return true;
1224 - }
1225 - return false;
1226 - }
981 + if ( 'note' === $type ) {
1227 982
1228 - /**
1229 - * Method hook_verify_ping_nonce()
1230 - *
1231 - * Verify nonce without session and user id.
1232 - *
1233 - * @param bool $input_value Boolean value, it should always be FALSE.
1234 - * @param string $nonce Nonce to verify.
1235 - * @param mixed $siteid Site ID.
1236 - *
1237 - * @return mixed If verified return 1 or 2, if not return false.
1238 - */
1239 - public static function hook_verify_ping_nonce( $input_value, $nonce = '', $siteid = false ) {
1240 - $action = 'pingnonce';
1241 - return self::verify_site_nonce( $nonce, $action, $siteid );
1242 - }
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 + );
1243 999
1244 - /**
1245 - * Method create_site_nonce()
1246 - *
1247 - * Create action nonce for site.
1248 - *
1249 - * @param mixed $action Action to perform.
1250 - * @param mixed $siteid Site ID.
1251 - *
1252 - * @return string Custom nonce.
1253 - */
1254 - public static function create_site_nonce( $action = - 1, $siteid = false ) {
1255 - if ( empty( $action ) || empty( $siteid || ! is_numeric( $siteid ) ) ) {
1256 - return false;
1257 - }
1258 - return substr( wp_hash( 'site|' . $siteid . '|' . $action, 'nonce' ), - 12, 10 );
1259 - }
1000 + if ( is_array( $more_allowed ) && ! empty( $more_allowed ) ) {
1001 + $allowed_html = array_merge( $allowed_html, $more_allowed );
1002 + }
1260 1003
1261 - /**
1262 - * Method verify_site_nonce()
1263 - *
1264 - * Verify nonce without session and user id.
1265 - *
1266 - * @param string $nonce Nonce to verify.
1267 - * @param mixed $action Action to perform.
1268 - * @param mixed $siteid Site ID.
1269 - *
1270 - * @return mixed If verified return 1 or 2, if not return false.
1271 - */
1272 - public static function verify_site_nonce( $nonce, $action = - 1, $siteid = 0 ) {
1273 - $nonce = (string) $nonce;
1274 - if ( empty( $nonce ) || empty( $siteid || ! is_numeric( $siteid ) ) ) {
1275 - return false;
1276 - }
1004 + $content = wp_kses( $content, $allowed_html );
1277 1005
1278 - $expected = substr( wp_hash( 'site|' . $siteid . '|' . $action, 'nonce' ), - 12, 10 );
1279 - if ( hash_equals( $expected, $nonce ) ) {
1280 - return 1;
1281 - }
1282 - return false;
1283 - }
1006 + } elseif ( 'mixed' === $type ) {
1284 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 + );
1285 1100
1286 - /**
1287 - * Find for multi keywords.
1288 - *
1289 - * @param string $name_str string find on.
1290 - * @param array $words Array string input.
1291 - * @return bool True|False.
1292 - */
1293 - public static function multi_find_keywords( $name_str, $words = array() ) {
1294 - if ( ! is_array( $words ) ) {
1295 - return false;
1296 - }
1297 - foreach ( $words as $word ) {
1298 - if ( stristr( $name_str, $word ) ) {
1299 - return true;
1101 + if ( is_array( $more_allowed ) && ! empty( $more_allowed ) ) {
1102 + $allowed_html = array_merge( $allowed_html, $more_allowed );
1103 + }
1300 1104
1301 - }
1302 - }
1303 - return false;
1304 - }
1105 + $content = wp_kses( $content, $allowed_html );
1106 + } else {
1107 + $content = wp_kses_post( $content );
1108 + }
1305 1109
1306 - /**
1307 - * Merge values from right array to left array.
1308 - *
1309 - * @param array $left_array left array.
1310 - * @param array $right_array right array.
1311 - *
1312 - * @return array $result result array.
1313 - */
1314 - public static function right_array_merge( $left_array, $right_array ) {
1315 - if ( ! is_array( $left_array ) || ! is_array( $right_array ) ) {
1316 - return array();
1317 - }
1318 - $result = array_intersect_key( $right_array, $left_array );
1319 - return array_merge( $left_array, $result );
1320 - }
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 + }
1321 2644 }