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.3 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 All 156 releases
← All changes | class/class-mainwp-db.php +174 -44 6.1.7 → 6.2.2 View file →
@@ -176,11 +176,11 @@
176 176
177 177 /**
178 178 * Method get_wp_options_join().
179 179 *
180 - * @param array $fields Extra option fields.
181 - * @param string $view_query view query.
182 - * @param array $params Additional parameters.
180 + * @param array $fields Extra option fields.
181 + * @param string $view_query view query.
182 + * @param array<string, mixed> $params Additional parameters.
183 183
184 184 *
185 185 * NOTE: This method is used to improve the performance of wp_options view, as the old view with subquery for each field will cause performance issue when there are many sites, and this method will generate the SQL with LEFT JOIN which will be much faster than subquery.
186 186 * The alias of wp table must be 'wp' to make sure the LEFT JOIN works, and the alias of wp_options table will be 'owp' + key of field in fields array, and the join condition is owp.wpid = wp.id AND owp.name = field name, and the select field is owp.value AS field name.
@@ -429,9 +429,9 @@
429 429 $status_conds = array();
430 430 if ( in_array( 'available_update', $status ) ) {
431 431 $available_sql = " ( wp.plugin_upgrades <> '' && wp.plugin_upgrades <> '[]' ) OR ( wp.theme_upgrades <> '' && wp.theme_upgrades <> '[]' ) OR ( wp.translation_upgrades <> '' && wp.translation_upgrades <> '[]' ) OR ( wp.premium_upgrades <> '' && wp.premium_upgrades <> '[]' ) ";
432 432 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
433 - $results = $this->wpdb->get_results( "SELECT wpid FROM {$table_name} WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]'" );
433 + $results = $this->wpdb->get_results( "SELECT wpid FROM {$table_name} WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the rest of the query is a literal.
434 434 if ( $results ) {
435 435 $wp_ids = array();
436 436 foreach ( $results as $item ) {
437 437 if ( ! empty( $item->wpid ) ) {
@@ -643,10 +643,11 @@
643 643 'ignored_trans_updates',
644 644 );
645 645 } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) {
646 646 $fields = array();
647 - if ( 'monitor_view' === $view ) {
647 + if ( 'monitor_view' === $view || 'ping_view' === $view ) {
648 648 $fields[] = 'health_site_status';
649 + $fields[] = 'bypass_cache';
649 650 }
650 651 } elseif ( 'custom_view' !== $view ) {
651 652 $fields = $default;
652 653 }
@@ -720,10 +721,11 @@
720 721 'ignored_trans_updates',
721 722 );
722 723 } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) {
723 724 $fields = array();
724 - if ( 'monitor_view' === $view ) {
725 + if ( 'monitor_view' === $view || 'ping_view' === $view ) {
725 726 $fields[] = 'health_site_status';
727 + $fields[] = 'bypass_cache';
726 728 }
727 729 } elseif ( 'custom_view' !== $view ) {
728 730 $fields = $default;
729 731 }
@@ -972,9 +974,9 @@
972 974 return false;
973 975 }
974 976
975 977 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
976 - $value = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT value FROM {$table_name} WHERE wpid = %d AND name = %s", $site_id, $option ) );
978 + $value = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT value FROM {$table_name} WHERE wpid = %d AND name = %s", $site_id, $option ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
977 979
978 980 if ( null === $value && null !== $default_value ) {
979 981 return $default_value;
980 982 }
@@ -1052,9 +1054,9 @@
1052 1054 }
1053 1055
1054 1056 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1055 1057 $placeholders = implode( ',', array_fill( 0, count( $get_options ), '%s' ) );
1056 - $options_db = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name, value FROM {$table_name} WHERE wpid = %d AND name IN ({$placeholders})", array_merge( array( $site_id ), $get_options ) ) );
1058 + $options_db = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name, value FROM {$table_name} WHERE wpid = %d AND name IN ({$placeholders})", array_merge( array( $site_id ), $get_options ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; $placeholders is a generated list of %s.
1057 1059
1058 1060 $fill_options = array(
1059 1061 'primary_lasttime_backup',
1060 1062 );
@@ -1091,9 +1093,9 @@
1091 1093 $site_id = $website->id;
1092 1094 }
1093 1095
1094 1096 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1095 - $rslt = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name FROM {$table_name} WHERE wpid = %d AND name = %s", $site_id, $option ) );
1097 + $rslt = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name FROM {$table_name} WHERE wpid = %d AND name = %s", $site_id, $option ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
1096 1098 if ( empty( $rslt ) ) {
1097 1099 $this->wpdb->insert(
1098 1100 $this->table_name( 'wp_options' ),
1099 1101 array(
@@ -1138,9 +1140,9 @@
1138 1140 }
1139 1141
1140 1142 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1141 1143 foreach ( $options as $opt ) {
1142 - $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE wpid=%d AND name=%s", $site_id, $opt ) );
1144 + $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE wpid=%d AND name=%s", $site_id, $opt ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
1143 1145 }
1144 1146 }
1145 1147
1146 1148
@@ -1161,9 +1163,9 @@
1161 1163 static::$general_options[] = array();
1162 1164 }
1163 1165
1164 1166 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1165 - $val = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT value FROM {$table_name} WHERE wpid = %d AND name = %s", 0, $option ) );
1167 + $val = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT value FROM {$table_name} WHERE wpid = %d AND name = %s", 0, $option ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
1166 1168
1167 1169 static::$general_options[ $option ] = $val;
1168 1170 return $val;
1169 1171 }
@@ -1204,9 +1206,9 @@
1204 1206 }
1205 1207
1206 1208 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1207 1209 $placeholders = implode( ',', array_fill( 0, count( $diff_options ), '%s' ) );
1208 - $options_db = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name, value FROM {$table_name} WHERE wpid = %d AND name IN ({$placeholders})", array_merge( array( 0 ), $diff_options ) ) );
1210 + $options_db = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name, value FROM {$table_name} WHERE wpid = %d AND name IN ({$placeholders})", array_merge( array( 0 ), $diff_options ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; $placeholders is a generated list of %s.
1209 1211
1210 1212 foreach ( (array) $options_db as $o ) {
1211 1213 $return_options[ $o->name ] = $o->value;
1212 1214 static::$general_options[ $o->name ] = $o->value;
@@ -1237,9 +1239,9 @@
1237 1239 }
1238 1240 static::$general_options[ $option ] = $value;
1239 1241
1240 1242 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1241 - $rslt = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name FROM {$table_name} WHERE wpid = %d AND name = %s", 0, $option ) );
1243 + $rslt = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name FROM {$table_name} WHERE wpid = %d AND name = %s", 0, $option ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
1242 1244
1243 1245 if ( empty( $rslt ) ) {
1244 1246 $this->wpdb->insert(
1245 1247 $this->table_name( 'wp_options' ),
@@ -1508,9 +1510,9 @@
1508 1510 $status_conds = array();
1509 1511 if ( in_array( 'available_update', $status ) ) {
1510 1512 $available_sql = " ( wp.plugin_upgrades <> '' && wp.plugin_upgrades <> '[]' ) OR ( wp.theme_upgrades <> '' && wp.theme_upgrades <> '[]' ) OR ( wp.translation_upgrades <> '' && wp.translation_upgrades <> '[]' ) OR ( wp.premium_upgrades <> '' && wp.premium_upgrades <> '[]' ) ";
1511 1513 $options_table = esc_sql( $this->table_name( 'wp_options' ) );
1512 - $results = $this->wpdb->get_results( "SELECT wpid FROM {$options_table} WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]'" );
1514 + $results = $this->wpdb->get_results( "SELECT wpid FROM {$options_table} WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the rest of the query is a literal.
1513 1515 if ( $results ) {
1514 1516 $wp_ids = array();
1515 1517 foreach ( $results as $item ) {
1516 1518 if ( ! empty( $item->wpid ) ) {
@@ -1970,9 +1972,12 @@
1970 1972 $obj_data = MainWP_Utility::map_site( $website, $data );
1971 1973
1972 1974 if ( $full_data ) {
1973 1975 $sum_upgrades = 0;
1974 - if ( '' !== $obj_data->plugin_upgrades ) {
1976 + // The option-backed columns are SQL NULL until a first sync writes them, and
1977 + // map_site() copies a null property through, so an '' check alone leaves
1978 + // json_decode() a null on PHP 8.1+.
1979 + if ( ! empty( $obj_data->plugin_upgrades ) ) {
1975 1980 $plugin_upgrades = json_decode( $obj_data->plugin_upgrades, true );
1976 1981 if ( is_array( $plugin_upgrades ) ) {
1977 1982 $sum_upgrades += count( $plugin_upgrades );
1978 1983 }
@@ -1977,9 +1982,9 @@
1977 1982 $sum_upgrades += count( $plugin_upgrades );
1978 1983 }
1979 1984 }
1980 1985
1981 - if ( '' !== $obj_data->theme_upgrades ) {
1986 + if ( ! empty( $obj_data->theme_upgrades ) ) {
1982 1987 $theme_upgrades = json_decode( $obj_data->theme_upgrades, true );
1983 1988 if ( is_array( $theme_upgrades ) ) {
1984 1989 $sum_upgrades += count( $theme_upgrades );
1985 1990 }
@@ -1984,9 +1989,9 @@
1984 1989 $sum_upgrades += count( $theme_upgrades );
1985 1990 }
1986 1991 }
1987 1992
1988 - if ( '' !== $obj_data->wp_upgrades ) {
1993 + if ( ! empty( $obj_data->wp_upgrades ) ) {
1989 1994 $wp_upgrades = json_decode( $obj_data->wp_upgrades, true );
1990 1995 if ( is_array( $wp_upgrades ) ) {
1991 1996 $sum_upgrades += count( $wp_upgrades );
1992 1997 }
@@ -2980,16 +2985,9 @@
2980 2985 $sites_table = $this->table_name( 'wp' );
2981 2986 $sync_table = $this->table_name( 'wp_sync' );
2982 2987
2983 2988 return $this->wpdb->get_row(
2984 - "
2985 - SELECT
2986 - COUNT(wp.id) AS total_sites,
2987 - SUM(CASE WHEN s.sync_errors = '' THEN 1 ELSE 0 END) AS connected_sites,
2988 - SUM(CASE WHEN s.sync_errors <> '' THEN 1 ELSE 0 END) AS disconnected_sites
2989 - FROM {$sites_table} wp
2990 - JOIN {$sync_table} s
2991 - ON s.wpid = wp.id " .
2989 + "SELECT COUNT(wp.id) AS total_sites, SUM(CASE WHEN s.sync_errors = '' THEN 1 ELSE 0 END) AS connected_sites, SUM(CASE WHEN s.sync_errors <> '' THEN 1 ELSE 0 END) AS disconnected_sites FROM {$sites_table} wp JOIN {$sync_table} s ON s.wpid = wp.id " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names come from table_name(), which is the DB prefix plus a hardcoded suffix.
2992 2990 $this->get_sql_where_allow_access_sites( 'wp', $is_staging ),
2993 2991 ARRAY_A
2994 2992 );
2995 2993 }
@@ -3163,9 +3161,9 @@
3163 3161 * @return string|null Child site IP address or null on failure.
3164 3162 */
3165 3163 public function get_wp_ip( $wpid ) {
3166 3164 $table_name = esc_sql( $this->table_name( 'request_log' ) );
3167 - return $this->wpdb->get_var( $this->wpdb->prepare( "SELECT ip FROM {$table_name} WHERE wpid = %d", $wpid ) );
3165 + return $this->wpdb->get_var( $this->wpdb->prepare( "SELECT ip FROM {$table_name} WHERE wpid = %d", $wpid ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3168 3166 }
3169 3167
3170 3168 /**
3171 3169 * Add website to the MainWP Dashboard.
@@ -3204,8 +3202,12 @@
3204 3202 $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null;
3205 3203 $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0;
3206 3204 $wpe = isset( $params['wpe'] ) ? $params['wpe'] : 0;
3207 3205 $isStaging = isset( $params['isStaging'] ) ? $params['isStaging'] : 0;
3206 + // 2 means "use the global mainwp_forceUseIPv4 option" and mainwp_string_to_bool()
3207 + // collapses it to 0, so it is kept before the boolean conversion. Null or absent
3208 + // stays null and leaves the NOT NULL column at its default.
3209 + $force_use_ipv4 = isset( $params['force_use_ipv4'] ) ? ( 2 === (int) $params['force_use_ipv4'] ? 2 : (int) mainwp_string_to_bool( $params['force_use_ipv4'] ) ) : null;
3208 3210
3209 3211 // MWP-1548: encrypt http_user / http_pass at rest. Empty / null
3210 3212 // values pass through unchanged (encrypt_credential is a no-op
3211 3213 // for those). A non-empty value that fails to encrypt produces
@@ -3264,8 +3266,12 @@
3264 3266 'wpe' => $wpe,
3265 3267 'is_staging' => $isStaging,
3266 3268 );
3267 3269
3270 + if ( null !== $force_use_ipv4 ) {
3271 + $values['force_use_ipv4'] = $force_use_ipv4;
3272 + }
3273 +
3268 3274 $syncValues = array(
3269 3275 'dtsSync' => 0,
3270 3276 'dtsSyncStart' => 0,
3271 3277 'dtsAutomaticSync' => 0,
@@ -3310,8 +3316,26 @@
3310 3316 'groupid' => $groupid,
3311 3317 )
3312 3318 );
3313 3319 }
3320 +
3321 + // Add primary monitor, use global settings.
3322 + $success = MainWP_DB_Uptime_Monitoring::instance()->update_wp_monitor(
3323 + array(
3324 + 'wpid' => $websiteid,
3325 + 'suburl' => '',
3326 + )
3327 + );
3328 +
3329 + $opt_name = 'mainwp_primary_monitor_repair_attempts_for_wp_' . $websiteid;
3330 +
3331 + if ( ! $success ) {
3332 + update_option( $opt_name, 1 );
3333 + MainWP_Utility::unhide_mainwp_message( 'failed-attempt-create-primary-monitor-' . $websiteid );
3334 + } elseif ( get_option( $opt_name ) ) {
3335 + delete_option( $opt_name );
3336 + }
3337 +
3314 3338 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3315 3339 return $websiteid;
3316 3340 }
3317 3341 }
@@ -3334,9 +3358,9 @@
3334 3358 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3335 3359 $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) );
3336 3360 $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) );
3337 3361 MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid );
3338 - MainWP_DB_Uptime_Monitoring::instance()->delete_monitor( array( 'wpid' => $websiteid ) );
3362 + MainWP_DB_Uptime_Monitoring::instance()->delete_site_monitors( $websiteid );
3339 3363 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3340 3364 return $nr;
3341 3365 }
3342 3366
@@ -3374,9 +3398,9 @@
3374 3398 }
3375 3399 // Lock the data stream to prevent other processes from updating at the same time.
3376 3400 $table_name = esc_sql( $this->table_name( 'wp' ) );
3377 3401 $sql = $this->wpdb->prepare(
3378 - "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE",
3402 + "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3379 3403 $websiteid
3380 3404 );
3381 3405 $this->wpdb->get_row( $sql );
3382 3406
@@ -3598,9 +3622,9 @@
3598 3622 $url .= '/';
3599 3623 }
3600 3624 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3601 3625 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3602 - $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT );
3626 + $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3603 3627 if ( $results ) {
3604 3628 return $results;
3605 3629 }
3606 3630
@@ -3612,9 +3636,9 @@
3612 3636 $url = str_replace( 'https://', 'https://www.', $url );
3613 3637 $url = str_replace( 'http://', 'http://www.', $url );
3614 3638 }
3615 3639
3616 - $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT );
3640 + $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3617 3641 if ( $results ) {
3618 3642 return $results;
3619 3643 }
3620 3644
@@ -3619,12 +3643,68 @@
3619 3643 }
3620 3644
3621 3645 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
3622 3646
3623 - return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') = %s", $url ), OBJECT );
3647 + return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3624 3648 }
3625 3649
3626 3650 /**
3651 + * Get recently-synced child sites whose stored url differs from the
3652 + * child-reported siteurl.
3653 + *
3654 + * The trailing slash must be normalized in SQL: stored urls always end
3655 + * with a slash while child-reported siteurl values never do, so a plain
3656 + * inequality would match nearly every site.
3657 + *
3658 + * @since 6.2
3659 + *
3660 + * @param int $days Freshness gate: only sites synced within this many days.
3661 + *
3662 + * @return array|object|null Rows with id, url, siteurl or null on failure.
3663 + */
3664 + public function get_websites_with_url_siteurl_mismatch( $days = 30 ) {
3665 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
3666 + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3667 + $minimum_sync = time() - intval( $days ) * DAY_IN_SECONDS;
3668 +
3669 + return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT wp.id, wp.url, wp.siteurl FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.siteurl <> '' AND TRIM(TRAILING '/' FROM wp.url) <> TRIM(TRAILING '/' FROM wp.siteurl) AND wp_sync.dtsSync >= %d", $minimum_sync ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3670 + }
3671 +
3672 + /**
3673 + * Get IDs of child sites that have a non-empty value for a website option.
3674 + *
3675 + * @since 6.2
3676 + *
3677 + * @param string $option_name Website option name.
3678 + * @param int $limit Maximum number of IDs to return.
3679 + *
3680 + * @return array Site IDs.
3681 + */
3682 + public function get_website_ids_with_nonempty_option( $option_name, $limit = 3 ) {
3683 + $options_table = esc_sql( $this->table_name( 'wp_options' ) );
3684 +
3685 + return $this->wpdb->get_col( $this->wpdb->prepare( "SELECT wpid FROM {$options_table} WHERE name = %s AND value <> '' ORDER BY wpid ASC LIMIT %d", $option_name, max( 1, intval( $limit ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3686 + }
3687 +
3688 + /**
3689 + * Get one child site whose stored url exactly matches the given URL,
3690 + * excluding a site ID. Used as the duplicate guard before a URL correction.
3691 + *
3692 + * @since 6.2
3693 + *
3694 + * @param string $url URL to match ( trailing slash enforced ).
3695 + * @param int $exclude_id Site ID to exclude.
3696 + *
3697 + * @return object|null Matching row ( id, url ) or null.
3698 + */
3699 + public function get_website_by_exact_url_excluding_id( $url, $exclude_id ) {
3700 + $url = rtrim( (string) $url, '/' );
3701 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
3702 +
3703 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT id, url FROM {$wp_table} WHERE TRIM(TRAILING '/' FROM url) = %s AND id <> %d", $url, intval( $exclude_id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3704 + }
3705 +
3706 + /**
3627 3707 * Method get_websites_to_notice_health_threshold()
3628 3708 *
3629 3709 * Get websites to notice site health.
3630 3710 *
@@ -3658,12 +3738,9 @@
3658 3738 $view_selects = ',' . $opts_view['selects'];
3659 3739 $view_joins = $opts_view['joins'];
3660 3740 }
3661 3741 return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery
3662 - "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp
3663 - JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid
3664 - {$view_joins}
3665 - WHERE wp.disable_health_check <> 1 AND wp.offline_check_result = 1 AND ( {$where_global_threshold} OR{$where_site_threshold} ) AND wp_sync.health_site_noticed = 0 " .
3742 + "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid {$view_joins} WHERE wp.disable_health_check <> 1 AND wp.offline_check_result = 1 AND ( {$where_global_threshold} OR{$where_site_threshold} ) AND wp_sync.health_site_noticed = 0 " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd, the view fragments come from get_wp_options_join(), which allowlists option names, and both threshold fragments are literals picked above.
3666 3743 $where . ' GROUP BY wp.id ',
3667 3744 OBJECT
3668 3745 );
3669 3746 }
@@ -3689,10 +3766,9 @@
3689 3766 }
3690 3767
3691 3768 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery.
3692 3769 return $this->wpdb->get_results(
3693 - "SELECT wp.*{$view_selects} FROM {$wp_table} wp
3694 - {$view_joins}" . ' WHERE wp.suspended = 0 AND wp.http_code_noticed = 0 AND wp.offline_check_result = -1 ' .
3770 + "SELECT wp.*{$view_selects} FROM {$wp_table} wp {$view_joins}" . ' WHERE wp.suspended = 0 AND wp.http_code_noticed = 0 AND wp.offline_check_result = -1 ' . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the view fragments come from get_wp_options_join(), which allowlists option names.
3695 3771 $where . ' GROUP BY wp.id ',
3696 3772 OBJECT
3697 3773 );
3698 3774 }
@@ -3941,18 +4017,28 @@
3941 4017 *
3942 4018 * @return mixed Result
3943 4019 */
3944 4020 public function insert_lookup_item( $item_name, $item_id, $obj_name, $obj_id ) {
4021 + // get_lookup_items() and delete_lookup_items() match item_name as given, so a
4022 + // name that sanitizing would alter is refused instead of being stored under a
4023 + // value the caller can never read back or delete.
4024 + if ( ! is_string( $item_name ) || sanitize_text_field( $item_name ) !== $item_name ) {
4025 + return false;
4026 + }
3945 4027 if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) {
3946 4028 return false;
3947 4029 }
3948 4030 $data = array(
3949 - 'item_name' => 'cost',
4031 + 'item_name' => $item_name,
3950 4032 'item_id' => $item_id,
3951 4033 'object_name' => $obj_name,
3952 4034 'object_id' => $obj_id,
3953 4035 );
3954 - $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data );
4036 + // wpdb refuses a value past the column length before any SQL runs, and
4037 + // insert_id would still hold the previous insert's id.
4038 + if ( false === $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data ) ) {
4039 + return false;
4040 + }
3955 4041 return $this->wpdb->insert_id; // must return lookup id.
3956 4042 }
3957 4043
3958 4044 /**
@@ -4127,9 +4213,9 @@
4127 4213 * @return bool result.
4128 4214 */
4129 4215 public function is_existed_enabled_rest_key() {
4130 4216 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4131 - $enabled = $this->wpdb->get_row( "SELECT * FROM {$table_name} WHERE enabled = 1 LIMIT 1" );
4217 + $enabled = $this->wpdb->get_row( "SELECT * FROM {$table_name} WHERE enabled = 1 LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4132 4218 return $enabled ? true : false;
4133 4219 }
4134 4220
4135 4221 /**
@@ -4140,9 +4226,9 @@
4140 4226 * @return array
4141 4227 */
4142 4228 public function get_rest_api_key_by( $id ) {
4143 4229 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4144 - return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table_name} WHERE key_id = %d", $id ) );
4230 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table_name} WHERE key_id = %d", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4145 4231 }
4146 4232
4147 4233 /**
4148 4234 * Method remove_rest_api_key().
@@ -4152,9 +4238,9 @@
4152 4238 * @return array
4153 4239 */
4154 4240 public function remove_rest_api_key( $id ) {
4155 4241 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4156 - return $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE key_id = %s", $id ) );
4242 + return $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE key_id = %s", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4157 4243 }
4158 4244
4159 4245 /**
4160 4246 * Method get_rest_api_keys().
@@ -4162,9 +4248,9 @@
4162 4248 * @return array
4163 4249 */
4164 4250 public function get_rest_api_keys() {
4165 4251 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4166 - return $this->wpdb->get_results( "SELECT * FROM {$table_name} ORDER BY key_id DESC" );
4252 + return $this->wpdb->get_results( "SELECT * FROM {$table_name} ORDER BY key_id DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4167 4253 }
4168 4254
4169 4255
4170 4256 /**
@@ -4234,9 +4320,9 @@
4234 4320 * @return mixed result
4235 4321 */
4236 4322 public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) {
4237 4323 $table_name = esc_sql( $this->table_name( 'schedule_processes' ) );
4238 - return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT pr.* FROM {$table_name} pr WHERE pr.item_id = %d AND pr.type = %s AND pr.process_slug = %s", $item_id, $type, $process_slug ) );
4324 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT pr.* FROM {$table_name} pr WHERE pr.item_id = %d AND pr.type = %s AND pr.process_slug = %s", $item_id, $type, $process_slug ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4239 4325 }
4240 4326
4241 4327 /**
4242 4328 * Log SQL queries for debugging via hook.
@@ -4266,6 +4352,50 @@
4266 4352 $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller );
4267 4353 if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) {
4268 4354 do_action( 'mainwp_log_system_query', $params, $sql, $caller );
4269 4355 }
4356 + }
4357 +
4358 + /**
4359 + * Method get_core_tables().
4360 + *
4361 + * Returns the list of core tables used by MainWP for validation data integrity and other purposes.
4362 + *
4363 + * @return array Core tables info
4364 + */
4365 + public function get_core_tables() {
4366 +
4367 + $core_tables = array(
4368 + $this->table_name( 'wp_clients' ),
4369 + $this->table_name( 'wp_clients_fields' ),
4370 + $this->table_name( 'wp_clients_field_values' ),
4371 + $this->table_name( 'wp_clients_contacts' ),
4372 + $this->table_name( 'monitors' ),
4373 + $this->table_name( 'monitor_heartbeat' ),
4374 + $this->table_name( 'monitor_stat_hourly' ),
4375 + $this->table_name( 'wp' ),
4376 + $this->table_name( 'wp_sync' ),
4377 + $this->table_name( 'wp_options' ),
4378 + $this->table_name( 'wp_settings_backup' ),
4379 + $this->table_name( 'users' ),
4380 + $this->table_name( 'wp_status' ),
4381 + $this->table_name( 'group' ),
4382 + $this->table_name( 'wp_group' ),
4383 + $this->table_name( 'lookup_item_objects' ),
4384 + $this->table_name( 'wp_backup_progress' ),
4385 + $this->table_name( 'wp_backup' ),
4386 + $this->table_name( 'api_keys' ),
4387 + $this->table_name( 'action_log' ),
4388 + $this->table_name( 'request_log' ),
4389 + $this->table_name( 'schedule_processes' ),
4390 + $this->table_name( 'cost_tracker' ),
4391 + );
4392 +
4393 + if ( ! defined( 'MAINWP_MODULE_LOG_ENABLED' ) || MAINWP_MODULE_LOG_ENABLED ) {
4394 + $core_tables[] = $this->table_name( 'wp_logs' );
4395 + $core_tables[] = $this->table_name( 'wp_logs_meta' );
4396 + $core_tables[] = $this->table_name( 'wp_logs_meta_archive' );
4397 + }
4398 +
4399 + return apply_filters( 'mainwp_database_core_tables', $core_tables );
4270 4400 }
4271 4401 }