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-db.php +170 -42 6.1.8 → 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 ) ) {
@@ -974,9 +974,9 @@
974 974 return false;
975 975 }
976 976
977 977 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
978 - $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.
979 979
980 980 if ( null === $value && null !== $default_value ) {
981 981 return $default_value;
982 982 }
@@ -1054,9 +1054,9 @@
1054 1054 }
1055 1055
1056 1056 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1057 1057 $placeholders = implode( ',', array_fill( 0, count( $get_options ), '%s' ) );
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 ) ) );
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.
1059 1059
1060 1060 $fill_options = array(
1061 1061 'primary_lasttime_backup',
1062 1062 );
@@ -1093,9 +1093,9 @@
1093 1093 $site_id = $website->id;
1094 1094 }
1095 1095
1096 1096 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1097 - $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.
1098 1098 if ( empty( $rslt ) ) {
1099 1099 $this->wpdb->insert(
1100 1100 $this->table_name( 'wp_options' ),
1101 1101 array(
@@ -1140,9 +1140,9 @@
1140 1140 }
1141 1141
1142 1142 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1143 1143 foreach ( $options as $opt ) {
1144 - $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.
1145 1145 }
1146 1146 }
1147 1147
1148 1148
@@ -1163,9 +1163,9 @@
1163 1163 static::$general_options[] = array();
1164 1164 }
1165 1165
1166 1166 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1167 - $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.
1168 1168
1169 1169 static::$general_options[ $option ] = $val;
1170 1170 return $val;
1171 1171 }
@@ -1206,9 +1206,9 @@
1206 1206 }
1207 1207
1208 1208 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1209 1209 $placeholders = implode( ',', array_fill( 0, count( $diff_options ), '%s' ) );
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 ) ) );
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.
1211 1211
1212 1212 foreach ( (array) $options_db as $o ) {
1213 1213 $return_options[ $o->name ] = $o->value;
1214 1214 static::$general_options[ $o->name ] = $o->value;
@@ -1239,9 +1239,9 @@
1239 1239 }
1240 1240 static::$general_options[ $option ] = $value;
1241 1241
1242 1242 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1243 - $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.
1244 1244
1245 1245 if ( empty( $rslt ) ) {
1246 1246 $this->wpdb->insert(
1247 1247 $this->table_name( 'wp_options' ),
@@ -1510,9 +1510,9 @@
1510 1510 $status_conds = array();
1511 1511 if ( in_array( 'available_update', $status ) ) {
1512 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 <> '[]' ) ";
1513 1513 $options_table = esc_sql( $this->table_name( 'wp_options' ) );
1514 - $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.
1515 1515 if ( $results ) {
1516 1516 $wp_ids = array();
1517 1517 foreach ( $results as $item ) {
1518 1518 if ( ! empty( $item->wpid ) ) {
@@ -1972,9 +1972,12 @@
1972 1972 $obj_data = MainWP_Utility::map_site( $website, $data );
1973 1973
1974 1974 if ( $full_data ) {
1975 1975 $sum_upgrades = 0;
1976 - 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 ) ) {
1977 1980 $plugin_upgrades = json_decode( $obj_data->plugin_upgrades, true );
1978 1981 if ( is_array( $plugin_upgrades ) ) {
1979 1982 $sum_upgrades += count( $plugin_upgrades );
1980 1983 }
@@ -1979,9 +1982,9 @@
1979 1982 $sum_upgrades += count( $plugin_upgrades );
1980 1983 }
1981 1984 }
1982 1985
1983 - if ( '' !== $obj_data->theme_upgrades ) {
1986 + if ( ! empty( $obj_data->theme_upgrades ) ) {
1984 1987 $theme_upgrades = json_decode( $obj_data->theme_upgrades, true );
1985 1988 if ( is_array( $theme_upgrades ) ) {
1986 1989 $sum_upgrades += count( $theme_upgrades );
1987 1990 }
@@ -1986,9 +1989,9 @@
1986 1989 $sum_upgrades += count( $theme_upgrades );
1987 1990 }
1988 1991 }
1989 1992
1990 - if ( '' !== $obj_data->wp_upgrades ) {
1993 + if ( ! empty( $obj_data->wp_upgrades ) ) {
1991 1994 $wp_upgrades = json_decode( $obj_data->wp_upgrades, true );
1992 1995 if ( is_array( $wp_upgrades ) ) {
1993 1996 $sum_upgrades += count( $wp_upgrades );
1994 1997 }
@@ -2982,16 +2985,9 @@
2982 2985 $sites_table = $this->table_name( 'wp' );
2983 2986 $sync_table = $this->table_name( 'wp_sync' );
2984 2987
2985 2988 return $this->wpdb->get_row(
2986 - "
2987 - SELECT
2988 - COUNT(wp.id) AS total_sites,
2989 - SUM(CASE WHEN s.sync_errors = '' THEN 1 ELSE 0 END) AS connected_sites,
2990 - SUM(CASE WHEN s.sync_errors <> '' THEN 1 ELSE 0 END) AS disconnected_sites
2991 - FROM {$sites_table} wp
2992 - JOIN {$sync_table} s
2993 - 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.
2994 2990 $this->get_sql_where_allow_access_sites( 'wp', $is_staging ),
2995 2991 ARRAY_A
2996 2992 );
2997 2993 }
@@ -3165,9 +3161,9 @@
3165 3161 * @return string|null Child site IP address or null on failure.
3166 3162 */
3167 3163 public function get_wp_ip( $wpid ) {
3168 3164 $table_name = esc_sql( $this->table_name( 'request_log' ) );
3169 - 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.
3170 3166 }
3171 3167
3172 3168 /**
3173 3169 * Add website to the MainWP Dashboard.
@@ -3206,8 +3202,12 @@
3206 3202 $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null;
3207 3203 $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0;
3208 3204 $wpe = isset( $params['wpe'] ) ? $params['wpe'] : 0;
3209 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;
3210 3210
3211 3211 // MWP-1548: encrypt http_user / http_pass at rest. Empty / null
3212 3212 // values pass through unchanged (encrypt_credential is a no-op
3213 3213 // for those). A non-empty value that fails to encrypt produces
@@ -3266,8 +3266,12 @@
3266 3266 'wpe' => $wpe,
3267 3267 'is_staging' => $isStaging,
3268 3268 );
3269 3269
3270 + if ( null !== $force_use_ipv4 ) {
3271 + $values['force_use_ipv4'] = $force_use_ipv4;
3272 + }
3273 +
3270 3274 $syncValues = array(
3271 3275 'dtsSync' => 0,
3272 3276 'dtsSyncStart' => 0,
3273 3277 'dtsAutomaticSync' => 0,
@@ -3312,8 +3316,26 @@
3312 3316 'groupid' => $groupid,
3313 3317 )
3314 3318 );
3315 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 +
3316 3338 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3317 3339 return $websiteid;
3318 3340 }
3319 3341 }
@@ -3336,9 +3358,9 @@
3336 3358 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3337 3359 $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) );
3338 3360 $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) );
3339 3361 MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid );
3340 - MainWP_DB_Uptime_Monitoring::instance()->delete_monitor( array( 'wpid' => $websiteid ) );
3362 + MainWP_DB_Uptime_Monitoring::instance()->delete_site_monitors( $websiteid );
3341 3363 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3342 3364 return $nr;
3343 3365 }
3344 3366
@@ -3376,9 +3398,9 @@
3376 3398 }
3377 3399 // Lock the data stream to prevent other processes from updating at the same time.
3378 3400 $table_name = esc_sql( $this->table_name( 'wp' ) );
3379 3401 $sql = $this->wpdb->prepare(
3380 - "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.
3381 3403 $websiteid
3382 3404 );
3383 3405 $this->wpdb->get_row( $sql );
3384 3406
@@ -3600,9 +3622,9 @@
3600 3622 $url .= '/';
3601 3623 }
3602 3624 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3603 3625 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3604 - $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.
3605 3627 if ( $results ) {
3606 3628 return $results;
3607 3629 }
3608 3630
@@ -3614,9 +3636,9 @@
3614 3636 $url = str_replace( 'https://', 'https://www.', $url );
3615 3637 $url = str_replace( 'http://', 'http://www.', $url );
3616 3638 }
3617 3639
3618 - $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.
3619 3641 if ( $results ) {
3620 3642 return $results;
3621 3643 }
3622 3644
@@ -3621,12 +3643,68 @@
3621 3643 }
3622 3644
3623 3645 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
3624 3646
3625 - 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.
3626 3648 }
3627 3649
3628 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 + /**
3629 3707 * Method get_websites_to_notice_health_threshold()
3630 3708 *
3631 3709 * Get websites to notice site health.
3632 3710 *
@@ -3660,12 +3738,9 @@
3660 3738 $view_selects = ',' . $opts_view['selects'];
3661 3739 $view_joins = $opts_view['joins'];
3662 3740 }
3663 3741 return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery
3664 - "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp
3665 - JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid
3666 - {$view_joins}
3667 - 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.
3668 3743 $where . ' GROUP BY wp.id ',
3669 3744 OBJECT
3670 3745 );
3671 3746 }
@@ -3691,10 +3766,9 @@
3691 3766 }
3692 3767
3693 3768 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery.
3694 3769 return $this->wpdb->get_results(
3695 - "SELECT wp.*{$view_selects} FROM {$wp_table} wp
3696 - {$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.
3697 3771 $where . ' GROUP BY wp.id ',
3698 3772 OBJECT
3699 3773 );
3700 3774 }
@@ -3943,18 +4017,28 @@
3943 4017 *
3944 4018 * @return mixed Result
3945 4019 */
3946 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 + }
3947 4027 if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) {
3948 4028 return false;
3949 4029 }
3950 4030 $data = array(
3951 - 'item_name' => 'cost',
4031 + 'item_name' => $item_name,
3952 4032 'item_id' => $item_id,
3953 4033 'object_name' => $obj_name,
3954 4034 'object_id' => $obj_id,
3955 4035 );
3956 - $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 + }
3957 4041 return $this->wpdb->insert_id; // must return lookup id.
3958 4042 }
3959 4043
3960 4044 /**
@@ -4129,9 +4213,9 @@
4129 4213 * @return bool result.
4130 4214 */
4131 4215 public function is_existed_enabled_rest_key() {
4132 4216 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4133 - $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.
4134 4218 return $enabled ? true : false;
4135 4219 }
4136 4220
4137 4221 /**
@@ -4142,9 +4226,9 @@
4142 4226 * @return array
4143 4227 */
4144 4228 public function get_rest_api_key_by( $id ) {
4145 4229 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4146 - 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.
4147 4231 }
4148 4232
4149 4233 /**
4150 4234 * Method remove_rest_api_key().
@@ -4154,9 +4238,9 @@
4154 4238 * @return array
4155 4239 */
4156 4240 public function remove_rest_api_key( $id ) {
4157 4241 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4158 - 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.
4159 4243 }
4160 4244
4161 4245 /**
4162 4246 * Method get_rest_api_keys().
@@ -4164,9 +4248,9 @@
4164 4248 * @return array
4165 4249 */
4166 4250 public function get_rest_api_keys() {
4167 4251 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4168 - 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.
4169 4253 }
4170 4254
4171 4255
4172 4256 /**
@@ -4236,9 +4320,9 @@
4236 4320 * @return mixed result
4237 4321 */
4238 4322 public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) {
4239 4323 $table_name = esc_sql( $this->table_name( 'schedule_processes' ) );
4240 - 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.
4241 4325 }
4242 4326
4243 4327 /**
4244 4328 * Log SQL queries for debugging via hook.
@@ -4268,6 +4352,50 @@
4268 4352 $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller );
4269 4353 if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) {
4270 4354 do_action( 'mainwp_log_system_query', $params, $sql, $caller );
4271 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 );
4272 4400 }
4273 4401 }