PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
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-system-utility.php +629 -55 5.2 → 6.2.1 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_System_Utility
@@ -25,8 +30,19 @@
25 30 */
26 31 private static $instance = null;
27 32
28 33 /**
34 + * Request-local contrast results for tag backgrounds.
35 + *
36 + * Keyed by normalized #rrggbb. Invalid colors are not stored.
37 + *
38 + * @static
39 + *
40 + * @var array
41 + */
42 + private static $tag_contrast_cache = array();
43 +
44 + /**
29 45 * Method instance()
30 46 *
31 47 * Create a public static instance.
32 48 *
@@ -39,10 +55,37 @@
39 55 }
40 56 return static::$instance;
41 57 }
42 58
59 + /**
60 + * Constructor.
61 + *
62 + * Run each time the class is called.
63 + *
64 + * @return void
65 + */
66 + public function __construct() {
67 + add_action( 'admin_init', array( &$this, 'admin_init' ) );
68 + }
43 69
44 70 /**
71 + * Method admin_init().
72 + *
73 + * @return void
74 + */
75 + public function admin_init() {
76 + $current_month = gmdate( 'Y-m' );
77 + if ( get_option( 'mainwp_last_short_term_notice_purge' ) !== $current_month ) { // Run once per month.
78 + MainWP_Utility::purge_short_term_notices();
79 + update_option(
80 + 'mainwp_last_short_term_notice_purge',
81 + $current_month,
82 + false
83 + );
84 + }
85 + }
86 +
87 + /**
45 88 * Method get_class_name()
46 89 *
47 90 * Get Class Name.
48 91 *
@@ -121,12 +164,13 @@
121 164 * @return mixed False|$enable_legacy_backup.
122 165 */
123 166 public static function get_primary_backup() {
124 167 $enable_legacy_backup = get_option( 'mainwp_enableLegacyBackupFeature' );
168 + $global_method = false;
125 169 if ( ! $enable_legacy_backup ) {
126 - return get_option( 'mainwp_primaryBackup', false );
170 + $global_method = get_option( 'mainwp_primaryBackup', false );
127 171 }
128 - return false;
172 + return $global_method;
129 173 }
130 174
131 175 /**
132 176 * Method get_notification_email()
@@ -175,9 +219,9 @@
175 219 $dirs = static::get_mainwp_dir();
176 220 $dir = $dirs[0] . 'icons' . DIRECTORY_SEPARATOR;
177 221 $url = $dirs[1] . 'icons/';
178 222 if ( ! $wp_filesystem->exists( $dir ) ) {
179 - $wp_filesystem->mkdir( $dir, 0777 );
223 + $wp_filesystem->mkdir( $dir, 0755 ); // MWP-1558: tightened from 0777 to WP convention; public-asset dir, blocks cross-tenant manipulation on shared hosting.
180 224 }
181 225 if ( ! $wp_filesystem->exists( $dir . 'index.php' ) ) {
182 226 $wp_filesystem->touch( $dir . 'index.php' );
183 227 }
@@ -254,12 +298,24 @@
254 298 */
255 299 global $wp_filesystem;
256 300
257 301 $upload_dir = wp_upload_dir();
258 - $dir = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'mainwp' . DIRECTORY_SEPARATOR;
259 - $url = $upload_dir['baseurl'] . '/mainwp/';
302 +
303 + /**
304 + * Allow filtering the upload directory array used by MainWP.
305 + *
306 + * @since 5.4.1.
307 + *
308 + * @param array $upload_dir Array of upload directory info (from wp_upload_dir()).
309 + * @param string $subdir Optional. Sub Directory requested.
310 + * @param bool $direct_access Optional. Direct access.
311 + */
312 + $upload_dir = apply_filters( 'mainwp_get_wp_upload_dir', $upload_dir, $subdir, $direct_access );
313 +
314 + $dir = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'mainwp' . DIRECTORY_SEPARATOR;
315 + $url = $upload_dir['baseurl'] . '/mainwp/';
260 316 if ( ! $wp_filesystem->exists( $dir ) ) {
261 - $wp_filesystem->mkdir( $dir, 0777 );
317 + $wp_filesystem->mkdir( $dir, 0755 ); // MWP-1558: tightened from 0777 to WP convention; closes cross-tenant pk/ substitution attack on shared hosting.
262 318 }
263 319 if ( ! $wp_filesystem->exists( $dir . 'index.php' ) ) {
264 320 $wp_filesystem->touch( $dir . 'index.php' );
265 321 }
@@ -268,9 +324,10 @@
268 324 $newdir = $dir . $subdir . DIRECTORY_SEPARATOR;
269 325 $url = $url . $subdir . '/';
270 326
271 327 if ( ! $wp_filesystem->exists( $newdir ) ) {
272 - $wp_filesystem->mkdir( $newdir, 0777 );
328 + // MWP-1558: 0750 for private (htaccess-protected) subdirs, 0755 for public-asset subdirs (icons, client-images, etc.).
329 + $wp_filesystem->mkdir( $newdir, $direct_access ? 0755 : 0750 );
273 330 }
274 331
275 332 if ( $direct_access ) {
276 333 if ( ! $wp_filesystem->exists( trailingslashit( $newdir ) . 'index.php' ) ) {
@@ -329,14 +386,21 @@
329 386 *
330 387 * @return string Sig Download URL.
331 388 */
332 389 public static function get_download_sig( $fullFile ) {
333 - $key_value = uniqid( 'sig_', true ) . filesize( $fullFile ) . time();
390 + $key_value = uniqid( 'sig_', true ) . filesize( $fullFile ) . time();
391 + $secret_value = uniqid( 'sig_secret_', true ) . filesize( $fullFile ) . time();
392 +
393 + $hashkey = wp_hash( $key_value );
394 +
334 395 $sig_values = array(
335 396 'sig' => md5( filesize( $fullFile ) ), // NOSONAR - safe for sig file size.
336 397 'key_value' => $key_value,
337 - 'hash_key' => wp_hash( $key_value ),
398 + 'hash_key' => $secret_value,
338 399 );
400 +
401 + set_site_transient( 'mainwp_fdl_' . $hashkey, $secret_value, 3 * HOUR_IN_SECONDS );
402 +
339 403 $sig_values = wp_json_encode( $sig_values );
340 404 $sig_values = rawurlencode( $sig_values );
341 405 return $sig_values;
342 406 }
@@ -358,10 +422,12 @@
358 422 if ( ! is_array( $value ) || empty( $value['key_value'] ) || empty( $value['sig'] ) || md5( filesize( $file ) ) !== $value['sig'] ) { // NOSONAR - it's safe for size matching, file in uploads folder.
359 423 return false;
360 424 }
361 425
362 - $hash_key = wp_hash( $value['key_value'] );
363 - if ( ! hash_equals( $hash_key, $value['hash_key'] ) ) {
426 + $hash_key = wp_hash( $value['key_value'] );
427 + $secure_key = get_site_transient( 'mainwp_fdl_' . $hash_key );
428 +
429 + if ( empty( $secure_key ) || empty( $value['hash_key'] ) || ! hash_equals( $secure_key, $value['hash_key'] ) ) {
364 430 return false;
365 431 }
366 432
367 433 return true;
@@ -401,9 +467,11 @@
401 467 global $wp_filesystem;
402 468
403 469 $dirs = static::get_mainwp_dir();
404 470
405 - $newdir = $dirs[0] . $userid;
471 + $userdir = $dirs[0] . $userid;
472 + $newdir = $userdir;
473 +
406 474 if ( '/' === $dir || null === $dir ) {
407 475 $newdir .= DIRECTORY_SEPARATOR;
408 476 } else {
409 477 $newdir .= DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR;
@@ -410,10 +478,15 @@
410 478 }
411 479
412 480 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
413 481
482 + // need to check user dir first.
483 + if ( ! $wp_filesystem->is_dir( $userdir ) ) {
484 + $wp_filesystem->mkdir( $userdir, 0750 ); // MWP-1558: tightened from 0777; per-user backup root.
485 + }
486 +
414 487 if ( ! $wp_filesystem->is_dir( $newdir ) ) {
415 - $wp_filesystem->mkdir( $newdir, 0777 );
488 + $wp_filesystem->mkdir( $newdir, 0750 ); // MWP-1558: tightened from 0777; per-user backup subdir.
416 489 }
417 490
418 491 if ( ! empty( $dirs[0] ) . $userid && ! $wp_filesystem->exists( trailingslashit( $dirs[0] . $userid ) . '.htaccess' ) ) {
419 492 $file_htaccess = trailingslashit( $dirs[0] . $userid ) . '.htaccess';
@@ -420,10 +493,15 @@
420 493 $wp_filesystem->put_contents( $file_htaccess, 'deny from all' );
421 494 }
422 495 } else {
423 496
497 + // need to check user dir first.
498 + if ( ! file_exists( $userdir ) ) {
499 + mkdir( $userdir, 0750, true ); // MWP-1558: tightened from 0777. NOSONAR - @newdir is valid.
500 + }
501 +
424 502 if ( ! file_exists( $newdir ) ) {
425 - mkdir( $newdir, 0777, true );
503 + mkdir( $newdir, 0750, true ); // MWP-1558: tightened from 0777. NOSONAR - @newdir is valid.
426 504 }
427 505
428 506 if ( ! empty( $dirs[0] ) . $userid && ! file_exists( trailingslashit( $dirs[0] . $userid ) . '.htaccess' ) ) {
429 507 $file = fopen( trailingslashit( $dirs[0] . $userid ) . '.htaccess', 'w+' );
@@ -465,8 +543,66 @@
465 543 return $dirs[1] . $userid . '/' . $dir . '/';
466 544 }
467 545
468 546 /**
547 + * Method get_or_create_filename_secret()
548 + *
549 + * Return a stable per-install secret used to derive opaque private filenames
550 + * for sensitive files under wp-content/uploads/mainwp/. Generated once per
551 + * install and persisted as a plain wp_option (autoload off).
552 + *
553 + * Storage rationale: the secret is stored as a plain wp_option rather than
554 + * via MainWP_Keys_Manager::update_key_value(). The Keys_Manager would store
555 + * the key file inside the very pk/ directory whose filenames this secret
556 + * protects, defeating the purpose. The plaintext-in-DB trade-off is
557 + * acceptable because the threat model targets external URL guessing only;
558 + * a database read primitive that retrieves this secret would also retrieve
559 + * the encrypted privkey ciphertext it would be used to locate.
560 + *
561 + * @return string 64-character hex secret (256 bits of entropy).
562 + */
563 + public static function get_or_create_filename_secret() {
564 + // Use site-option storage so the secret is network-wide on multisite (consistent across blogs)
565 + // and falls through to get_option() on single-site. Matches MainWP_Install's pattern for network state.
566 + $secret = get_site_option( 'mainwp_private_filename_secret' );
567 + if ( ! is_string( $secret ) || ! preg_match( '/^[a-f0-9]{64}$/', $secret ) ) {
568 + $candidate = bin2hex( random_bytes( 32 ) );
569 + // add_site_option is atomic at the DB layer: only one concurrent writer wins.
570 + // Losing requests re-read so all callers see the same secret. Prevents a race where
571 + // two concurrent first-upgrade requests would both generate (and write) different
572 + // secrets, breaking subsequent filename derivations done by the losing request.
573 + if ( add_site_option( 'mainwp_private_filename_secret', $candidate ) ) {
574 + $secret = $candidate;
575 + } else {
576 + $secret = get_site_option( 'mainwp_private_filename_secret' );
577 + }
578 + }
579 + return $secret;
580 + }
581 +
582 + /**
583 + * Method get_private_filename()
584 + *
585 + * Compute the opaque filename for a private file under a wp-content/uploads/mainwp/
586 + * subdirectory. Replaces predictable patterns like 'mainwp_priv_encrypt_keys_<site_id>'
587 + * with HMAC-SHA256 derivations that are not externally enumerable.
588 + *
589 + * Filenames are deterministic per install: the same (subdir, key, purpose)
590 + * always yields the same output. Different subdir or purpose values produce
591 + * independent filename spaces with no cross-collision.
592 + *
593 + * @param string $subdir Subdirectory under mainwp/ (e.g., 'pk', 'cookies').
594 + * @param string $key Internal identifier (e.g., site_id, cookie salt).
595 + * @param string $purpose Domain separator to prevent cross-subdir filename collisions.
596 + *
597 + * @return string 64-character hex filename.
598 + */
599 + public static function get_private_filename( $subdir, $key, $purpose ) {
600 + $secret = static::get_or_create_filename_secret();
601 + return hash_hmac( 'sha256', $subdir . ':' . $purpose . ':' . $key, $secret );
602 + }
603 +
604 + /**
469 605 * Method get_mainwp_dir_allow_access()
470 606 *
471 607 * Get MainWP specific sub folder allow access.
472 608 *
@@ -629,22 +765,14 @@
629 765 $tagx = MainWP_DB_Common::instance()->get_group_by_name( $tag );
630 766 $tagc = is_object( $tagx ) && '' !== $tagx->color ? $tagx->color : '';
631 767 }
632 768
633 - if ( '' !== $tagc ) {
634 - $tag_a_style = 'style="color:#fff!important;opacity:1;"';
635 - $tag_style = 'style="background-color:' . esc_html( $tagc ) . '"';
636 - } else {
637 - $tag_a_style = '';
638 - $tag_style = '';
769 + $tag_id = '';
770 + if ( isset( $tags_ids[ $idx ] ) && ! empty( $tags_ids[ $idx ] ) ) {
771 + $tag_id = $tags_ids[ $idx ];
639 772 }
640 773
641 - if ( isset( $tags_ids[ $idx ] ) && ! empty( $tags_ids[ $idx ] ) ) {
642 - $tag_id = $tags_ids[ $idx ];
643 - $tags_labels .= '<span ' . $tag_style . ' class="ui tag mini label"><a ' . $tag_a_style . ' href="' . esc_url( $href . $tag_id ) . '">' . esc_html( $tag ) . '</a></span>';
644 - } else {
645 - $tags_labels .= '<span ' . $tag_style . ' class="ui tag mini label">' . esc_html( $tag ) . '</span>';
646 - }
774 + $tags_labels .= self::get_site_tag_label( $tag, $tag_id, $href, $tagc );
647 775 }
648 776 }
649 777 }
650 778 return $tags_labels;
@@ -681,22 +809,14 @@
681 809 foreach ( $tags as $idx => $tag ) {
682 810 $tag = trim( $tag );
683 811 $tagc = $tags_colors[ $idx ];
684 812
685 - if ( '' !== $tagc ) {
686 - $tag_a_style = 'style="color:#fff!important;opacity:1;"';
687 - $tag_style = 'style="background-color:' . esc_html( $tagc ) . '"';
688 - } else {
689 - $tag_a_style = '';
690 - $tag_style = '';
813 + $tag_id = '';
814 + if ( isset( $tags_ids[ $idx ] ) && ! empty( $tags_ids[ $idx ] ) ) {
815 + $tag_id = $tags_ids[ $idx ];
691 816 }
692 817
693 - if ( isset( $tags_ids[ $idx ] ) && ! empty( $tags_ids[ $idx ] ) ) {
694 - $tag_id = $tags_ids[ $idx ];
695 - $tags_labels .= '<span ' . $tag_style . ' class="ui tag mini label"><a ' . $tag_a_style . ' href="' . esc_url( $href . $tag_id ) . '">' . esc_html( $tag ) . '</a></span>';
696 - } else {
697 - $tags_labels .= '<span ' . $tag_style . ' class="ui tag mini label">' . esc_html( $tag ) . '</span>';
698 - }
818 + $tags_labels .= self::get_site_tag_label( $tag, $tag_id, $href, $tagc );
699 819 }
700 820 }
701 821 }
702 822 return $tags_labels;
@@ -702,8 +822,133 @@
702 822 return $tags_labels;
703 823 }
704 824
705 825 /**
826 + * Method get_tag_contrast_color()
827 + *
828 + * Pick white or black text for a tag background.
829 + * White is returned when its contrast is at least 4.5:1.
830 + *
831 + * @param string $background Stored tag color.
832 + *
833 + * @return string #ffffff, #000000, or an empty string when the color is unusable.
834 + */
835 + public static function get_tag_contrast_color( $background ) {
836 + $normalized = self::normalize_tag_color( $background );
837 + if ( '' === $normalized ) {
838 + return '';
839 + }
840 +
841 + if ( isset( self::$tag_contrast_cache[ $normalized ] ) ) {
842 + return self::$tag_contrast_cache[ $normalized ];
843 + }
844 +
845 + $white_contrast = 1.05 / ( self::tag_relative_luminance( $normalized ) + 0.05 );
846 + if ( $white_contrast >= 4.5 ) {
847 + $foreground = '#ffffff';
848 + } else {
849 + $foreground = '#000000';
850 + }
851 +
852 + self::$tag_contrast_cache[ $normalized ] = $foreground;
853 + return $foreground;
854 + }
855 +
856 + /**
857 + * Method normalize_tag_color()
858 + *
859 + * Reduce a stored tag color to lowercase #rrggbb.
860 + *
861 + * @param string $background Stored tag color.
862 + *
863 + * @return string Normalized color, or an empty string when it is not 3 or 6 digit hex.
864 + */
865 + private static function normalize_tag_color( $background ) {
866 + if ( ! is_string( $background ) ) {
867 + return '';
868 + }
869 +
870 + $background = trim( $background );
871 + if ( ! preg_match( '/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/', $background ) ) {
872 + return '';
873 + }
874 +
875 + $background = strtolower( $background );
876 + if ( 4 === strlen( $background ) ) {
877 + $background = '#' . $background[1] . $background[1] . $background[2] . $background[2] . $background[3] . $background[3];
878 + }
879 +
880 + return $background;
881 + }
882 +
883 + /**
884 + * Method tag_relative_luminance()
885 + *
886 + * WCAG relative luminance for a normalized #rrggbb color.
887 + *
888 + * @param string $hex Normalized background.
889 + *
890 + * @return float Luminance between 0 and 1.
891 + */
892 + private static function tag_relative_luminance( $hex ) {
893 + $red = self::tag_channel_linear( hexdec( substr( $hex, 1, 2 ) ) / 255 );
894 + $green = self::tag_channel_linear( hexdec( substr( $hex, 3, 2 ) ) / 255 );
895 + $blue = self::tag_channel_linear( hexdec( substr( $hex, 5, 2 ) ) / 255 );
896 +
897 + return ( 0.2126 * $red ) + ( 0.7152 * $green ) + ( 0.0722 * $blue );
898 + }
899 +
900 + /**
901 + * Method tag_channel_linear()
902 + *
903 + * Convert an sRGB channel to linear light.
904 + *
905 + * @param float $channel Channel from 0 to 1.
906 + *
907 + * @return float Linear channel.
908 + */
909 + private static function tag_channel_linear( $channel ) {
910 + if ( $channel <= 0.04045 ) {
911 + return $channel / 12.92;
912 + }
913 +
914 + return pow( ( $channel + 0.055 ) / 1.055, 2.4 );
915 + }
916 +
917 + /**
918 + * Method get_site_tag_label()
919 + *
920 + * Build one tag chip for the linked and plain renderers.
921 + *
922 + * @param string $tag Tag name.
923 + * @param string $tag_id Tag id. An empty id renders a plain chip.
924 + * @param string $href Link prefix.
925 + * @param string $background Stored background color.
926 + *
927 + * @return string Chip HTML.
928 + */
929 + private static function get_site_tag_label( $tag, $tag_id, $href, $background ) {
930 + $foreground = static::get_tag_contrast_color( $background );
931 + $tag_style = '';
932 +
933 + if ( '' !== $foreground ) {
934 + $normalized = self::normalize_tag_color( $background );
935 + $tag_style = 'style="background-color:' . esc_attr( $normalized ) . ';color:' . esc_attr( $foreground ) . '"';
936 + }
937 +
938 + if ( ! empty( $tag_id ) ) {
939 + $tag_a_style = 'style="opacity:1;"';
940 + if ( '' !== $foreground ) {
941 + $tag_a_style = 'style="color:' . esc_attr( $foreground ) . '!important;opacity:1;"';
942 + }
943 +
944 + return '<span ' . $tag_style . ' tag_id="' . esc_attr( $tag_id ) . '" class="ui tag mini label"><a ' . $tag_a_style . ' href="' . esc_url( $href . $tag_id ) . '">' . esc_html( $tag ) . '</a></span>';
945 + }
946 +
947 + return '<span ' . $tag_style . ' class="ui tag mini label">' . esc_html( $tag ) . '</span>';
948 + }
949 +
950 + /**
706 951 * Filter client tags
707 952 *
708 953 * @param array $item Array containing tags.
709 954 *
@@ -817,8 +1062,33 @@
817 1062 return $screen->id;
818 1063 }
819 1064
820 1065 /**
1066 + * Check register error
1067 + *
1068 + * @param string $error Error message.
1069 + *
1070 + * @return string
1071 + */
1072 + public static function check_register_error( $error ) {
1073 + if ( is_string( $error ) ) {
1074 + $error_langs = array(
1075 + 'This site already contains a link. Please deactivate and reactivate the',
1076 + 'Diese Website enthält bereits diesen Link',
1077 + 'Este sitio ya contiene un enlace. Por favor, desactive y vuelva a activar el',
1078 + 'Ce site contient déjà un lien. Veuillez désactiver et réactiver l’extension',
1079 + 'Este site já contém um link. Desative e reative o plug-in',
1080 + );
1081 + foreach ( $error_langs as $err ) {
1082 + if ( false !== stripos( $error, $err ) ) {
1083 + return esc_html__( 'The child site already contains connection data. To proceed, you need to clear the connection data on the child site. Navigate to WP Admin > Settings > MainWP Child > MainWP Child Settings, and click the Clear Connection Data button. After clearing the data, try reconnecting the site.', 'mainwp' );
1084 + }
1085 + }
1086 + }
1087 + return $error;
1088 + }
1089 +
1090 + /**
821 1091 * Method get_child_response()
822 1092 *
823 1093 * Get response from Child Site.
824 1094 *
@@ -831,8 +1101,9 @@
831 1101
832 1102 if ( is_array( $resp ) ) {
833 1103 if ( isset( $resp['error'] ) ) {
834 1104 $resp['error'] = MainWP_Utility::esc_content( $resp['error'] );
1105 + $resp['error'] = static::check_register_error( $resp['error'] );
835 1106 }
836 1107
837 1108 if ( isset( $resp['message'] ) && is_string( $resp['message'] ) ) {
838 1109 $resp['message'] = MainWP_Utility::esc_content( $resp['message'] );
@@ -901,14 +1172,16 @@
901 1172 /**
902 1173 * Get tokens of site.
903 1174 *
904 1175 * @param object $site The website.
1176 + * @param bool $monitor Get tokens for monitor.
1177 + * @param mixed $website_status Object containing the child site status.
905 1178 *
906 1179 * @return array Array of tokens.
907 1180 *
908 1181 * @uses \MainWP\Dashboard\MainWP_DB::get_website_option()
909 1182 */
910 - public static function get_tokens_site_values( $site ) {
1183 + public static function get_tokens_site_values( $site, $monitor = false, $website_status = false) { //phpcs:ignore -- NOSONAR -complex.
911 1184
912 1185 $tokens_values = array(
913 1186 '[site.name]' => $site->name,
914 1187 '[site.url]' => $site->url,
@@ -924,12 +1197,35 @@
924 1197 'client.site.php' => 'phpversion', // Displays the PHP version of the child site.
925 1198 'client.site.mysql' => 'mysql_version', // Displays the MySQL version of the child site.
926 1199 );
927 1200 foreach ( $map_site_tokens as $tok => $val ) {
928 - $tokens_value[ '[' . $tok . ']' ] = ( is_array( $site_info ) && isset( $site_info[ $val ] ) ) ? $site_info[ $val ] : '';
1201 + $tokens_values[ '[' . $tok . ']' ] = ( is_array( $site_info ) && isset( $site_info[ $val ] ) ) ? $site_info[ $val ] : '';
929 1202 }
930 1203 }
931 1204
1205 + if ( $monitor ) {
1206 + if ( is_object( $website_status ) && property_exists( $website_status, 'status' ) ) {
1207 + $status = $website_status->status ? 'UP' : 'DOWN';
1208 + } else {
1209 + $active_monitor = 0;
1210 + $primary_monitor = MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by( $site->id, 'issub', 0 );
1211 + if ( $primary_monitor ) {
1212 + $global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
1213 + $active_monitor = MainWP_Uptime_Monitoring_Connect::get_apply_setting( 'active', (int) $primary_monitor->active, $global_settings, -1, 60 );
1214 + }
1215 + if ( $active_monitor ) {
1216 + $status = 'PENDING';
1217 + $last = MainWP_DB_Uptime_Monitoring::instance()->get_last_site_heartbeat( $site->id, false );
1218 + if ( $last ) {
1219 + $status = $last && $last->status ? 'UP' : 'DOWN';
1220 + }
1221 + } else {
1222 + $status = 'DISABLED';
1223 + }
1224 + }
1225 +
1226 + $tokens_values['[uptime.status]'] = $status;
1227 + }
932 1228 return $tokens_values;
933 1229 }
934 1230
935 1231 /**
@@ -988,8 +1284,37 @@
988 1284 return false;
989 1285 }
990 1286
991 1287 /**
1288 + * Method save_cached_icons().
1289 + *
1290 + * Save cached icons
1291 + *
1292 + * @param string $icon The icon.
1293 + * @param string $slug slug.
1294 + * @param string $type Type: plugin|theme.
1295 + */
1296 + public static function save_cached_icons( $icon, $slug, $type ) {
1297 + $file_extension = strtolower( pathinfo( $icon, PATHINFO_EXTENSION ) );
1298 +
1299 + $file_exts = apply_filters(
1300 + 'mainwp_save_cached_icons_file_ext',
1301 + array(
1302 + 'jpeg',
1303 + 'jpg',
1304 + 'gif',
1305 + 'ico',
1306 + 'png',
1307 + )
1308 + );
1309 +
1310 + if ( ! in_array( $file_extension, $file_exts ) ) {
1311 + $icon = '';
1312 + }
1313 + static::update_cached_icons( $icon, $slug, $type, false, true );
1314 + }
1315 +
1316 + /**
992 1317 * Method update_cached_icons().
993 1318 *
994 1319 * Update cached icons
995 1320 *
@@ -996,10 +1321,11 @@
996 1321 * @param string $icon The icon.
997 1322 * @param string $slug slug.
998 1323 * @param string $type Type: plugin|theme.
999 1324 * @param bool $custom_icon Custom icon or not. Default: false.
1325 + * @param bool $noexp No expire icon - that for sync icon.
1000 1326 */
1001 - public static function update_cached_icons( $icon, $slug, $type, $custom_icon = false ) {
1327 + public static function update_cached_icons( $icon, $slug, $type, $custom_icon = false, $noexp = false ) {
1002 1328
1003 1329 if ( 'plugin' === $type ) {
1004 1330 $option_name = 'plugins_icons';
1005 1331 } elseif ( 'theme' === $type ) {
@@ -1024,16 +1350,39 @@
1024 1350
1025 1351 $value['lasttime_cached'] = time();
1026 1352
1027 1353 if ( $custom_icon ) {
1354 +
1355 + if ( isset( $value['path_custom'] ) && isset( $value['path'] ) && $value['path_custom'] === $icon && '' === $value['path'] ) {
1356 + return true; // no change.
1357 + }
1358 +
1028 1359 $value['path_custom'] = $icon;
1360 + $value['path'] = '';
1029 1361 } else {
1030 - $value['path'] = $icon;
1362 +
1363 + if ( isset( $value['path_custom'] ) && isset( $value['path'] ) && $value['path'] === $icon && '' === $value['path_custom'] ) {
1364 + return true; // no change.
1365 + }
1366 +
1367 + $value['path'] = $icon;
1368 + $value['path_custom'] = '';
1031 1369 }
1032 1370
1371 + if ( $noexp ) {
1372 + $value['noexpire'] = 1;
1373 + } elseif ( isset( $value['noexpire'] ) ) {
1374 + unset( $value['noexpire'] );
1375 + }
1376 +
1033 1377 // update cache.
1034 1378 $cached_icons[ $slug ] = $value;
1035 1379
1380 + /**
1381 + * @since 5.4.0.18
1382 + */
1383 + $cached_icons = apply_filters( 'mainwp_before_save_cached_icons', $cached_icons, $icon, $slug, $type, $custom_icon, $noexp );
1384 +
1036 1385 MainWP_DB::instance()->update_general_option( $option_name, $cached_icons, 'array' );
1037 1386 return true;
1038 1387 }
1039 1388
@@ -1194,9 +1543,9 @@
1194 1543 if ( time() > ( intval( $lasttime_clear_cached ) + MONTH_IN_SECONDS ) ) {
1195 1544 $updated = false;
1196 1545 $new_cached = array();
1197 1546 foreach ( $cached_icons as $sl => $val ) {
1198 - if ( empty( $val['path_custom'] ) && time() < ( intval( $val['lasttime_cached'] ) + 12 * MONTH_IN_SECONDS ) ) {
1547 + if ( empty( $val['noexpire'] ) && empty( $val['path_custom'] ) && time() < ( intval( $val['lasttime_cached'] ) + 12 * MONTH_IN_SECONDS ) ) {
1199 1548 $new_cached[ $sl ] = $val; // unset.
1200 1549 $updated = true;
1201 1550 }
1202 1551 }
@@ -1267,12 +1616,16 @@
1267 1616 * @param string $type Type icon, plugin|theme.
1268 1617 */
1269 1618 private static function get_plugin_theme_icon( $slug, $type ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
1270 1619
1620 + $icon_placeholder_cls = '';
1621 +
1271 1622 if ( 'plugin' === $type ) {
1272 - $option_name = 'plugins_icons';
1623 + $option_name = 'plugins_icons';
1624 + $icon_placeholder_cls = 'plug circular inverted';
1273 1625 } elseif ( 'theme' === $type ) {
1274 - $option_name = 'themes_icons';
1626 + $option_name = 'themes_icons';
1627 + $icon_placeholder_cls = 'tint circular inverted';
1275 1628 } else {
1276 1629 return '<i style="font-size: 17px" class="plug circular inverted icon" not-cached-path="true"></i>';
1277 1630 }
1278 1631
@@ -1277,8 +1630,12 @@
1277 1630 }
1278 1631
1279 1632 $cached_icons = MainWP_DB::instance()->get_general_option( $option_name, 'array' );
1280 1633
1634 + if ( ! is_array( $cached_icons ) ) {
1635 + $cached_icons = array();
1636 + }
1637 +
1281 1638 $cached_days = apply_filters( 'mainwp_plugin_theme_icon_cache_days', 15, $slug, $type ); // default 15 days.
1282 1639
1283 1640 $attr_slug = ' icon-type="' . esc_attr( $type ) . '" item-slug="' . esc_attr( $slug ) . '" ';
1284 1641 $cls_expired = ' cached-icon-expired ';
@@ -1283,8 +1640,12 @@
1283 1640 $attr_slug = ' icon-type="' . esc_attr( $type ) . '" item-slug="' . esc_attr( $slug ) . '" ';
1284 1641 $cls_expired = ' cached-icon-expired ';
1285 1642 $cls_uploadable = ' cached-icon-customable ';
1286 1643
1644 + if ( empty( $slug ) ) {
1645 + $cls_uploadable = '';
1646 + }
1647 +
1287 1648 $icon = '';
1288 1649
1289 1650 if ( isset( $cached_icons[ $slug ] ) ) {
1290 1651 $scr = '';
@@ -1315,18 +1676,18 @@
1315 1676 if ( time() > ( $lasttime_cached + $cached_days * DAY_IN_SECONDS ) || $lasttime_cached < $forced_exprided ) { // expired.
1316 1677 if ( ! empty( $scr ) ) {
1317 1678 $icon = '<img style="display:inline-block" class="ui mini circular image ' . ( $is_custom_icon ? $cls_uploadable : $cls_expired ) . '" ' . $attr_slug . 'src="' . esc_attr( $scr ) . '" alt="Icon"/>'; // to update expired icon.
1318 1679 } else {
1319 - $icon = '<i style="font-size: 17px" class="plug circular inverted icon ' . $cls_expired . $cls_uploadable . '" ' . $attr_slug . '></i>'; // to update expired icon.
1680 + $icon = '<i style="font-size: 17px" class="' . esc_attr( $icon_placeholder_cls ) . ' icon ' . $cls_expired . $cls_uploadable . '" ' . $attr_slug . '></i>'; // to update expired icon.
1320 1681 }
1321 1682 } elseif ( ! empty( $scr ) ) {
1322 1683 $use_cls_expired = $set_expired ? $cls_expired : '';
1323 1684 $icon = '<img style="display:inline-block" class="ui mini circular image ' . ( $is_custom_icon ? $cls_uploadable : $use_cls_expired ) . '" ' . $attr_slug . ' cached-path-icon="true" src="' . esc_attr( $scr ) . '" alt="Icon"/>';
1324 1685 } else {
1325 - $icon = '<i style="font-size: 17px" class="plug circular inverted icon ' . ( $set_expired ? $cls_expired : '' ) . $cls_uploadable . '" ' . $attr_slug . ' cached-path-icon="true"></i>';
1686 + $icon = '<i style="font-size: 17px" class="' . esc_attr( $icon_placeholder_cls ) . ' icon ' . ( $set_expired ? $cls_expired : '' ) . $cls_uploadable . '" ' . $attr_slug . ' cached-path-icon="true"></i>';
1326 1687 }
1327 1688 } elseif ( empty( $icon ) ) {
1328 - $icon = '<i style="font-size: 17px" class="plug circular inverted icon ' . $cls_expired . '" ' . $attr_slug . ' not-cached-path="true"></i>'; // not upload when not existed in the cached.
1689 + $icon = '<i style="font-size: 17px" class="' . esc_attr( $icon_placeholder_cls ) . ' icon ' . $cls_expired . $cls_uploadable . '" ' . $attr_slug . ' not-cached-path="true"></i>'; // not upload when not existed in the cached.
1329 1690 }
1330 1691 return $icon;
1331 1692 }
1332 1693
@@ -1348,8 +1709,15 @@
1348 1709 $dirs = static::get_mainwp_dir( $sub_folder, true );
1349 1710 $base_dir = $dirs[0];
1350 1711 $base_url = $dirs[1];
1351 1712
1713 + /**
1714 + * WordPress files system object.
1715 + *
1716 + * @global object
1717 + */
1718 + global $wp_filesystem;
1719 +
1352 1720 $output = array();
1353 1721 $filename = '';
1354 1722 $filepath = '';
1355 1723
@@ -1397,9 +1765,9 @@
1397 1765
1398 1766 $dest_file = $base_dir . '/' . $file_name;
1399 1767 $dest_file = dirname( $dest_file ) . '/' . wp_unique_filename( dirname( $dest_file ), basename( $dest_file ) );
1400 1768
1401 - if ( move_uploaded_file( $tmp_file, $dest_file ) ) {
1769 + if ( $wp_filesystem->put_contents( $dest_file, $wp_filesystem->get_contents( $tmp_file ) ) ) {
1402 1770 if ( file_exists( $dest_file ) ) {
1403 1771 list( $width, $height ) = getimagesize( $dest_file );
1404 1772 }
1405 1773
@@ -1591,22 +1959,228 @@
1591 1959 'ssl_version',
1592 1960 'sync_errors',
1593 1961 'signature_algo',
1594 1962 'verify_method',
1963 + 'suspended',
1595 1964 );
1596 1965 }
1597 1966
1598 1967 /**
1599 - * Method get_staging_options_sites_view_for_current_users()
1968 + * Method get_select_staging_view_sites()
1600 1969 *
1601 1970 * Get staging options sites view for current users.
1602 1971 *
1603 1972 * @return string Site views.
1604 1973 */
1605 - public static function get_staging_options_sites_view_for_current_users() {
1606 - $view = apply_filters( 'mainwp_staging_current_user_sites_view', 'undefined' );
1607 - if ( 'undefined' === $view ) { // to compatible.
1608 - $view = get_user_option( 'mainwp_staging_options_updates_view' );
1974 + public static function get_select_staging_view_sites() {
1975 +
1976 + $view = get_user_option( 'mainwp_staging_options_updates_view' );
1977 +
1978 + if ( empty( $view ) ) {
1979 + $view = 'livesites';
1609 1980 }
1610 - return $view;
1981 +
1982 + return apply_filters( 'mainwp_staging_current_user_sites_view', $view );
1983 + }
1984 +
1985 + /**
1986 + * Method get_custom_nonce().
1987 + *
1988 + * @param string $type Type.
1989 + * @param string $slug Slug to create nonce.
1990 + * @return string
1991 + */
1992 + public static function get_custom_nonce( $type, $slug ) {
1993 + global $current_user;
1994 + if ( empty( $current_user ) || empty( $current_user->ID ) ) {
1995 + return '';
1996 + }
1997 + return wp_create_nonce( $type . '-' . $current_user->ID . '-' . $slug );
1998 + }
1999 +
2000 + /**
2001 + * Method is_valid_custom_nonce().
2002 + *
2003 + * @param string $type Type.
2004 + * @param string $slug Slug to create nonce.
2005 + * @param string $nonce Nonce value.
2006 + * @return string
2007 + */
2008 + public static function is_valid_custom_nonce( $type, $slug, $nonce ) {
2009 + global $current_user;
2010 + if ( empty( $current_user ) || empty( $current_user->ID ) ) {
2011 + return false;
2012 + }
2013 + return wp_verify_nonce( $nonce, $type . '-' . $current_user->ID . '-' . $slug );
2014 + }
2015 +
2016 +
2017 + /**
2018 + * Method get_http_version_const_str().
2019 + *
2020 + * @param int $http_ver_int Version value.
2021 + *
2022 + * @return string Version const string.
2023 + */
2024 + public static function get_http_version_const_str( $http_ver_int ) {
2025 + $const_names = array(
2026 + 'CURL_HTTP_VERSION_1_0',
2027 + 'CURL_HTTP_VERSION_1_1',
2028 + 'CURL_HTTP_VERSION_2',
2029 + 'CURL_HTTP_VERSION_2TLS',
2030 + 'CURL_HTTP_VERSION_2_0',
2031 + 'CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE',
2032 + 'CURL_HTTP_VERSION_3',
2033 + 'CURL_HTTP_VERSION_3ONLY',
2034 + 'CURL_HTTP_VERSION_NONE',
2035 + );
2036 + foreach ( $const_names as $const ) {
2037 + if ( defined( $const ) && constant( $const ) === $http_ver_int ) {
2038 + return $const;
2039 + }
2040 + }
2041 + return $http_ver_int;
2042 + }
2043 +
2044 +
2045 + /**
2046 + * Method test_self_connect()
2047 + *
2048 + * Server self-connect test.
2049 + *
2050 + * @param string $url Dashboard self-connect URL.
2051 + *
2052 + * @return array|WP_Error
2053 + */
2054 + public static function test_self_connect( $url ) {
2055 +
2056 + $secret = static::get_self_connect_secret();
2057 + $timestamp = time();
2058 +
2059 + if ( empty( $secret ) ) {
2060 + return new \WP_Error(
2061 + 'mainwp_self_connect_secret',
2062 + __( 'Unable to generate self-connect secret.', 'mainwp' )
2063 + );
2064 + }
2065 +
2066 + $message = 'mainwp_self_connect|' . $timestamp;
2067 +
2068 + $signature = hash_hmac(
2069 + 'sha256',
2070 + $message,
2071 + $secret
2072 + );
2073 +
2074 + /**
2075 + * Filter: https_local_ssl_verify
2076 + *
2077 + * Filters whether the server-self check shoul verify SSL Cert.
2078 + *
2079 + * @since Unknown
2080 + */
2081 + $args = array(
2082 + 'blocking' => true,
2083 + 'sslverify' => apply_filters( 'https_local_ssl_verify', true ),
2084 + 'timeout' => 15,
2085 + 'headers' => array(
2086 + 'X-MainWP-Self-Connect-Timestamp' => (string) $timestamp,
2087 + 'X-MainWP-Self-Connect-Signature' => $signature,
2088 + ),
2089 + 'body' => array(
2090 + 'action' => 'mainwp_self_connect',
2091 + 'mainwp_run' => 'self_connect',
2092 + ),
2093 + );
2094 + return wp_remote_post( $url, $args );
2095 + }
2096 +
2097 +
2098 + /**
2099 + * Method handle_self_connect()
2100 + *
2101 + * Server self-connect response.
2102 + *
2103 + * @return void
2104 + */
2105 + public function handle_self_connect() {
2106 +
2107 + if ( ! $this->verify_self_connect_request() ) {
2108 + status_header( 403 );
2109 + exit;
2110 + }
2111 +
2112 + status_header( 200 );
2113 + header( 'Content-Type: text/plain; charset=utf-8' );
2114 +
2115 + echo 'MainWP Self Connect OK';
2116 + exit;
2117 + }
2118 +
2119 +
2120 + /**
2121 + * Method verify_self_connect_request()
2122 + *
2123 + * Server self-connect test.
2124 + *
2125 + * @return bool
2126 + */
2127 + private function verify_self_connect_request() {
2128 +
2129 + $signature = isset( $_SERVER['HTTP_X_MAINWP_SELF_CONNECT_SIGNATURE'] )
2130 + ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_MAINWP_SELF_CONNECT_SIGNATURE'] ) )
2131 + : '';
2132 +
2133 + $timestamp = isset( $_SERVER['HTTP_X_MAINWP_SELF_CONNECT_TIMESTAMP'] )
2134 + ? (int) $_SERVER['HTTP_X_MAINWP_SELF_CONNECT_TIMESTAMP']
2135 + : 0;
2136 +
2137 + if ( 64 !== strlen( $signature ) || empty( $timestamp ) ) {
2138 + return false;
2139 + }
2140 +
2141 + // Prevent replay attacks.
2142 + if ( abs( time() - $timestamp ) > 300 ) {
2143 + return false;
2144 + }
2145 +
2146 + $secret = static::get_self_connect_secret();
2147 +
2148 + if ( empty( $secret ) ) {
2149 + return false;
2150 + }
2151 +
2152 + $message = 'mainwp_self_connect|' . $timestamp;
2153 +
2154 + $expected = hash_hmac(
2155 + 'sha256',
2156 + $message,
2157 + $secret
2158 + );
2159 +
2160 + return hash_equals( $expected, $signature );
2161 + }
2162 +
2163 +
2164 + /**
2165 + * Method get_self_connect_secret()
2166 + *
2167 + * @return string Self connect secret.
2168 + */
2169 + public static function get_self_connect_secret() {
2170 +
2171 + $option_name = 'mainwp_self_connect_secret';
2172 + $secret = get_option( $option_name, '' );
2173 +
2174 + if ( ! is_string( $secret ) || 64 !== strlen( $secret ) ) {
2175 + try {
2176 + $secret = bin2hex( random_bytes( 32 ) );
2177 + } catch ( \Exception $e ) {
2178 + return '';
2179 + }
2180 +
2181 + update_option( $option_name, $secret, false );
2182 + }
2183 +
2184 + return $secret;
1611 2185 }
1612 2186 }