| @@ -8,8 +8,13 @@ | ||
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | 10 | namespace MainWP\Dashboard; |
| 11 | 11 | |
| 12 | +// Exit if accessed directly. | |
| 13 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 14 | + exit; | |
| 15 | +} | |
| 16 | + | |
| 12 | 17 | /** |
| 13 | 18 | * Class MainWP_DB |
| 14 | 19 | * |
| 15 | 20 | * @package MainWP\Dashboard |
| @@ -78,10 +83,41 @@ | ||
| 78 | 83 | return static::$instance; |
| 79 | 84 | } |
| 80 | 85 | |
| 81 | 86 | /** |
| 87 | + * Keep only option names that are safe to splice into SQL as an identifier. | |
| 88 | + * | |
| 89 | + * Every wp_options view builder below puts each name in identifier position | |
| 90 | + * (column and table aliases), where escape()/esc_sql() is not a defense: it | |
| 91 | + * escapes quotes but leaves backticks alone, so a name like "a`, (SELECT ...) | |
| 92 | + * AS `b" breaks out of the alias. Callers reach these through ?custom_fields on | |
| 93 | + * the REST sites endpoints and through the view_fields param of | |
| 94 | + * get_sql_website_by_params(). Anything outside [A-Za-z0-9_] is dropped rather | |
| 95 | + * than escaped, which also makes the surviving escape() calls no-ops. | |
| 96 | + * | |
| 97 | + * @param array $fields Option names. | |
| 98 | + * | |
| 99 | + * @return array Option names safe for identifier position. | |
| 100 | + */ | |
| 101 | + protected function filter_safe_option_names( $fields ) { | |
| 102 | + if ( ! is_array( $fields ) ) { | |
| 103 | + return array(); | |
| 104 | + } | |
| 105 | + return array_values( | |
| 106 | + array_filter( | |
| 107 | + $fields, | |
| 108 | + function ( $name ) { | |
| 109 | + return is_string( $name ) && preg_match( '/^[A-Za-z0-9_]+$/', $name ); | |
| 110 | + } | |
| 111 | + ) | |
| 112 | + ); | |
| 113 | + } | |
| 114 | + | |
| 115 | + /** | |
| 82 | 116 | * Get wp_options database table view. |
| 83 | 117 | * |
| 118 | + * @compatible function. | |
| 119 | + * | |
| 84 | 120 | * @param array $fields Extra option fields. |
| 85 | 121 | * @param string $view_query view query. |
| 86 | 122 | * |
| 87 | 123 | * @return array wp_options view. |
| @@ -95,9 +131,9 @@ | ||
| 95 | 131 | $view = '(SELECT intwp.id AS wpid '; |
| 96 | 132 | |
| 97 | 133 | $included_opts = array(); |
| 98 | 134 | |
| 99 | - if ( empty( $fields ) || 'default' === $view_query ) { | |
| 135 | + if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) { | |
| 100 | 136 | $view .= ',(SELECT recent_comments.value FROM ' . $this->table_name( 'wp_options' ) . ' recent_comments WHERE recent_comments.wpid = intwp.id AND recent_comments.name = "recent_comments" LIMIT 1) AS recent_comments, |
| 101 | 137 | (SELECT recent_posts.value FROM ' . $this->table_name( 'wp_options' ) . ' recent_posts WHERE recent_posts.wpid = intwp.id AND recent_posts.name = "recent_posts" LIMIT 1) AS recent_posts, |
| 102 | 138 | (SELECT recent_pages.value FROM ' . $this->table_name( 'wp_options' ) . ' recent_pages WHERE recent_pages.wpid = intwp.id AND recent_pages.name = "recent_pages" LIMIT 1) AS recent_pages, |
| 103 | 139 | (SELECT phpversion.value FROM ' . $this->table_name( 'wp_options' ) . ' phpversion WHERE phpversion.wpid = intwp.id AND phpversion.name = "phpversion" LIMIT 1) AS phpversion, |
| @@ -117,8 +153,10 @@ | ||
| 117 | 153 | if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) { |
| 118 | 154 | $fields[] = 'cust_site_icon_info'; |
| 119 | 155 | } |
| 120 | 156 | |
| 157 | + $fields = $this->filter_safe_option_names( $fields ); | |
| 158 | + | |
| 121 | 159 | if ( is_array( $fields ) ) { |
| 122 | 160 | foreach ( $fields as $field ) { |
| 123 | 161 | if ( empty( $field ) ) { |
| 124 | 162 | continue; |
| @@ -126,9 +164,9 @@ | ||
| 126 | 164 | if ( in_array( $field, $included_opts ) ) { |
| 127 | 165 | continue; |
| 128 | 166 | } |
| 129 | 167 | $view .= ', '; |
| 130 | - $view .= '(SELECT ' . $this->escape( $field ) . '.value FROM ' . $this->table_name( 'wp_options' ) . ' ' . $this->escape( $field ) . ' WHERE ' . $this->escape( $field ) . '.wpid = intwp.id AND ' . $this->escape( $field ) . '.name = "' . $this->escape( $field ) . '" LIMIT 1) AS ' . $this->escape( $field ); | |
| 168 | + $view .= '(SELECT `' . $this->escape( $field ) . '`.value FROM ' . $this->table_name( 'wp_options' ) . ' `' . $this->escape( $field ) . '` WHERE `' . $this->escape( $field ) . '`.wpid = intwp.id AND `' . $this->escape( $field ) . '`.name = "' . $this->escape( $field ) . '" LIMIT 1) AS `' . $this->escape( $field ) . '`'; | |
| 131 | 169 | } |
| 132 | 170 | } |
| 133 | 171 | |
| 134 | 172 | $view .= ' FROM ' . $this->table_name( 'wp' ) . ' intwp)'; |
| @@ -135,11 +173,150 @@ | ||
| 135 | 173 | |
| 136 | 174 | return $view; |
| 137 | 175 | } |
| 138 | 176 | |
| 177 | + /** | |
| 178 | + * Method get_wp_options_join(). | |
| 179 | + * | |
| 180 | + * @param array $fields Extra option fields. | |
| 181 | + * @param string $view_query view query. | |
| 182 | + * @param array<string, mixed> $params Additional parameters. | |
| 139 | 183 | |
| 184 | + * | |
| 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 | + * 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. | |
| 187 | + * | |
| 188 | + * @since 6.0.8 | |
| 189 | + * | |
| 190 | + * @return array wp_options view. | |
| 191 | + */ | |
| 192 | + public function get_wp_options_join( $fields = array(), $view_query = 'default', $params = array() ) { | |
| 140 | 193 | |
| 194 | + if ( ! is_array( $fields ) ) { | |
| 195 | + $fields = array(); | |
| 196 | + } | |
| 197 | + | |
| 198 | + if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) { | |
| 199 | + $fields[] = 'recent_comments'; | |
| 200 | + $fields[] = 'recent_posts'; | |
| 201 | + $fields[] = 'recent_pages'; | |
| 202 | + $fields[] = 'phpversion'; | |
| 203 | + $fields[] = 'added_timestamp'; | |
| 204 | + $fields[] = 'wp_upgrades'; | |
| 205 | + } | |
| 206 | + | |
| 207 | + if ( ! in_array( 'signature_algo', $fields ) ) { | |
| 208 | + $fields[] = 'signature_algo'; | |
| 209 | + } | |
| 210 | + | |
| 211 | + if ( ! in_array( 'verify_method', $fields ) ) { | |
| 212 | + $fields[] = 'verify_method'; | |
| 213 | + } | |
| 214 | + | |
| 215 | + if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) { | |
| 216 | + $fields[] = 'cust_site_icon_info'; | |
| 217 | + } | |
| 218 | + | |
| 219 | + $fields = array_values( array_unique( array_filter( $this->filter_safe_option_names( $fields ) ) ) ); | |
| 220 | + | |
| 221 | + $tbl_wp_options = $this->table_name( 'wp_options' ); | |
| 222 | + | |
| 223 | + $selects = array(); | |
| 224 | + $joins = array(); | |
| 225 | + | |
| 226 | + foreach ( $fields as $name ) { | |
| 227 | + $alias = 'owp_' . preg_replace( '/[^a-z0-9_]/i', '_', $name ); | |
| 228 | + | |
| 229 | + // SELECT. | |
| 230 | + $selects[] = "{$alias}.value AS `" . $this->escape( $name ) . '`'; | |
| 231 | + | |
| 232 | + // JOIN. | |
| 233 | + $joins[] = "LEFT JOIN {$tbl_wp_options} {$alias} | |
| 234 | + ON {$alias}.wpid = wp.id | |
| 235 | + AND {$alias}.name = '" . $this->escape( $name ) . "'"; | |
| 236 | + } | |
| 237 | + | |
| 238 | + return array( | |
| 239 | + 'selects' => implode( ', ', $selects ), | |
| 240 | + 'joins' => implode( "\n", $joins ), | |
| 241 | + ); | |
| 242 | + } | |
| 243 | + | |
| 141 | 244 | /** |
| 245 | + * Method get_wp_options_view(). | |
| 246 | + * | |
| 247 | + * @param array $fields Extra option fields. | |
| 248 | + * @param string $view_query view query. | |
| 249 | + * @param int $siteid Site id. | |
| 250 | + * | |
| 251 | + * @return string SQL subquery for wp_options view. | |
| 252 | + */ | |
| 253 | + public function get_wp_options_view( $fields = array(), $view_query = 'default', $siteid = 0 ) { | |
| 254 | + | |
| 255 | + if ( ! is_array( $fields ) ) { | |
| 256 | + $fields = array(); | |
| 257 | + } | |
| 258 | + | |
| 259 | + $where_site = ''; | |
| 260 | + | |
| 261 | + if ( ! empty( $siteid ) && is_numeric( $siteid ) ) { | |
| 262 | + $where_site = ' AND wpid = ' . intval( $siteid ) . ' '; | |
| 263 | + } | |
| 264 | + | |
| 265 | + $view = '(SELECT wpid '; | |
| 266 | + | |
| 267 | + $included_opts = array(); | |
| 268 | + | |
| 269 | + if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) { | |
| 270 | + $view .= ', | |
| 271 | + MAX(CASE WHEN name = "recent_comments" THEN value END) AS recent_comments, | |
| 272 | + MAX(CASE WHEN name = "recent_posts" THEN value END) AS recent_posts, | |
| 273 | + MAX(CASE WHEN name = "recent_pages" THEN value END) AS recent_pages, | |
| 274 | + MAX(CASE WHEN name = "phpversion" THEN value END) AS phpversion, | |
| 275 | + MAX(CASE WHEN name = "added_timestamp" THEN value END) AS added_timestamp, | |
| 276 | + MAX(CASE WHEN name = "wp_upgrades" THEN value END) AS wp_upgrades '; | |
| 277 | + $included_opts = array( 'recent_comments', 'recent_posts', 'recent_pages', 'phpversion', 'added_timestamp', 'wp_upgrades' ); | |
| 278 | + } | |
| 279 | + | |
| 280 | + if ( ! in_array( 'signature_algo', $fields ) ) { | |
| 281 | + $fields[] = 'signature_algo'; | |
| 282 | + } | |
| 283 | + | |
| 284 | + if ( ! in_array( 'verify_method', $fields ) ) { | |
| 285 | + $fields[] = 'verify_method'; | |
| 286 | + } | |
| 287 | + | |
| 288 | + if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) { | |
| 289 | + $fields[] = 'cust_site_icon_info'; | |
| 290 | + } | |
| 291 | + | |
| 292 | + $fields = $this->filter_safe_option_names( $fields ); | |
| 293 | + | |
| 294 | + if ( is_array( $fields ) ) { | |
| 295 | + foreach ( $fields as $field ) { | |
| 296 | + if ( empty( $field ) ) { | |
| 297 | + continue; | |
| 298 | + } | |
| 299 | + if ( in_array( $field, $included_opts ) ) { | |
| 300 | + continue; | |
| 301 | + } | |
| 302 | + $view .= ', '; | |
| 303 | + $view .= 'MAX(CASE WHEN name = "' . $this->escape( $field ) . '" THEN value END) AS `' . $this->escape( $field ) . '`'; | |
| 304 | + | |
| 305 | + $included_opts[] = $this->escape( $field ); | |
| 306 | + } | |
| 307 | + } | |
| 308 | + | |
| 309 | + $view .= ' FROM ' . $this->table_name( 'wp_options' ) . | |
| 310 | + " WHERE 1 {$where_site} AND name IN ('" . implode( "','", $included_opts ) . "') | |
| 311 | + GROUP BY wpid ) "; | |
| 312 | + | |
| 313 | + return $view; | |
| 314 | + } | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + /** | |
| 142 | 319 | * Get SQL to get child sites for current user. |
| 143 | 320 | * |
| 144 | 321 | * @since 5.2. |
| 145 | 322 | * @param array $params other params. |
| @@ -150,8 +327,16 @@ | ||
| 150 | 327 | |
| 151 | 328 | if ( ! is_array( $params ) ) { |
| 152 | 329 | $params = array(); |
| 153 | 330 | } |
| 331 | + | |
| 332 | + /** | |
| 333 | + * The hook mainwp_get_sql_websites_by_params | |
| 334 | + * | |
| 335 | + * @since 5.5 | |
| 336 | + */ | |
| 337 | + $params = apply_filters( 'mainwp_get_sql_websites_by_params', $params ); | |
| 338 | + | |
| 154 | 339 | $view = isset( $params['view'] ) ? $params['view'] : 'default'; |
| 155 | 340 | $with_clients = isset( $params['with_clients'] ) && $params['with_clients'] ? true : false; |
| 156 | 341 | |
| 157 | 342 | // legacy support. |
| @@ -157,9 +342,10 @@ | ||
| 157 | 342 | // legacy support. |
| 158 | 343 | $selectgroups = isset( $params['with_tags'] ) && $params['with_tags'] ? true : false; |
| 159 | 344 | $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url'; |
| 160 | 345 | $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false; |
| 161 | - $rowcount = isset( $params['rowcount'] ) && $params['rowcount'] ? true : false; | |
| 346 | + $rowcount = isset( $params['rowcount'] ) ? (int) $params['rowcount'] : false; | |
| 347 | + $count_sql = isset( $params['count_sql'] ) && $params['count_sql'] ? true : false; | |
| 162 | 348 | $extraWhere = isset( $params['where'] ) ? $params['where'] : null; // NOTE: without 'AND' at begining and ending of 'where'. |
| 163 | 349 | $for_manager = isset( $params['for_manager'] ) && $params['for_manager'] ? true : false; |
| 164 | 350 | $others_fields = isset( $params['others_fields'] ) && is_array( $params['others_fields'] ) ? $params['others_fields'] : array( 'favi_icon' ); |
| 165 | 351 | $is_staging = isset( $params['is_staging'] ) && in_array( $params['is_staging'], array( 'yes', 'no' ) ) ? $params['is_staging'] : 'no'; |
| @@ -164,8 +350,10 @@ | ||
| 164 | 350 | $others_fields = isset( $params['others_fields'] ) && is_array( $params['others_fields'] ) ? $params['others_fields'] : array( 'favi_icon' ); |
| 165 | 351 | $is_staging = isset( $params['is_staging'] ) && in_array( $params['is_staging'], array( 'yes', 'no' ) ) ? $params['is_staging'] : 'no'; |
| 166 | 352 | $limit = isset( $params['limit'] ) ? intval( $params['limit'] ) : ''; |
| 167 | 353 | |
| 354 | + $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false; | |
| 355 | + | |
| 168 | 356 | $s = isset( $params['s'] ) ? $params['s'] : ''; |
| 169 | 357 | $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array(); |
| 170 | 358 | $include = isset( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array(); |
| 171 | 359 | $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array(); |
| @@ -171,8 +359,14 @@ | ||
| 171 | 359 | $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array(); |
| 172 | 360 | $page = isset( $params['page'] ) ? intval( $params['page'] ) : false; |
| 173 | 361 | $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false; |
| 174 | 362 | |
| 363 | + // This parameter is used to enable caching in certain cases. | |
| 364 | + $_included_cache_ids = isset( $params['_included_cache_ids'] ) ? wp_parse_id_list( $params['_included_cache_ids'] ) : array(); | |
| 365 | + | |
| 366 | + $select_wpfields = isset( $params['select_wp_fields'] ) ? wp_parse_list( $params['select_wp_fields'] ) : ''; | |
| 367 | + $select_syncfields = isset( $params['select_sync_fields'] ) ? wp_parse_list( $params['select_sync_fields'] ) : ''; | |
| 368 | + | |
| 175 | 369 | $where = ''; |
| 176 | 370 | |
| 177 | 371 | if ( ! empty( $extraWhere ) ) { |
| 178 | 372 | $where .= ' AND ' . $extraWhere; |
| @@ -190,9 +384,17 @@ | ||
| 190 | 384 | $connected_sql = ' AND wp_sync.sync_errors <> "" '; |
| 191 | 385 | } |
| 192 | 386 | |
| 193 | 387 | if ( ! empty( $s ) ) { |
| 194 | - $where .= ' AND ( wp.id LIKE "%' . $this->escape( $s ) . '%" OR wp.name LIKE "%' . $this->escape( $s ) . '%" OR wp.url LIKE "%' . $this->escape( $s ) . '%" ) '; | |
| 388 | + $s = trim( $s ); | |
| 389 | + // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety. | |
| 390 | + $like_pattern = '%' . $this->wpdb->esc_like( $s ) . '%'; | |
| 391 | + $where .= $this->wpdb->prepare( | |
| 392 | + ' AND ( wp.id LIKE %s OR wp.name LIKE %s OR wp.url LIKE %s ) ', | |
| 393 | + $like_pattern, | |
| 394 | + $like_pattern, | |
| 395 | + $like_pattern | |
| 396 | + ); | |
| 195 | 397 | } |
| 196 | 398 | |
| 197 | 399 | if ( ! empty( $exclude ) ) { |
| 198 | 400 | $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') '; |
| @@ -201,14 +403,35 @@ | ||
| 201 | 403 | if ( ! empty( $include ) ) { |
| 202 | 404 | $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') '; |
| 203 | 405 | } |
| 204 | 406 | |
| 407 | + if ( ! empty( $_included_cache_ids ) ) { | |
| 408 | + $where .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') '; | |
| 409 | + } | |
| 410 | + | |
| 411 | + $specific_wp_fields = ''; | |
| 412 | + if ( ! empty( $select_wpfields ) ) { | |
| 413 | + foreach ( $select_wpfields as $_field ) { | |
| 414 | + $specific_wp_fields .= 'wp.' . $this->escape( $_field ) . ','; | |
| 415 | + } | |
| 416 | + $specific_wp_fields = rtrim( $specific_wp_fields, ',' ); | |
| 417 | + } | |
| 418 | + | |
| 419 | + $specific_sync_fields = ''; | |
| 420 | + if ( ! empty( $select_syncfields ) ) { | |
| 421 | + foreach ( $select_syncfields as $_field ) { | |
| 422 | + $specific_sync_fields .= 'wp_sync.' . $this->escape( $_field ) . ','; | |
| 423 | + } | |
| 424 | + $specific_sync_fields = rtrim( $specific_sync_fields, ',' ); | |
| 425 | + } | |
| 426 | + | |
| 205 | 427 | // any, connected, disconnected, suspended, available_update. |
| 206 | 428 | if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) { |
| 207 | 429 | $status_conds = array(); |
| 208 | 430 | if ( in_array( 'available_update', $status ) ) { |
| 209 | 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 <> '[]' ) "; |
| 210 | - $results = $this->wpdb->get_results( 'SELECT wpid FROM ' . $this->table_name( 'wp_options' ) . " WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]' " ); | |
| 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 <> '[]'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the rest of the query is a literal. | |
| 211 | 434 | if ( $results ) { |
| 212 | 435 | $wp_ids = array(); |
| 213 | 436 | foreach ( $results as $item ) { |
| 214 | 437 | if ( ! empty( $item->wpid ) ) { |
| @@ -223,9 +446,9 @@ | ||
| 223 | 446 | $status_conds[] = ' ( ' . $available_sql . ') '; |
| 224 | 447 | } |
| 225 | 448 | |
| 226 | 449 | if ( in_array( 'connected', $status ) ) { |
| 227 | - $status_conds[] = ' ( wp_sync.sync_errors == "" ) '; | |
| 450 | + $status_conds[] = ' ( wp_sync.sync_errors = "" ) '; | |
| 228 | 451 | } |
| 229 | 452 | if ( in_array( 'disconnected', $status ) ) { |
| 230 | 453 | $status_conds[] = " wp_sync.sync_errors <> '' "; |
| 231 | 454 | } |
| @@ -236,8 +459,11 @@ | ||
| 236 | 459 | |
| 237 | 460 | if ( ! empty( $status_conds ) ) { |
| 238 | 461 | $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) '; |
| 239 | 462 | } |
| 463 | + if ( in_array( 'unsuspended', $status ) && ! in_array( 'suspended', $status ) ) { // to sure not conflict the suspended status. | |
| 464 | + $where .= ' AND wp.suspended = 0 '; | |
| 465 | + } | |
| 240 | 466 | } |
| 241 | 467 | |
| 242 | 468 | if ( ! empty( $page ) && ! empty( $per_page ) ) { |
| 243 | 469 | $limit = ( $page - 1 ) * $per_page . ',' . $per_page; |
| @@ -251,9 +477,10 @@ | ||
| 251 | 477 | $join_clients = ''; |
| 252 | 478 | |
| 253 | 479 | if ( $with_clients ) { |
| 254 | 480 | $select_clients = ', wpclient.name as client_name '; |
| 255 | - $join_clients = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id '; | |
| 481 | + $clients_table = esc_sql( $this->table_name( 'wp_clients' ) ); | |
| 482 | + $join_clients = " LEFT JOIN {$clients_table} wpclient ON wp.client_id = wpclient.client_id "; | |
| 256 | 483 | } |
| 257 | 484 | |
| 258 | 485 | $base_fields = array( |
| 259 | 486 | 'wp.id', |
| @@ -268,9 +495,8 @@ | ||
| 268 | 495 | 'wp.privkey', |
| 269 | 496 | 'wp.pubkey', |
| 270 | 497 | 'wp.wpe', |
| 271 | 498 | 'wp.is_staging', |
| 272 | - 'wp.pubkey', | |
| 273 | 499 | 'wp.force_use_ipv4', |
| 274 | 500 | 'wp.siteurl', |
| 275 | 501 | 'wp.suspended', |
| 276 | 502 | 'wp.mainwpdir', |
| @@ -282,8 +508,9 @@ | ||
| 282 | 508 | 'wp.userid', |
| 283 | 509 | 'wp.plugins', |
| 284 | 510 | 'wp.themes', |
| 285 | 511 | 'wp.offline_check_result', // 1 - online, -1 offline. |
| 512 | + 'wp.automatic_update', | |
| 286 | 513 | ); |
| 287 | 514 | |
| 288 | 515 | $select = ' wp.*,wp_sync.* '; |
| 289 | 516 | if ( 'base_view' === $view ) { |
| @@ -299,35 +526,72 @@ | ||
| 299 | 526 | ); |
| 300 | 527 | $select = implode( ',', array_merge( $updates_fields, $base_fields ) ); |
| 301 | 528 | } |
| 302 | 529 | |
| 303 | - $select .= ',wp_optionview.* '; // to fix bug. | |
| 530 | + $view_selects = ''; | |
| 531 | + $view_joins = ''; | |
| 304 | 532 | |
| 533 | + if ( $use_comp_subquery ) { | |
| 534 | + $view_selects = ',wp_optionview.* '; | |
| 535 | + $view_joins = ' JOIN ' . $this->get_option_view_by( $view, $others_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid '; | |
| 536 | + } else { | |
| 537 | + $opts_view = $this->get_option_view_by_join( $view, $others_fields ); | |
| 538 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 539 | + $view_selects = ',' . $opts_view['selects']; | |
| 540 | + $view_joins = $opts_view['joins']; | |
| 541 | + } | |
| 542 | + } | |
| 543 | + | |
| 305 | 544 | // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax. |
| 306 | 545 | if ( $selectgroups ) { |
| 307 | - $qry = 'SELECT ' . $select . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, | |
| 308 | - ' . $select_clients . ' | |
| 309 | - FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 546 | + $select_qry = 'SELECT ' . $select . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors ' . | |
| 547 | + $select_clients; | |
| 548 | + | |
| 549 | + $qry = ' FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 310 | 550 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 311 | 551 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 312 | 552 | ' . $join_clients . ' |
| 313 | 553 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 314 | - JOIN ' . $this->get_option_view_by( $view, $others_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 315 | - WHERE 1 ' . $where . $connected_sql . ' | |
| 554 | + ' . $view_joins . ' | |
| 555 | + WHERE 1 ' . $where . $connected_sql; | |
| 556 | + $group_qry = ' | |
| 316 | 557 | GROUP BY wp.id, wp_sync.sync_id |
| 317 | 558 | ORDER BY ' . $orderBy; |
| 559 | + } elseif ( ! empty( $specific_wp_fields ) ) { // Optimize select sites data. | |
| 560 | + $select = $specific_wp_fields; | |
| 561 | + $join_sync = ''; | |
| 562 | + $group_by = 'wp.id'; | |
| 563 | + | |
| 564 | + if ( ! empty( $specific_sync_fields ) ) { | |
| 565 | + $select .= ',' . $specific_sync_fields; | |
| 566 | + $join_sync = ' JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid'; | |
| 567 | + $group_by .= ', wp_sync.sync_id'; | |
| 568 | + } | |
| 569 | + $select_qry = 'SELECT ' . $select . $view_selects; | |
| 570 | + $qry = ' FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 571 | + ' . $join_sync . ' ' . $view_joins . ' | |
| 572 | + WHERE 1 ' . $where . $connected_sql; | |
| 573 | + $group_qry = ' | |
| 574 | + GROUP BY ' . $group_by . ' | |
| 575 | + ORDER BY ' . $orderBy; | |
| 318 | 576 | } else { |
| 319 | - $qry = 'SELECT ' . $select . | |
| 320 | - $select_clients . ' | |
| 321 | - FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 577 | + $select_qry = 'SELECT ' . $select . $view_selects . $select_clients; | |
| 578 | + $qry = ' FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 322 | 579 | ' . $join_clients . ' |
| 323 | 580 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 324 | - JOIN ' . $this->get_option_view_by( $view, $others_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 325 | - WHERE 1 ' . $where . $connected_sql . ' | |
| 581 | + ' . $view_joins . ' | |
| 582 | + WHERE 1 ' . $where . $connected_sql; | |
| 583 | + $group_qry = ' | |
| 326 | 584 | GROUP BY wp.id, wp_sync.sync_id |
| 327 | 585 | ORDER BY ' . $orderBy; |
| 328 | 586 | } |
| 329 | 587 | |
| 588 | + if ( $count_sql ) { | |
| 589 | + return 'SELECT COUNT(DISTINCT wp.id) ' . $qry; | |
| 590 | + } | |
| 591 | + | |
| 592 | + $qry = $select_qry . $qry . $group_qry; | |
| 593 | + | |
| 330 | 594 | if ( ( false !== $offset ) && ( false !== $rowcount ) ) { |
| 331 | 595 | $qry .= ' LIMIT ' . $offset . ', ' . $rowcount; |
| 332 | 596 | } elseif ( false !== $rowcount ) { |
| 333 | 597 | $qry .= ' LIMIT ' . $rowcount; |
| @@ -343,14 +607,96 @@ | ||
| 343 | 607 | $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites ); |
| 344 | 608 | } |
| 345 | 609 | } |
| 346 | 610 | |
| 611 | + if ( ! empty( $_included_cache_ids ) ) { | |
| 612 | + MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql websites by params=%s]', $qry ) ); | |
| 613 | + } | |
| 614 | + MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql websites by params=%s]', $qry ) ); | |
| 347 | 615 | return $qry; |
| 348 | 616 | } |
| 349 | 617 | |
| 350 | 618 | /** |
| 619 | + * Improve get wp_options database table view. | |
| 620 | + * | |
| 621 | + * @param array $view Option view. | |
| 622 | + * @param array $other_fields Extra option fields. | |
| 623 | + * | |
| 624 | + * @return array wp_options view. | |
| 625 | + */ | |
| 626 | + public function get_option_view_by_join( $view = '', $other_fields = array() ) { | |
| 627 | + | |
| 628 | + $default = array( | |
| 629 | + 'recent_comments', | |
| 630 | + 'recent_posts', | |
| 631 | + 'recent_pages', | |
| 632 | + 'phpversion', | |
| 633 | + 'added_timestamp', | |
| 634 | + 'wp_upgrades', | |
| 635 | + ); | |
| 636 | + | |
| 637 | + $fields = array(); | |
| 638 | + | |
| 639 | + if ( 'updates_view' === $view ) { | |
| 640 | + $fields = array( | |
| 641 | + 'wp_upgrades', | |
| 642 | + 'ignored_wp_upgrades', | |
| 643 | + 'ignored_trans_updates', | |
| 644 | + ); | |
| 645 | + } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) { | |
| 646 | + $fields = array(); | |
| 647 | + if ( 'monitor_view' === $view || 'ping_view' === $view ) { | |
| 648 | + $fields[] = 'health_site_status'; | |
| 649 | + $fields[] = 'bypass_cache'; | |
| 650 | + } | |
| 651 | + } elseif ( 'custom_view' !== $view ) { | |
| 652 | + $fields = $default; | |
| 653 | + } | |
| 654 | + | |
| 655 | + if ( is_array( $other_fields ) && ! empty( $other_fields ) ) { | |
| 656 | + $fields = array_unique( array_merge( $fields, $other_fields ) ); | |
| 657 | + } | |
| 658 | + | |
| 659 | + if ( 'custom_view' !== $view ) { | |
| 660 | + if ( ! in_array( 'signature_algo', $fields ) ) { | |
| 661 | + $fields[] = 'signature_algo'; | |
| 662 | + } | |
| 663 | + | |
| 664 | + if ( ! in_array( 'verify_method', $fields ) ) { | |
| 665 | + $fields[] = 'verify_method'; | |
| 666 | + } | |
| 667 | + } | |
| 668 | + | |
| 669 | + $fields = array_values( array_filter( $this->filter_safe_option_names( $fields ) ) ); | |
| 670 | + | |
| 671 | + $tbl_wp_options = $this->table_name( 'wp_options' ); | |
| 672 | + | |
| 673 | + $selects = array(); | |
| 674 | + $joins = array(); | |
| 675 | + | |
| 676 | + foreach ( $fields as $name ) { | |
| 677 | + $alias = 'owp_' . preg_replace( '/[^a-z0-9_]/i', '_', $name ); | |
| 678 | + | |
| 679 | + // SELECT. | |
| 680 | + $selects[] = "{$alias}.value AS `" . $this->escape( $name ) . '`'; | |
| 681 | + | |
| 682 | + // JOIN. | |
| 683 | + $joins[] = "LEFT JOIN {$tbl_wp_options} {$alias} | |
| 684 | + ON {$alias}.wpid = wp.id | |
| 685 | + AND {$alias}.name = '" . $this->escape( $name ) . "'"; | |
| 686 | + } | |
| 687 | + | |
| 688 | + return array( | |
| 689 | + 'selects' => implode( ', ', $selects ), | |
| 690 | + 'joins' => implode( "\n", $joins ), | |
| 691 | + ); | |
| 692 | + } | |
| 693 | + | |
| 694 | + /** | |
| 351 | 695 | * Get wp_options database table view. |
| 352 | 696 | * |
| 697 | + * Use new get_option_view_by_join() method to improve the performance of wp_options view, and this method is used to generate the SQL with LEFT JOIN which will be much faster than subquery, but it will return the same result as get_option_view() method, and 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. | |
| 698 | + * | |
| 353 | 699 | * @param array $view Option view. |
| 354 | 700 | * @param array $other_fields Extra option fields. |
| 355 | 701 | * |
| 356 | 702 | * @return array wp_options view. |
| @@ -365,16 +711,23 @@ | ||
| 365 | 711 | 'added_timestamp', |
| 366 | 712 | 'wp_upgrades', |
| 367 | 713 | ); |
| 368 | 714 | |
| 715 | + $fields = array(); | |
| 716 | + | |
| 369 | 717 | if ( 'updates_view' === $view ) { |
| 370 | 718 | $fields = array( |
| 371 | 719 | 'wp_upgrades', |
| 372 | 720 | 'ignored_wp_upgrades', |
| 721 | + 'ignored_trans_updates', | |
| 373 | 722 | ); |
| 374 | 723 | } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) { |
| 375 | 724 | $fields = array(); |
| 376 | - } else { | |
| 725 | + if ( 'monitor_view' === $view || 'ping_view' === $view ) { | |
| 726 | + $fields[] = 'health_site_status'; | |
| 727 | + $fields[] = 'bypass_cache'; | |
| 728 | + } | |
| 729 | + } elseif ( 'custom_view' !== $view ) { | |
| 377 | 730 | $fields = $default; |
| 378 | 731 | } |
| 379 | 732 | |
| 380 | 733 | if ( is_array( $other_fields ) && ! empty( $other_fields ) ) { |
| @@ -380,17 +733,21 @@ | ||
| 380 | 733 | if ( is_array( $other_fields ) && ! empty( $other_fields ) ) { |
| 381 | 734 | $fields = array_unique( array_merge( $fields, $other_fields ) ); |
| 382 | 735 | } |
| 383 | 736 | |
| 384 | - $view_query = '(SELECT intwp.id AS wpid '; | |
| 737 | + $view_query = '(SELECT wpid '; | |
| 385 | 738 | |
| 386 | - if ( ! in_array( 'signature_algo', $fields ) ) { | |
| 387 | - $fields[] = 'signature_algo'; | |
| 739 | + if ( 'custom_view' !== $view ) { | |
| 740 | + if ( ! in_array( 'signature_algo', $fields ) ) { | |
| 741 | + $fields[] = 'signature_algo'; | |
| 742 | + } | |
| 743 | + | |
| 744 | + if ( ! in_array( 'verify_method', $fields ) ) { | |
| 745 | + $fields[] = 'verify_method'; | |
| 746 | + } | |
| 388 | 747 | } |
| 389 | 748 | |
| 390 | - if ( ! in_array( 'verify_method', $fields ) ) { | |
| 391 | - $fields[] = 'verify_method'; | |
| 392 | - } | |
| 749 | + $fields = $this->filter_safe_option_names( $fields ); | |
| 393 | 750 | |
| 394 | 751 | foreach ( $fields as $field ) { |
| 395 | 752 | |
| 396 | 753 | if ( empty( $field ) ) { |
| @@ -397,12 +754,14 @@ | ||
| 397 | 754 | continue; |
| 398 | 755 | } |
| 399 | 756 | |
| 400 | 757 | $view_query .= ', '; |
| 401 | - $view_query .= '(SELECT ' . $this->escape( $field ) . '.value FROM ' . $this->table_name( 'wp_options' ) . ' ' . $this->escape( $field ) . ' WHERE ' . $this->escape( $field ) . '.wpid = intwp.id AND ' . $this->escape( $field ) . '.name = "' . $this->escape( $field ) . '" LIMIT 1) AS ' . $this->escape( $field ); | |
| 758 | + $view_query .= 'MAX(CASE WHEN name = "' . $this->escape( $field ) . '" THEN value END) AS `' . $this->escape( $field ) . '`'; | |
| 402 | 759 | } |
| 403 | 760 | |
| 404 | - $view_query .= ' FROM ' . $this->table_name( 'wp' ) . ' intwp)'; | |
| 761 | + $view_query .= ' FROM ' . $this->table_name( 'wp_options' ) . | |
| 762 | + " WHERE name IN ('" . implode( "','", $fields ) . "') | |
| 763 | + GROUP BY wpid ) "; | |
| 405 | 764 | |
| 406 | 765 | return $view_query; |
| 407 | 766 | } |
| 408 | 767 | |
| @@ -428,18 +787,20 @@ | ||
| 428 | 787 | * |
| 429 | 788 | * @return array $connected_sites Array of connected sites. |
| 430 | 789 | */ |
| 431 | 790 | public function get_connected_websites( $sites_ids = false ) { |
| 432 | - $where = $this->get_sql_where_allow_access_sites( 'wp' ); | |
| 791 | + $where = $this->get_sql_where_allow_access_sites( 'wp' ); | |
| 792 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 793 | + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) ); | |
| 433 | 794 | |
| 434 | - $sql = 'SELECT wp.*,wp_sync.* | |
| 435 | - FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 436 | - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync | |
| 795 | + $sql = "SELECT wp.*,wp_sync.* | |
| 796 | + FROM {$wp_table} wp | |
| 797 | + JOIN {$wp_sync_table} wp_sync | |
| 437 | 798 | ON wp.id = wp_sync.wpid |
| 438 | - WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors = "") ' . | |
| 799 | + WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors = \"\") " . | |
| 439 | 800 | $where; |
| 440 | 801 | |
| 441 | - $websites = $this->wpdb->get_results( $sql ); | |
| 802 | + $websites = $this->wpdb->get_results( $sql ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is fully escaped: table names via esc_sql(), WHERE fragment from validated get_sql_where_allow_access_sites() | |
| 442 | 803 | $connected_sites = array(); |
| 443 | 804 | if ( $websites ) { |
| 444 | 805 | foreach ( $websites as $website ) { |
| 445 | 806 | |
| @@ -464,18 +825,20 @@ | ||
| 464 | 825 | * |
| 465 | 826 | * @return array $disc_sites Array of disonnected sites. |
| 466 | 827 | */ |
| 467 | 828 | public function get_disconnected_websites( $sites_ids = false ) { |
| 468 | - $where = $this->get_sql_where_allow_access_sites( 'wp' ); | |
| 829 | + $where = $this->get_sql_where_allow_access_sites( 'wp' ); | |
| 830 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 831 | + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) ); | |
| 469 | 832 | |
| 470 | - $sql = 'SELECT wp.*,wp_sync.* | |
| 471 | - FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 472 | - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync | |
| 833 | + $sql = "SELECT wp.*,wp_sync.* | |
| 834 | + FROM {$wp_table} wp | |
| 835 | + JOIN {$wp_sync_table} wp_sync | |
| 473 | 836 | ON wp.id = wp_sync.wpid |
| 474 | - WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors <> "") ' . | |
| 837 | + WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors <> \"\") " . | |
| 475 | 838 | $where; |
| 476 | 839 | |
| 477 | - $websites = $this->wpdb->get_results( $sql ); | |
| 840 | + $websites = $this->wpdb->get_results( $sql ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is fully escaped: table names via esc_sql(), WHERE fragment from validated get_sql_where_allow_access_sites() | |
| 478 | 841 | $disc_sites = array(); |
| 479 | 842 | if ( $websites ) { |
| 480 | 843 | foreach ( $websites as $website ) { |
| 481 | 844 | |
| @@ -501,8 +864,12 @@ | ||
| 501 | 864 | * |
| 502 | 865 | * @return int Child site count. |
| 503 | 866 | * |
| 504 | 867 | * @uses \MainWP\Dashboard\MainWP_System::is_multi_user() |
| 868 | + * | |
| 869 | + * @see get_websites_count_for_current_user() For Abilities API with status/tags/client filters. | |
| 870 | + * This method is intentionally simple for UI display purposes (total sites count). | |
| 871 | + * The two methods serve different use cases and should not be consolidated. | |
| 505 | 872 | */ |
| 506 | 873 | public function get_websites_count( $userId = null, $all_access = false ) { |
| 507 | 874 | static $total_sites; |
| 508 | 875 | if ( null !== $total_sites ) { // NOSONAR -- static value. |
| @@ -518,15 +885,16 @@ | ||
| 518 | 885 | global $current_user; |
| 519 | 886 | |
| 520 | 887 | $userId = $current_user->ID; |
| 521 | 888 | } |
| 522 | - $where = ( null === $userId ? '' : ' wp.userid = ' . $userId ); | |
| 889 | + $where = ( null === $userId ? '' : ' wp.userid = ' . intval( $userId ) ); | |
| 523 | 890 | if ( ! $all_access ) { |
| 524 | 891 | $where .= $this->get_sql_where_allow_access_sites( 'wp' ); |
| 525 | 892 | } |
| 526 | - $qry = 'SELECT COUNT(wp.id) FROM ' . $this->table_name( 'wp' ) . ' wp WHERE 1 ' . $where; | |
| 893 | + $table_name = esc_sql( $this->table_name( 'wp' ) ); | |
| 894 | + $qry = "SELECT COUNT(wp.id) FROM {$table_name} wp WHERE 1 {$where}"; | |
| 527 | 895 | |
| 528 | - $total = $this->wpdb->get_var( $qry ); | |
| 896 | + $total = $this->wpdb->get_var( $qry ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query is fully escaped: table names via esc_sql(), WHERE fragment from validated get_sql_where_allow_access_sites() | |
| 529 | 897 | $total_sites = $total;// NOSONAR -- static value. |
| 530 | 898 | return $total; |
| 531 | 899 | } |
| 532 | 900 | |
| @@ -554,9 +922,9 @@ | ||
| 554 | 922 | |
| 555 | 923 | $select_stats = ' ( SELECT COUNT(wp.id) as count_all '; |
| 556 | 924 | if ( ! empty( $params['count_disconnected'] ) ) { |
| 557 | 925 | $select_stats .= ',( SELECT COUNT(wp_disconnected.id) FROM ' . $this->table_name( 'wp' ) . ' wp_disconnected LEFT JOIN ' . $this->table_name( 'wp_sync' ) . ' as wp_sync '; |
| 558 | - $select_stats .= ' ON wp_disconnected.id = wp_sync.wpid WHERE wp_sync.sync_errors != "" ) as count_disconnected '; | |
| 926 | + $select_stats .= ' ON wp_disconnected.id = wp_sync.wpid WHERE wp_sync.sync_errors <> "" ) as count_disconnected '; | |
| 559 | 927 | } |
| 560 | 928 | if ( ! empty( $params['count_suspended'] ) ) { |
| 561 | 929 | $select_stats .= ',( SELECT COUNT(wp_suspended.id) FROM ' . $this->table_name( 'wp' ) . ' wp_suspended WHERE wp_suspended.suspended = 1 ) as count_suspended '; |
| 562 | 930 | } |
| @@ -605,9 +973,10 @@ | ||
| 605 | 973 | } else { |
| 606 | 974 | return false; |
| 607 | 975 | } |
| 608 | 976 | |
| 609 | - $value = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT value FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid = %d AND name = "' . $this->escape( $option ) . '"', $site_id ) ); | |
| 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 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd. | |
| 610 | 979 | |
| 611 | 980 | if ( null === $value && null !== $default_value ) { |
| 612 | 981 | return $default_value; |
| 613 | 982 | } |
| @@ -683,13 +1052,12 @@ | ||
| 683 | 1052 | if ( empty( $get_options ) ) { |
| 684 | 1053 | return $arr_options; // all options. |
| 685 | 1054 | } |
| 686 | 1055 | |
| 687 | - $options_name = implode( "','", $get_options ); | |
| 688 | - $options_name = "'" . $options_name . "'"; | |
| 1056 | + $table_name = esc_sql( $this->table_name( 'wp_options' ) ); | |
| 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 ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; $placeholders is a generated list of %s. | |
| 689 | 1059 | |
| 690 | - $options_db = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT name, value FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid = %d AND name IN (' . $options_name . ')', $site_id ) ); | |
| 691 | - | |
| 692 | 1060 | $fill_options = array( |
| 693 | 1061 | 'primary_lasttime_backup', |
| 694 | 1062 | ); |
| 695 | 1063 | |
| @@ -724,9 +1092,10 @@ | ||
| 724 | 1092 | } else { |
| 725 | 1093 | $site_id = $website->id; |
| 726 | 1094 | } |
| 727 | 1095 | |
| 728 | - $rslt = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT name FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid = %d AND name = "' . $this->escape( $option ) . '"', $site_id ) ); | |
| 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 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd. | |
| 729 | 1098 | if ( empty( $rslt ) ) { |
| 730 | 1099 | $this->wpdb->insert( |
| 731 | 1100 | $this->table_name( 'wp_options' ), |
| 732 | 1101 | array( |
| @@ -748,8 +1117,37 @@ | ||
| 748 | 1117 | } |
| 749 | 1118 | |
| 750 | 1119 | |
| 751 | 1120 | /** |
| 1121 | + * Remove child site options. | |
| 1122 | + * | |
| 1123 | + * @param object $website Child site object. | |
| 1124 | + * @param mixed $options Option to update. | |
| 1125 | + */ | |
| 1126 | + public function remove_website_option( $website, $options ) { | |
| 1127 | + | |
| 1128 | + if ( empty( $options ) ) { | |
| 1129 | + return; | |
| 1130 | + } | |
| 1131 | + | |
| 1132 | + if ( is_numeric( $website ) ) { | |
| 1133 | + $site_id = intval( $website ); | |
| 1134 | + } else { | |
| 1135 | + $site_id = $website->id; | |
| 1136 | + } | |
| 1137 | + | |
| 1138 | + if ( ! is_array( $options ) ) { | |
| 1139 | + $options = (array) $options; | |
| 1140 | + } | |
| 1141 | + | |
| 1142 | + $table_name = esc_sql( $this->table_name( 'wp_options' ) ); | |
| 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 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd. | |
| 1145 | + } | |
| 1146 | + } | |
| 1147 | + | |
| 1148 | + | |
| 1149 | + /** | |
| 752 | 1150 | * Get general Child site option. |
| 753 | 1151 | * |
| 754 | 1152 | * @param mixed $option Child Site option name. |
| 755 | 1153 | * |
| @@ -764,9 +1162,10 @@ | ||
| 764 | 1162 | } else { |
| 765 | 1163 | static::$general_options[] = array(); |
| 766 | 1164 | } |
| 767 | 1165 | |
| 768 | - $val = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT value FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid = %d AND name = "' . $this->escape( $option ) . '"', 0 ) ); | |
| 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 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd. | |
| 769 | 1168 | |
| 770 | 1169 | static::$general_options[ $option ] = $val; |
| 771 | 1170 | return $val; |
| 772 | 1171 | } |
| @@ -801,12 +1200,15 @@ | ||
| 801 | 1200 | $diff_options[] = $opt; |
| 802 | 1201 | } |
| 803 | 1202 | } |
| 804 | 1203 | |
| 805 | - $options_name = implode( "','", $diff_options ); | |
| 806 | - $options_name = "'" . $options_name . "'"; | |
| 1204 | + if ( empty( $diff_options ) ) { | |
| 1205 | + return $return_options; | |
| 1206 | + } | |
| 807 | 1207 | |
| 808 | - $options_db = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT name, value FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid = %d AND name IN (' . $options_name . ')', 0 ) ); | |
| 1208 | + $table_name = esc_sql( $this->table_name( 'wp_options' ) ); | |
| 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 ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; $placeholders is a generated list of %s. | |
| 809 | 1211 | |
| 810 | 1212 | foreach ( (array) $options_db as $o ) { |
| 811 | 1213 | $return_options[ $o->name ] = $o->value; |
| 812 | 1214 | static::$general_options[ $o->name ] = $o->value; |
| @@ -836,9 +1238,10 @@ | ||
| 836 | 1238 | static::$general_options[] = array(); |
| 837 | 1239 | } |
| 838 | 1240 | static::$general_options[ $option ] = $value; |
| 839 | 1241 | |
| 840 | - $rslt = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT name FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid = %d AND name = "' . $this->escape( $option ) . '"', 0 ) ); | |
| 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 ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd. | |
| 841 | 1244 | |
| 842 | 1245 | if ( empty( $rslt ) ) { |
| 843 | 1246 | $this->wpdb->insert( |
| 844 | 1247 | $this->table_name( 'wp_options' ), |
| @@ -903,13 +1306,23 @@ | ||
| 903 | 1306 | */ |
| 904 | 1307 | public function get_sql_websites() { |
| 905 | 1308 | $where = $this->get_sql_where_allow_access_sites( 'wp' ); |
| 906 | 1309 | |
| 907 | - return 'SELECT wp.*,wp_sync.*,wp_optionview.* | |
| 1310 | + $view_selects = ''; | |
| 1311 | + $view_joins = ''; | |
| 1312 | + | |
| 1313 | + $opts_view = $this->get_wp_options_join(); | |
| 1314 | + | |
| 1315 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 1316 | + $view_selects = ',' . $opts_view['selects']; | |
| 1317 | + $view_joins = $opts_view['joins']; | |
| 1318 | + } | |
| 1319 | + | |
| 1320 | + return 'SELECT wp.*,wp_sync.*' . $view_selects . ' | |
| 908 | 1321 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 909 | 1322 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 910 | - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 911 | - WHERE 1 ' . $where; | |
| 1323 | + ' . $view_joins . ' | |
| 1324 | + WHERE 1 ' . $where . ' ORDER BY wp.id'; | |
| 912 | 1325 | } |
| 913 | 1326 | |
| 914 | 1327 | /** |
| 915 | 1328 | * Get child sites by user id via SQL. |
| @@ -934,24 +1347,34 @@ | ||
| 934 | 1347 | } |
| 935 | 1348 | |
| 936 | 1349 | $where .= $this->get_sql_where_allow_access_sites( 'wp' ); |
| 937 | 1350 | |
| 1351 | + $view_selects = ''; | |
| 1352 | + $view_joins = ''; | |
| 1353 | + | |
| 1354 | + $opts_view = $this->get_wp_options_join(); | |
| 1355 | + | |
| 1356 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 1357 | + $view_selects = ',' . $opts_view['selects']; | |
| 1358 | + $view_joins = $opts_view['joins']; | |
| 1359 | + } | |
| 1360 | + | |
| 938 | 1361 | if ( $selectgroups ) { |
| 939 | - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 1362 | + $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 940 | 1363 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 941 | 1364 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 942 | 1365 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 943 | 1366 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 944 | - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 1367 | + ' . $view_joins . ' | |
| 945 | 1368 | WHERE wp.userid = ' . $userid . " |
| 946 | 1369 | $where |
| 947 | 1370 | GROUP BY wp.id, wp_sync.sync_id |
| 948 | 1371 | ORDER BY " . $orderBy; |
| 949 | 1372 | } else { |
| 950 | - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.* | |
| 1373 | + $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ' | |
| 951 | 1374 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 952 | 1375 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 953 | - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 1376 | + ' . $view_joins . ' | |
| 954 | 1377 | WHERE wp.userid = ' . $userid . " |
| 955 | 1378 | $where |
| 956 | 1379 | ORDER BY " . $orderBy; |
| 957 | 1380 | } |
| @@ -998,8 +1421,15 @@ | ||
| 998 | 1421 | $is_staging = 'no', |
| 999 | 1422 | $params = array() |
| 1000 | 1423 | ) { |
| 1001 | 1424 | |
| 1425 | + /** | |
| 1426 | + * The hook mainwp_get_sql_websites | |
| 1427 | + * | |
| 1428 | + * @since 5.5 | |
| 1429 | + */ | |
| 1430 | + $params = apply_filters( 'mainwp_get_sql_websites', $params, $selectgroups, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager, $extra_view, $is_staging ); | |
| 1431 | + | |
| 1002 | 1432 | $where = ''; |
| 1003 | 1433 | if ( MainWP_System::instance()->is_multi_user() ) { |
| 1004 | 1434 | |
| 1005 | 1435 | /** |
| @@ -1013,9 +1443,15 @@ | ||
| 1013 | 1443 | } |
| 1014 | 1444 | |
| 1015 | 1445 | if ( null !== $search_site ) { |
| 1016 | 1446 | $search_site = trim( $search_site ); |
| 1017 | - $where .= ' AND (wp.name LIKE "%' . $search_site . '%" OR wp.url LIKE "%' . $search_site . '%") '; | |
| 1447 | + // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety. | |
| 1448 | + $like_pattern = '%' . $this->wpdb->esc_like( $search_site ) . '%'; | |
| 1449 | + $where .= $this->wpdb->prepare( | |
| 1450 | + ' AND (wp.name LIKE %s OR wp.url LIKE %s) ', | |
| 1451 | + $like_pattern, | |
| 1452 | + $like_pattern | |
| 1453 | + ); | |
| 1018 | 1454 | } |
| 1019 | 1455 | |
| 1020 | 1456 | if ( ! empty( $extraWhere ) ) { |
| 1021 | 1457 | $where .= ' AND ' . $extraWhere . ' '; |
| @@ -1032,8 +1468,10 @@ | ||
| 1032 | 1468 | } elseif ( is_array( $params ) && isset( $params['connected'] ) && 'no' === $params['connected'] ) { |
| 1033 | 1469 | $connected_sql = ' AND wp_sync.sync_errors <> "" '; |
| 1034 | 1470 | } |
| 1035 | 1471 | |
| 1472 | + $use_comp_subquery = false; | |
| 1473 | + | |
| 1036 | 1474 | $limit = ''; |
| 1037 | 1475 | if ( $params && is_array( $params ) ) { |
| 1038 | 1476 | $s = isset( $params['s'] ) ? $params['s'] : ''; |
| 1039 | 1477 | $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array(); |
| @@ -1041,10 +1479,19 @@ | ||
| 1041 | 1479 | $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array(); |
| 1042 | 1480 | $page = isset( $params['page'] ) ? intval( $params['page'] ) : false; |
| 1043 | 1481 | $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false; |
| 1044 | 1482 | |
| 1483 | + // This parameter is used to enable caching in certain cases. | |
| 1484 | + $_included_cache_ids = isset( $params['_included_cache_ids'] ) ? wp_parse_id_list( $params['_included_cache_ids'] ) : array(); | |
| 1485 | + | |
| 1045 | 1486 | if ( ! empty( $s ) ) { |
| 1046 | - $where .= ' AND ( wp.id LIKE "%' . $this->escape( $s ) . '%" OR wp.name LIKE "%' . $this->escape( $s ) . '%" OR wp.url LIKE "%' . $this->escape( $s ) . '%" ) '; | |
| 1487 | + $s = trim( $s ); | |
| 1488 | + // Note: This SQL is executed via m_query() which bypasses wpdb, so we can't | |
| 1489 | + // use wpdb->prepare() (its placeholders won't be resolved). Instead, escape | |
| 1490 | + // LIKE wildcards and the value manually. First escape LIKE special chars, | |
| 1491 | + // then SQL escape the result. | |
| 1492 | + $like_value = '%' . $this->escape( $this->wpdb->esc_like( $s ) ) . '%'; | |
| 1493 | + $where .= " AND ( wp.id LIKE '{$like_value}' OR wp.name LIKE '{$like_value}' OR wp.url LIKE '{$like_value}' ) "; | |
| 1047 | 1494 | } |
| 1048 | 1495 | |
| 1049 | 1496 | if ( ! empty( $exclude ) ) { |
| 1050 | 1497 | $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') '; |
| @@ -1053,14 +1500,19 @@ | ||
| 1053 | 1500 | if ( ! empty( $include ) ) { |
| 1054 | 1501 | $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') '; |
| 1055 | 1502 | } |
| 1056 | 1503 | |
| 1504 | + if ( ! empty( $_included_cache_ids ) ) { | |
| 1505 | + $where .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') '; | |
| 1506 | + } | |
| 1507 | + | |
| 1057 | 1508 | // any, connected, disconnected, suspended, available_update. |
| 1058 | 1509 | if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) { |
| 1059 | 1510 | $status_conds = array(); |
| 1060 | 1511 | if ( in_array( 'available_update', $status ) ) { |
| 1061 | 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 <> '[]' ) "; |
| 1062 | - $results = $this->wpdb->get_results( 'SELECT wpid FROM ' . $this->table_name( 'wp_options' ) . " WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]' " ); | |
| 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 <> '[]'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the rest of the query is a literal. | |
| 1063 | 1515 | if ( $results ) { |
| 1064 | 1516 | $wp_ids = array(); |
| 1065 | 1517 | foreach ( $results as $item ) { |
| 1066 | 1518 | if ( ! empty( $item->wpid ) ) { |
| @@ -1075,9 +1527,9 @@ | ||
| 1075 | 1527 | $status_conds[] = ' ( ' . $available_sql . ') '; |
| 1076 | 1528 | } |
| 1077 | 1529 | |
| 1078 | 1530 | if ( in_array( 'connected', $status ) ) { |
| 1079 | - $status_conds[] = ' ( wp_sync.sync_errors == "" ) '; | |
| 1531 | + $status_conds[] = ' ( wp_sync.sync_errors = "" ) '; | |
| 1080 | 1532 | } |
| 1081 | 1533 | if ( in_array( 'disconnected', $status ) ) { |
| 1082 | 1534 | $status_conds[] = " wp_sync.sync_errors <> '' "; |
| 1083 | 1535 | } |
| @@ -1088,13 +1540,18 @@ | ||
| 1088 | 1540 | |
| 1089 | 1541 | if ( ! empty( $status_conds ) ) { |
| 1090 | 1542 | $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) '; |
| 1091 | 1543 | } |
| 1544 | + | |
| 1545 | + if ( in_array( 'unsuspended', $status ) && ! in_array( 'suspended', $status ) ) { // to sure not conflict the suspended status. | |
| 1546 | + $where .= ' AND wp.suspended = 0 '; | |
| 1547 | + } | |
| 1092 | 1548 | } |
| 1093 | 1549 | |
| 1094 | 1550 | if ( ! empty( $page ) && ! empty( $per_page ) ) { |
| 1095 | 1551 | $limit = ( $page - 1 ) * $per_page . ',' . $per_page; |
| 1096 | 1552 | } |
| 1553 | + $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false; | |
| 1097 | 1554 | } |
| 1098 | 1555 | |
| 1099 | 1556 | if ( 'wp.url' === $orderBy ) { |
| 1100 | 1557 | $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')"; |
| @@ -1099,11 +1556,26 @@ | ||
| 1099 | 1556 | if ( 'wp.url' === $orderBy ) { |
| 1100 | 1557 | $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')"; |
| 1101 | 1558 | } |
| 1102 | 1559 | |
| 1560 | + $view_selects = ''; | |
| 1561 | + $view_joins = ''; | |
| 1562 | + | |
| 1563 | + if ( $use_comp_subquery ) { | |
| 1564 | + $view_selects = ',wp_optionview.* '; | |
| 1565 | + $view_joins = ' JOIN ' . $this->get_wp_options_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid '; | |
| 1566 | + } else { | |
| 1567 | + $opts_view = $this->get_wp_options_join( $extra_view ); | |
| 1568 | + | |
| 1569 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 1570 | + $view_selects = ',' . $opts_view['selects']; | |
| 1571 | + $view_joins = $opts_view['joins']; | |
| 1572 | + } | |
| 1573 | + } | |
| 1574 | + | |
| 1103 | 1575 | // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax. |
| 1104 | 1576 | if ( $selectgroups ) { |
| 1105 | - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, | |
| 1577 | + $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, | |
| 1106 | 1578 | wpclient.name as client_name |
| 1107 | 1579 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 1108 | 1580 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 1109 | 1581 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| @@ -1108,18 +1580,18 @@ | ||
| 1108 | 1580 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 1109 | 1581 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 1110 | 1582 | LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id |
| 1111 | 1583 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1112 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 1584 | + ' . $view_joins . ' | |
| 1113 | 1585 | WHERE 1 ' . $where . $connected_sql . ' |
| 1114 | 1586 | GROUP BY wp.id, wp_sync.sync_id |
| 1115 | 1587 | ORDER BY ' . $orderBy; |
| 1116 | 1588 | } else { |
| 1117 | - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.*, wpclient.name as client_name | |
| 1589 | + $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ', wpclient.name as client_name | |
| 1118 | 1590 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 1119 | 1591 | LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id |
| 1120 | 1592 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1121 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 1593 | + ' . $view_joins . ' | |
| 1122 | 1594 | WHERE 1 ' . $where . $connected_sql . ' |
| 1123 | 1595 | GROUP BY wp.id, wp_sync.sync_id |
| 1124 | 1596 | ORDER BY ' . $orderBy; |
| 1125 | 1597 | } |
| @@ -1140,13 +1612,17 @@ | ||
| 1140 | 1612 | $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites ); |
| 1141 | 1613 | } |
| 1142 | 1614 | } |
| 1143 | 1615 | |
| 1616 | + if ( ! empty( $_included_cache_ids ) ) { | |
| 1617 | + MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql websites=%s]', $qry ) ); | |
| 1618 | + } | |
| 1619 | + MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql websites=%s]', $qry ) ); | |
| 1620 | + | |
| 1144 | 1621 | return $qry; |
| 1145 | 1622 | } |
| 1146 | 1623 | |
| 1147 | 1624 | |
| 1148 | - | |
| 1149 | 1625 | /** |
| 1150 | 1626 | * Get SQL to get wp child sites for current user. |
| 1151 | 1627 | * |
| 1152 | 1628 | * @since 4.3 |
| @@ -1159,17 +1635,18 @@ | ||
| 1159 | 1635 | if ( ! is_array( $params ) ) { |
| 1160 | 1636 | $params = array(); |
| 1161 | 1637 | } |
| 1162 | 1638 | |
| 1163 | - $selectgroups = ! empty( $params['select_groups'] ) ? true : false; | |
| 1164 | - $search_site = isset( $params['search_site'] ) && ! empty( $params['search_site'] ) ? $params['search_site'] : null; | |
| 1165 | - $orderBy = isset( $params['order_by'] ) && ! empty( $params['order_by'] ) ? $params['order_by'] : 'wp.url'; | |
| 1166 | - $offset = isset( $params['offset'] ) ? $params['offset'] : false; | |
| 1167 | - $rowcount = isset( $params['row_count'] ) ? $params['row_count'] : false; | |
| 1168 | - $for_manager = isset( $params['for_manager'] ) ? $params['for_manager'] : false; | |
| 1169 | - $extraWhere = isset( $params['extra_where'] ) && ! empty( $params['extra_where'] ) ? $params['extra_where'] : null; | |
| 1170 | - $extra_view = isset( $params['extra_view'] ) && is_array( $params['extra_view'] ) && ! empty( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' ); | |
| 1171 | - $extra_join = isset( $params['extra_join'] ) ? $params['extra_join'] : ''; | |
| 1639 | + $selectgroups = ! empty( $params['select_groups'] ) ? true : false; | |
| 1640 | + $search_site = isset( $params['search_site'] ) && ! empty( $params['search_site'] ) ? $params['search_site'] : null; | |
| 1641 | + $orderBy = isset( $params['order_by'] ) && ! empty( $params['order_by'] ) ? $params['order_by'] : 'wp.url'; | |
| 1642 | + $offset = isset( $params['offset'] ) ? $params['offset'] : false; | |
| 1643 | + $rowcount = isset( $params['row_count'] ) ? $params['row_count'] : false; | |
| 1644 | + $for_manager = isset( $params['for_manager'] ) ? $params['for_manager'] : false; | |
| 1645 | + $extraWhere = isset( $params['extra_where'] ) && ! empty( $params['extra_where'] ) ? $params['extra_where'] : null; | |
| 1646 | + $extra_view = isset( $params['extra_view'] ) && is_array( $params['extra_view'] ) && ! empty( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' ); | |
| 1647 | + $extra_join = isset( $params['extra_join'] ) ? $params['extra_join'] : ''; | |
| 1648 | + $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false; | |
| 1172 | 1649 | |
| 1173 | 1650 | $extra_select_wp_fields = isset( $params['extra_select_wp_fields'] ) && is_array( $params['extra_select_wp_fields'] ) && ! empty( $params['extra_select_wp_fields'] ) ? $params['extra_select_wp_fields'] : array(); |
| 1174 | 1651 | $extra_select_sql_fields = isset( $params['extra_select_sql_fields'] ) && ! empty( $params['extra_select_sql_fields'] ) ? $params['extra_select_sql_fields'] : ''; |
| 1175 | 1652 | |
| @@ -1179,9 +1656,15 @@ | ||
| 1179 | 1656 | $where = ''; |
| 1180 | 1657 | |
| 1181 | 1658 | if ( null !== $search_site ) { |
| 1182 | 1659 | $search_site = trim( $search_site ); |
| 1183 | - $where .= ' AND (wp.name LIKE "%' . $this->escape( $search_site ) . '%" OR wp.url LIKE "%' . $this->escape( $search_site ) . '%") '; | |
| 1660 | + // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety. | |
| 1661 | + $like_pattern = '%' . $this->wpdb->esc_like( $search_site ) . '%'; | |
| 1662 | + $where .= $this->wpdb->prepare( | |
| 1663 | + ' AND (wp.name LIKE %s OR wp.url LIKE %s) ', | |
| 1664 | + $like_pattern, | |
| 1665 | + $like_pattern | |
| 1666 | + ); | |
| 1184 | 1667 | } |
| 1185 | 1668 | |
| 1186 | 1669 | if ( null !== $extraWhere ) { |
| 1187 | 1670 | $where .= ' AND ' . $extraWhere; |
| @@ -1200,8 +1683,22 @@ | ||
| 1200 | 1683 | if ( ! empty( $extra_select_sql_fields ) ) { |
| 1201 | 1684 | $extra_select_sql_fields = ',' . $extra_select_sql_fields; |
| 1202 | 1685 | } |
| 1203 | 1686 | |
| 1687 | + $view_selects = ''; | |
| 1688 | + $view_joins = ''; | |
| 1689 | + | |
| 1690 | + if ( $use_comp_subquery ) { | |
| 1691 | + $view_selects = ',wp_optionview.* '; | |
| 1692 | + $view_joins = ' JOIN ' . $this->get_wp_options_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid '; | |
| 1693 | + } else { | |
| 1694 | + $opts_view = $this->get_wp_options_join( $extra_view ); | |
| 1695 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 1696 | + $view_selects = ',' . $opts_view['selects']; | |
| 1697 | + $view_joins = $opts_view['joins']; | |
| 1698 | + } | |
| 1699 | + } | |
| 1700 | + | |
| 1204 | 1701 | // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax. |
| 1205 | 1702 | if ( $selectgroups ) { |
| 1206 | 1703 | if ( $count_only ) { |
| 1207 | 1704 | $select = ' COUNT(DISTINCT(wp.id)) '; |
| @@ -1207,9 +1704,9 @@ | ||
| 1207 | 1704 | $select = ' COUNT(DISTINCT(wp.id)) '; |
| 1208 | 1705 | } else { |
| 1209 | 1706 | $select = $select_wp_fields . ' |
| 1210 | 1707 | ' . $extra_select_sql_fields . ' |
| 1211 | - ,wp_sync.sync_errors,wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, wpclient.name as client_name '; | |
| 1708 | + ,wp_sync.sync_errors' . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, wpclient.name as client_name '; | |
| 1212 | 1709 | } |
| 1213 | 1710 | $qry = 'SELECT ' . $select . ' |
| 1214 | 1711 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 1215 | 1712 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| @@ -1215,10 +1712,10 @@ | ||
| 1215 | 1712 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 1216 | 1713 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 1217 | 1714 | LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id |
| 1218 | 1715 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1219 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid ' . | |
| 1220 | - $extra_join . ' | |
| 1716 | + ' . $view_joins . ' | |
| 1717 | + ' . $extra_join . ' | |
| 1221 | 1718 | WHERE 1 ' . $where; |
| 1222 | 1719 | if ( ! $count_only ) { |
| 1223 | 1720 | $qry .= ' GROUP BY wp.id, wp_sync.sync_id'; |
| 1224 | 1721 | } |
| @@ -1228,16 +1725,16 @@ | ||
| 1228 | 1725 | $select = ' COUNT(DISTINCT(wp.id)) '; |
| 1229 | 1726 | } else { |
| 1230 | 1727 | $select = $select_wp_fields . ' |
| 1231 | 1728 | ' . $extra_select_sql_fields . ' |
| 1232 | - ,wp_sync.sync_errors,wp_optionview.*, wpclient.name as client_name '; | |
| 1729 | + ,wp_sync.sync_errors' . $view_selects . ', wpclient.name as client_name '; | |
| 1233 | 1730 | } |
| 1234 | 1731 | $qry = 'SELECT ' . $select . ' |
| 1235 | 1732 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 1236 | 1733 | LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id |
| 1237 | 1734 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1238 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid ' . | |
| 1239 | - $extra_join . ' | |
| 1735 | + ' . $view_joins . ' | |
| 1736 | + ' . $extra_join . ' | |
| 1240 | 1737 | WHERE 1 ' . $where; |
| 1241 | 1738 | if ( ! $count_only ) { |
| 1242 | 1739 | $qry .= ' GROUP BY wp.id, wp_sync.sync_id'; |
| 1243 | 1740 | } |
| @@ -1348,9 +1845,9 @@ | ||
| 1348 | 1845 | $format = isset( $params['format'] ) ? $params['format'] : ''; |
| 1349 | 1846 | $clients = isset( $params['client'] ) ? $params['client'] : ''; |
| 1350 | 1847 | $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array(); |
| 1351 | 1848 | |
| 1352 | - $for_manager = false; | |
| 1849 | + $for_manager = isset( $params['no_perm_check'] ) && true === $params['no_perm_check']; | |
| 1353 | 1850 | |
| 1354 | 1851 | $urlsWhere = ''; |
| 1355 | 1852 | |
| 1356 | 1853 | if ( isset( $params['urls'] ) && ! empty( $params['urls'] ) ) { |
| @@ -1465,9 +1962,12 @@ | ||
| 1465 | 1962 | $data = array_unique( array_merge( $fields, $data ) ); // to prevent difference fields name. |
| 1466 | 1963 | } |
| 1467 | 1964 | |
| 1468 | 1965 | $dbwebsites = array(); |
| 1469 | - $websites = $this->query( $this->get_sql_websites_for_current_user( $selectgroups, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager, $extra_view, $is_staging, $args ) ); | |
| 1966 | + | |
| 1967 | + $sql = $this->get_sql_websites_for_current_user( $selectgroups, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager, $extra_view, $is_staging, $args ); | |
| 1968 | + $websites = $this->query( $sql ); | |
| 1969 | + | |
| 1470 | 1970 | while ( $websites && ( $website = static::fetch_object( $websites ) ) ) { |
| 1471 | 1971 | |
| 1472 | 1972 | $obj_data = MainWP_Utility::map_site( $website, $data ); |
| 1473 | 1973 | |
| @@ -1472,9 +1972,12 @@ | ||
| 1472 | 1972 | $obj_data = MainWP_Utility::map_site( $website, $data ); |
| 1473 | 1973 | |
| 1474 | 1974 | if ( $full_data ) { |
| 1475 | 1975 | $sum_upgrades = 0; |
| 1476 | - 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 ) ) { | |
| 1477 | 1980 | $plugin_upgrades = json_decode( $obj_data->plugin_upgrades, true ); |
| 1478 | 1981 | if ( is_array( $plugin_upgrades ) ) { |
| 1479 | 1982 | $sum_upgrades += count( $plugin_upgrades ); |
| 1480 | 1983 | } |
| @@ -1479,9 +1982,9 @@ | ||
| 1479 | 1982 | $sum_upgrades += count( $plugin_upgrades ); |
| 1480 | 1983 | } |
| 1481 | 1984 | } |
| 1482 | 1985 | |
| 1483 | - if ( '' !== $obj_data->theme_upgrades ) { | |
| 1986 | + if ( ! empty( $obj_data->theme_upgrades ) ) { | |
| 1484 | 1987 | $theme_upgrades = json_decode( $obj_data->theme_upgrades, true ); |
| 1485 | 1988 | if ( is_array( $theme_upgrades ) ) { |
| 1486 | 1989 | $sum_upgrades += count( $theme_upgrades ); |
| 1487 | 1990 | } |
| @@ -1486,9 +1989,9 @@ | ||
| 1486 | 1989 | $sum_upgrades += count( $theme_upgrades ); |
| 1487 | 1990 | } |
| 1488 | 1991 | } |
| 1489 | 1992 | |
| 1490 | - if ( '' !== $obj_data->wp_upgrades ) { | |
| 1993 | + if ( ! empty( $obj_data->wp_upgrades ) ) { | |
| 1491 | 1994 | $wp_upgrades = json_decode( $obj_data->wp_upgrades, true ); |
| 1492 | 1995 | if ( is_array( $wp_upgrades ) ) { |
| 1493 | 1996 | $sum_upgrades += count( $wp_upgrades ); |
| 1494 | 1997 | } |
| @@ -1506,8 +2009,126 @@ | ||
| 1506 | 2009 | return $dbwebsites; |
| 1507 | 2010 | } |
| 1508 | 2011 | |
| 1509 | 2012 | /** |
| 2013 | + * Get count of child sites for the current user with filters. | |
| 2014 | + * | |
| 2015 | + * This method is optimized for the Abilities API to return site counts | |
| 2016 | + * with filtering support for status, tags (groups), and client_id. | |
| 2017 | + * | |
| 2018 | + * IMPORTANT: This method is intended ONLY for Abilities API consumers. | |
| 2019 | + * For legacy UI code paths (e.g., admin dashboard widgets, site count displays), | |
| 2020 | + * use get_websites_count() instead. The two methods serve different purposes: | |
| 2021 | + * - get_websites_count(): Simple total count for UI display (cached, no filters) | |
| 2022 | + * - get_websites_count_for_current_user(): Filtered count for API pagination | |
| 2023 | + * | |
| 2024 | + * @since 5.3 | |
| 2025 | + * | |
| 2026 | + * @param array $params Filter parameters: | |
| 2027 | + * - status (string): 'connected', 'disconnected', 'suspended' | |
| 2028 | + * - tags (array): Array of tag/group IDs to filter by | |
| 2029 | + * - client_id (int): Client ID to filter by. | |
| 2030 | + * | |
| 2031 | + * @return int Count of sites matching the filters. | |
| 2032 | + */ | |
| 2033 | + public function get_websites_count_for_current_user( $params = array() ) { //phpcs:ignore -- NOSONAR - complex. | |
| 2034 | + | |
| 2035 | + if ( ! is_array( $params ) ) { | |
| 2036 | + $params = array(); | |
| 2037 | + } | |
| 2038 | + | |
| 2039 | + $status = isset( $params['status'] ) ? $params['status'] : ''; | |
| 2040 | + $tags = isset( $params['tags'] ) && is_array( $params['tags'] ) ? $params['tags'] : array(); | |
| 2041 | + $client_id = isset( $params['client_id'] ) ? intval( $params['client_id'] ) : 0; | |
| 2042 | + $s = isset( $params['s'] ) ? $params['s'] : ''; | |
| 2043 | + | |
| 2044 | + // Validate status value (defense-in-depth for direct callers outside Abilities API). | |
| 2045 | + $valid_statuses = array( 'connected', 'disconnected', 'suspended', '' ); | |
| 2046 | + if ( ! in_array( $status, $valid_statuses, true ) ) { | |
| 2047 | + $status = ''; | |
| 2048 | + } | |
| 2049 | + | |
| 2050 | + $where = ''; | |
| 2051 | + $sql_params = array(); | |
| 2052 | + | |
| 2053 | + // Multi-user support: filter by current user. | |
| 2054 | + if ( MainWP_System::instance()->is_multi_user() ) { | |
| 2055 | + global $current_user; | |
| 2056 | + $where .= ' AND wp.userid = %d '; | |
| 2057 | + $sql_params[] = (int) $current_user->ID; | |
| 2058 | + } | |
| 2059 | + | |
| 2060 | + // Access control for sites. | |
| 2061 | + $where .= $this->get_sql_where_allow_access_sites( 'wp', 'no' ); | |
| 2062 | + | |
| 2063 | + // Status filtering: connected, disconnected, suspended. | |
| 2064 | + if ( ! empty( $status ) ) { | |
| 2065 | + switch ( $status ) { | |
| 2066 | + case 'connected': | |
| 2067 | + $where .= ' AND wp_sync.sync_errors = "" AND wp.suspended = 0 '; | |
| 2068 | + break; | |
| 2069 | + case 'disconnected': | |
| 2070 | + $where .= ' AND wp_sync.sync_errors <> "" '; | |
| 2071 | + break; | |
| 2072 | + case 'suspended': | |
| 2073 | + $where .= ' AND wp.suspended = 1 '; | |
| 2074 | + break; | |
| 2075 | + default: | |
| 2076 | + // No additional filtering. | |
| 2077 | + break; | |
| 2078 | + } | |
| 2079 | + } | |
| 2080 | + | |
| 2081 | + // Client ID filtering. | |
| 2082 | + if ( ! empty( $client_id ) ) { | |
| 2083 | + $where .= ' AND wp.client_id = %d '; | |
| 2084 | + $sql_params[] = (int) $client_id; | |
| 2085 | + } | |
| 2086 | + | |
| 2087 | + // Search filtering. | |
| 2088 | + if ( ! empty( $s ) ) { | |
| 2089 | + $s = trim( $s ); | |
| 2090 | + $like_pattern = '%' . $this->wpdb->esc_like( $s ) . '%'; | |
| 2091 | + $where .= $this->wpdb->prepare( | |
| 2092 | + ' AND ( wp.id LIKE %s OR wp.name LIKE %s OR wp.url LIKE %s ) ', | |
| 2093 | + $like_pattern, | |
| 2094 | + $like_pattern, | |
| 2095 | + $like_pattern | |
| 2096 | + ); | |
| 2097 | + } | |
| 2098 | + | |
| 2099 | + // Tags (groups) filtering. | |
| 2100 | + $join_group = ''; | |
| 2101 | + if ( ! empty( $tags ) ) { | |
| 2102 | + $tags = array_map( 'intval', $tags ); | |
| 2103 | + $tags = array_filter( | |
| 2104 | + $tags, | |
| 2105 | + function ( $id ) { | |
| 2106 | + return $id > 0; | |
| 2107 | + } | |
| 2108 | + ); | |
| 2109 | + if ( ! empty( $tags ) ) { | |
| 2110 | + $join_group = ' JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid '; | |
| 2111 | + $placeholders = implode( ', ', array_fill( 0, count( $tags ), '%d' ) ); | |
| 2112 | + $where .= " AND wpgroup.groupid IN ( $placeholders ) "; | |
| 2113 | + $sql_params = array_merge( $sql_params, $tags ); | |
| 2114 | + } | |
| 2115 | + } | |
| 2116 | + | |
| 2117 | + $qry = 'SELECT COUNT(DISTINCT wp.id) FROM ' . $this->table_name( 'wp' ) . ' wp ' . | |
| 2118 | + 'JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid ' . | |
| 2119 | + $join_group . | |
| 2120 | + 'WHERE 1 ' . $where; | |
| 2121 | + | |
| 2122 | + // Only call prepare() when we have placeholders. | |
| 2123 | + $result = $sql_params | |
| 2124 | + ? $this->wpdb->get_var( $this->wpdb->prepare( $qry, $sql_params ) ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- We have already prepared the query with the parameters. | |
| 2125 | + : $this->wpdb->get_var( $qry ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- We have already prepared the query with the parameters. | |
| 2126 | + | |
| 2127 | + return (int) $result; | |
| 2128 | + } | |
| 2129 | + | |
| 2130 | + /** | |
| 1510 | 2131 | * Get the child sites the current user has searched for. |
| 1511 | 2132 | * |
| 1512 | 2133 | * @param array $params Query parameters. |
| 1513 | 2134 | * |
| @@ -1520,11 +2141,11 @@ | ||
| 1520 | 2141 | if ( ! is_array( $params ) ) { |
| 1521 | 2142 | $params = array(); |
| 1522 | 2143 | } |
| 1523 | 2144 | |
| 1524 | - $view = isset( $params['view'] ) ? $params['view'] : 'default'; // must be default to compatible with get_option_view(). | |
| 2145 | + $view = isset( $params['view'] ) ? $params['view'] : 'default'; // must be default. | |
| 1525 | 2146 | $selectgroups = isset( $params['selectgroups'] ) && $params['selectgroups'] ? true : false; |
| 1526 | - $search_site = isset( $params['search'] ) ? $this->escape( trim( $params['search'] ) ) : null; | |
| 2147 | + $search_site = isset( $params['search'] ) ? trim( $params['search'] ) : null; | |
| 1527 | 2148 | $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url'; |
| 1528 | 2149 | $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false; |
| 1529 | 2150 | $rowcount = isset( $params['rowcount'] ) ? intval( $params['rowcount'] ) : false; |
| 1530 | 2151 | $extraWhere = isset( $params['extra_where'] ) ? $params['extra_where'] : null; // without AND prefix. |
| @@ -1533,11 +2154,16 @@ | ||
| 1533 | 2154 | $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no'; |
| 1534 | 2155 | $is_count = isset( $params['count_only'] ) && $params['count_only'] ? true : false; |
| 1535 | 2156 | $group_ids = isset( $params['group_id'] ) && ! empty( $params['group_id'] ) ? $params['group_id'] : array(); |
| 1536 | 2157 | $client_ids = isset( $params['client_id'] ) && ! empty( $params['client_id'] ) ? $params['client_id'] : array(); |
| 2158 | + $group_logic = ( isset( $params['group_logic'] ) && 'and' === $params['group_logic'] ) ? 'and' : 'or'; | |
| 1537 | 2159 | $is_not = isset( $params['isnot'] ) && ! empty( $params['isnot'] ) ? true : false; |
| 1538 | 2160 | $selected_sites = isset( $params['selected_sites'] ) ? $params['selected_sites'] : array(); |
| 1539 | 2161 | |
| 2162 | + // This parameter is used to enable caching in certain cases. | |
| 2163 | + $_included_cache_ids = isset( $params['_included_cache_ids'] ) ? wp_parse_id_list( $params['_included_cache_ids'] ) : array(); | |
| 2164 | + $where_cache_ids = ''; | |
| 2165 | + | |
| 1540 | 2166 | if ( ! is_array( $group_ids ) ) { |
| 1541 | 2167 | $group_ids = array(); |
| 1542 | 2168 | } |
| 1543 | 2169 | |
| @@ -1595,19 +2221,46 @@ | ||
| 1595 | 2221 | |
| 1596 | 2222 | $where .= ' AND wp.userid = ' . $current_user->ID . ' '; |
| 1597 | 2223 | } |
| 1598 | 2224 | |
| 1599 | - if ( ! empty( $selected_sites ) ) { | |
| 1600 | - $where .= ' AND wp.id IN (' . implode( ',', $selected_sites ) . ') '; | |
| 2225 | + if ( ! empty( $_included_cache_ids ) ) { | |
| 2226 | + $where_cache_ids .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') '; | |
| 2227 | + } else { | |
| 2228 | + if ( ! empty( $selected_sites ) ) { | |
| 2229 | + $where .= ' AND wp.id IN (' . implode( ',', $selected_sites ) . ') '; | |
| 2230 | + } | |
| 2231 | + | |
| 2232 | + if ( ! empty( $extraWhere ) ) { | |
| 2233 | + $where .= ' AND ' . $extraWhere; | |
| 2234 | + } | |
| 1601 | 2235 | } |
| 1602 | 2236 | |
| 1603 | - // for searching. | |
| 2237 | + // Search filtering. | |
| 1604 | 2238 | if ( null !== $search_site && '' !== $search_site ) { |
| 1605 | - $where .= ' AND (wp.name LIKE "%' . $search_site . '%" OR wp.url LIKE "%' . $search_site . '%") '; | |
| 2239 | + // Escape LIKE wildcards first (%, _, \). | |
| 2240 | + // Note: Cannot use WordPress escaping functions (esc_like, esc_sql, prepare) because | |
| 2241 | + // WordPress 6.2+ creates placeholder tokens that are only substituted during query execution. | |
| 2242 | + // Since this method returns a SQL string for later execution, tokens would never be substituted. | |
| 2243 | + // We use direct mysqli_real_escape_string() which is the same underlying function WordPress uses. | |
| 2244 | + $search_escaped = str_replace( array( '\\', '%', '_' ), array( '\\\\', '\\%', '\\_' ), $search_site ); | |
| 2245 | + $like_pattern = '%' . $search_escaped . '%'; | |
| 2246 | + | |
| 2247 | + // SQL escape using direct mysqli function to bypass WordPress placeholder tokens. | |
| 2248 | + if ( $this->wpdb->dbh instanceof \mysqli ) { | |
| 2249 | + $like_pattern_escaped = mysqli_real_escape_string( $this->wpdb->dbh, $like_pattern ); | |
| 2250 | + } else { | |
| 2251 | + // Fallback: reject if no mysqli connection available. | |
| 2252 | + $like_pattern_escaped = ''; | |
| 2253 | + } | |
| 2254 | + | |
| 2255 | + if ( '' !== $like_pattern_escaped ) { | |
| 2256 | + $where .= " AND (wp.name LIKE '" . $like_pattern_escaped . "' OR wp.url LIKE '" . $like_pattern_escaped . "') "; | |
| 2257 | + } | |
| 1606 | 2258 | } |
| 1607 | 2259 | |
| 1608 | - if ( null !== $extraWhere ) { | |
| 1609 | - $where .= ' AND ' . $extraWhere; | |
| 2260 | + $staging_enabled = is_plugin_active( 'mainwp-staging-extension/mainwp-staging-extension.php' ) || is_plugin_active( 'mainwp-timecapsule-extension/mainwp-timecapsule-extension.php' ); | |
| 2261 | + if ( ! $staging_enabled ) { | |
| 2262 | + $is_staging = 'no'; | |
| 1610 | 2263 | } |
| 1611 | 2264 | |
| 1612 | 2265 | if ( ! $for_manager ) { |
| 1613 | 2266 | $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging ); |
| @@ -1622,10 +2275,11 @@ | ||
| 1622 | 2275 | if ( ! empty( $orderBy ) ) { |
| 1623 | 2276 | $orderBy = ' ORDER BY ' . $orderBy; |
| 1624 | 2277 | } |
| 1625 | 2278 | |
| 1626 | - $join_group = ''; | |
| 1627 | - $where_group = ''; | |
| 2279 | + $join_group = ''; | |
| 2280 | + $where_group = ''; | |
| 2281 | + $having_group = ''; | |
| 1628 | 2282 | |
| 1629 | 2283 | if ( in_array( 'nogroups', $group_ids ) ) { |
| 1630 | 2284 | $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid '; |
| 1631 | 2285 | $group_ids = array_filter( |
| @@ -1634,14 +2288,21 @@ | ||
| 1634 | 2288 | return 'nogroups' !== $e; |
| 1635 | 2289 | } |
| 1636 | 2290 | ); |
| 1637 | 2291 | if ( ! empty( $group_ids ) ) { |
| 1638 | - $groups = implode( ',', $group_ids ); | |
| 2292 | + $groups = implode( ',', $group_ids ); | |
| 2293 | + $groups_count = count( $group_ids ); | |
| 1639 | 2294 | if ( $is_not ) { |
| 1640 | - $where_group = ' AND wpgroup.groupid IS NOT NULL AND wpgroup.groupid NOT IN (' . $groups . ') '; | |
| 1641 | - // to fix. | |
| 1642 | - $sub_select_is_not = ' SELECT wp.id FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid WHERE wpgroup.groupid IN (' . $groups . ') '; | |
| 1643 | - $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_is_not . ' ) '; | |
| 2295 | + $where_group = ' AND wpgroup.groupid IS NOT NULL '; | |
| 2296 | + if ( 'and' === $group_logic ) { | |
| 2297 | + $sub_select_match_all = ' SELECT wpand.id FROM ' . $this->table_name( 'wp' ) . ' wpand JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup_and ON wpand.id = wpgroup_and.wpid WHERE wpgroup_and.groupid IN (' . $groups . ') GROUP BY wpand.id HAVING COUNT(DISTINCT wpgroup_and.groupid) = ' . $groups_count . ' '; | |
| 2298 | + $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_match_all . ' ) '; | |
| 2299 | + } else { | |
| 2300 | + $sub_select_is_not = ' SELECT wp_or.id FROM ' . $this->table_name( 'wp' ) . ' wp_or JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup_or ON wp_or.id = wpgroup_or.wpid WHERE wpgroup_or.groupid IN (' . $groups . ') '; | |
| 2301 | + $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_is_not . ' ) '; | |
| 2302 | + } | |
| 2303 | + } elseif ( 'and' === $group_logic ) { | |
| 2304 | + $where_group = ' AND 1 = 0 '; | |
| 1644 | 2305 | } else { |
| 1645 | 2306 | $where_group = ' AND ( wpgroup.groupid IS NULL OR wpgroup.groupid IN (' . $groups . ') ) '; |
| 1646 | 2307 | } |
| 1647 | 2308 | } elseif ( $is_not ) { |
| @@ -1649,18 +2310,26 @@ | ||
| 1649 | 2310 | } else { |
| 1650 | 2311 | $where_group = ' AND wpgroup.groupid IS NULL '; |
| 1651 | 2312 | } |
| 1652 | 2313 | } elseif ( $group_ids ) { |
| 1653 | - $groups = implode( ',', $group_ids ); | |
| 2314 | + $groups = implode( ',', $group_ids ); | |
| 2315 | + $groups_count = count( $group_ids ); | |
| 1654 | 2316 | if ( $is_not ) { |
| 1655 | 2317 | $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid '; |
| 1656 | - $where_group = ' AND ( wpgroup.groupid NOT IN (' . $groups . ') OR wpgroup.groupid IS NULL ) '; | |
| 1657 | - // to fix. | |
| 1658 | - $sub_select_is_not = ' SELECT wp.id FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid WHERE wpgroup.groupid IN (' . $groups . ') '; | |
| 1659 | - $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_is_not . ' ) '; | |
| 2318 | + $where_group = ''; | |
| 2319 | + if ( 'and' === $group_logic ) { | |
| 2320 | + $sub_select_match_all = ' SELECT wpand.id FROM ' . $this->table_name( 'wp' ) . ' wpand JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup_and ON wpand.id = wpgroup_and.wpid WHERE wpgroup_and.groupid IN (' . $groups . ') GROUP BY wpand.id HAVING COUNT(DISTINCT wpgroup_and.groupid) = ' . $groups_count . ' '; | |
| 2321 | + $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_match_all . ' ) '; | |
| 2322 | + } else { | |
| 2323 | + $sub_select_is_not = ' SELECT wp_or.id FROM ' . $this->table_name( 'wp' ) . ' wp_or JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup_or ON wp_or.id = wpgroup_or.wpid WHERE wpgroup_or.groupid IN (' . $groups . ') '; | |
| 2324 | + $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_is_not . ' ) '; | |
| 2325 | + } | |
| 1660 | 2326 | } else { |
| 1661 | 2327 | $join_group = ' JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid '; |
| 1662 | 2328 | $where_group = ' AND wpgroup.groupid IN (' . $groups . ') '; |
| 2329 | + if ( 'and' === $group_logic ) { | |
| 2330 | + $having_group = 'COUNT(DISTINCT wpgroup.groupid) = ' . $groups_count; | |
| 2331 | + } | |
| 1663 | 2332 | } |
| 1664 | 2333 | } |
| 1665 | 2334 | |
| 1666 | 2335 | $select_groups_belong = ''; |
| @@ -1670,9 +2339,16 @@ | ||
| 1670 | 2339 | } |
| 1671 | 2340 | |
| 1672 | 2341 | $join_client = ''; |
| 1673 | 2342 | $where_client = ''; |
| 1674 | - | |
| 2343 | + $group_by = ' GROUP BY wp.id, wp_sync.sync_id'; | |
| 2344 | + if ( ! empty( $having_group ) ) { | |
| 2345 | + $group_by .= ' HAVING ' . $having_group; | |
| 2346 | + } | |
| 2347 | + $group_by = ' GROUP BY wp.id, wp_sync.sync_id'; | |
| 2348 | + if ( ! empty( $having_group ) ) { | |
| 2349 | + $group_by .= ' HAVING ' . $having_group; | |
| 2350 | + } | |
| 1675 | 2351 | if ( in_array( 'noclients', $client_ids ) ) { |
| 1676 | 2352 | $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id '; |
| 1677 | 2353 | $client_ids = array_filter( |
| 1678 | 2354 | $client_ids, |
| @@ -1720,9 +2396,8 @@ | ||
| 1720 | 2396 | 'wp.privkey', |
| 1721 | 2397 | 'wp.pubkey', |
| 1722 | 2398 | 'wp.wpe', |
| 1723 | 2399 | 'wp.is_staging', |
| 1724 | - 'wp.pubkey', | |
| 1725 | 2400 | 'wp.force_use_ipv4', |
| 1726 | 2401 | 'wp.siteurl', |
| 1727 | 2402 | 'wp.suspended', |
| 1728 | 2403 | 'wp.mainwpdir', |
| @@ -1736,9 +2411,8 @@ | ||
| 1736 | 2411 | |
| 1737 | 2412 | $legacy_status_fields = array( |
| 1738 | 2413 | 'wp.offline_check_result', // 1 - online, -1 offline. |
| 1739 | 2414 | 'wp.http_response_code', |
| 1740 | - 'wp.http_code_noticed', | |
| 1741 | 2415 | 'wp.offline_checks_last', |
| 1742 | 2416 | ); |
| 1743 | 2417 | |
| 1744 | 2418 | $light_fields = array_merge( $light_fields, $legacy_status_fields ); |
| @@ -1754,13 +2428,42 @@ | ||
| 1754 | 2428 | $select_fields = $light_fields; |
| 1755 | 2429 | } elseif ( 'monitor_view' === $view ) { |
| 1756 | 2430 | $select_fields = $light_fields; |
| 1757 | 2431 | $select_fields[] = 'mo.*'; |
| 1758 | - $join_monitors = ' LEFT JOIN ' . $this->table_name( 'monitors' ) . ' mo ON wp.id = mo.wpid AND mo.issub = 0 '; | |
| 2432 | + $join_monitors = 'LEFT JOIN ( | |
| 2433 | + SELECT m1.* | |
| 2434 | + FROM ' . $this->table_name( 'monitors' ) . ' m1 | |
| 2435 | + JOIN ( | |
| 2436 | + SELECT wpid, MAX(monitor_id) AS max_id | |
| 2437 | + FROM ' . $this->table_name( 'monitors' ) . ' | |
| 2438 | + WHERE issub = 0 | |
| 2439 | + GROUP BY wpid | |
| 2440 | + ) mm ON mm.wpid = m1.wpid AND m1.monitor_id = mm.max_id | |
| 2441 | + ) mo ON mo.wpid = wp.id '; | |
| 2442 | + } elseif ( 'manage_site' === $view ) { | |
| 2443 | + $select_fields[] = 'mo.monitor_id'; | |
| 2444 | + $join_monitors = ' LEFT JOIN ( | |
| 2445 | + SELECT wpid, MAX(monitor_id) AS monitor_id | |
| 2446 | + FROM ' . $this->table_name( 'monitors' ) . ' | |
| 2447 | + WHERE issub = 0 | |
| 2448 | + GROUP BY wpid | |
| 2449 | + ) AS mo | |
| 2450 | + ON mo.wpid = wp.id '; | |
| 2451 | + | |
| 1759 | 2452 | } |
| 1760 | 2453 | |
| 1761 | 2454 | $select = implode( ',', $select_fields ); |
| 1762 | 2455 | |
| 2456 | + $view_selects = ''; | |
| 2457 | + $view_joins = ''; | |
| 2458 | + | |
| 2459 | + $opts_view = $this->get_wp_options_join( $extra_view, $view ); | |
| 2460 | + | |
| 2461 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 2462 | + $view_selects = ',' . $opts_view['selects']; | |
| 2463 | + $view_joins = $opts_view['joins']; | |
| 2464 | + } | |
| 2465 | + | |
| 1763 | 2466 | // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax. |
| 1764 | 2467 | if ( $selectgroups ) { |
| 1765 | 2468 | |
| 1766 | 2469 | if ( empty( $join_group ) ) { |
| @@ -1766,9 +2469,9 @@ | ||
| 1766 | 2469 | if ( empty( $join_group ) ) { |
| 1767 | 2470 | $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid '; |
| 1768 | 2471 | } |
| 1769 | 2472 | |
| 1770 | - $qry = 'SELECT ' . $select . ', wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, wpclient.name as client_name ' . | |
| 2473 | + $qry = 'SELECT ' . $select . $view_selects . ', GROUP_CONCAT(DISTINCT gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(DISTINCT gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(DISTINCT gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors, wpclient.name as client_name ' . | |
| 1771 | 2474 | $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' . |
| 1772 | 2475 | $join_client . ' ' . |
| 1773 | 2476 | $join_group . |
| 1774 | 2477 | $join_monitors . ' |
| @@ -1774,34 +2477,36 @@ | ||
| 1774 | 2477 | $join_monitors . ' |
| 1775 | 2478 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgroup.groupid = gr.id |
| 1776 | 2479 | |
| 1777 | 2480 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1778 | - JOIN ' . $this->get_option_view( $extra_view, $view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 1779 | - WHERE 1 ' . $where . $where_group . $where_client . ' | |
| 1780 | - GROUP BY wp.id, wp_sync.sync_id ' . | |
| 2481 | + ' . $view_joins . ' | |
| 2482 | + WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by . | |
| 1781 | 2483 | $orderBy; |
| 1782 | 2484 | } else { |
| 1783 | - $qry = 'SELECT ' . $select . ', wp_optionview.*, wpclient.name as client_name ' . | |
| 2485 | + $qry = 'SELECT ' . $select . $view_selects . ', wpclient.name as client_name ' . | |
| 1784 | 2486 | $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' . |
| 1785 | 2487 | $join_group . ' ' . |
| 1786 | 2488 | $join_client . |
| 1787 | 2489 | $join_monitors . ' |
| 1788 | 2490 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1789 | - JOIN ' . $this->get_option_view( $extra_view, $view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 1790 | - WHERE 1 ' . $where . $where_group . $where_client . ' | |
| 1791 | - GROUP BY wp.id, wp_sync.sync_id ' . | |
| 2491 | + ' . $view_joins . ' | |
| 2492 | + WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by . | |
| 1792 | 2493 | $orderBy; |
| 1793 | 2494 | } |
| 1794 | 2495 | |
| 1795 | 2496 | if ( ( false !== $offset ) && ( false !== $rowcount ) ) { |
| 1796 | - $qry .= ' LIMIT ' . $offset . ', ' . $rowcount; | |
| 2497 | + // When cache IDs are provided, they already represent the page-specific subset, | |
| 2498 | + // so the effective offset within that subset is always 0. | |
| 2499 | + $effective_offset = ! empty( $_included_cache_ids ) ? 0 : $offset; | |
| 2500 | + $qry .= ' LIMIT ' . $effective_offset . ', ' . $rowcount; | |
| 1797 | 2501 | } elseif ( false !== $rowcount ) { |
| 1798 | 2502 | $qry .= ' LIMIT ' . $rowcount; |
| 1799 | 2503 | } |
| 1800 | 2504 | |
| 1801 | - if ( ! empty( $params['dev_log_query'] ) ) { | |
| 1802 | - error_log( $qry ); //phpcs:ignore -- NOSONAR - for dev. | |
| 2505 | + if ( ! empty( $_included_cache_ids ) ) { | |
| 2506 | + MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql search websites=%s]', $qry ) ); | |
| 1803 | 2507 | } |
| 2508 | + MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql search websites=%s]', $qry ) ); | |
| 1804 | 2509 | |
| 1805 | 2510 | return $qry; |
| 1806 | 2511 | } |
| 1807 | 2512 | |
| @@ -1985,24 +2690,41 @@ | ||
| 1985 | 2690 | $view_fields = array(); |
| 1986 | 2691 | } |
| 1987 | 2692 | |
| 1988 | 2693 | if ( MainWP_Utility::ctype_digit( $id ) ) { |
| 2694 | + | |
| 2695 | + $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false; | |
| 2696 | + | |
| 2697 | + $view_selects = ''; | |
| 2698 | + $view_joins = ''; | |
| 2699 | + | |
| 2700 | + if ( $use_comp_subquery ) { | |
| 2701 | + $view_selects = ',wp_optionview.* '; | |
| 2702 | + $view_joins = ' JOIN ' . $this->get_option_view_by( $view, $view_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid '; | |
| 2703 | + } else { | |
| 2704 | + $opts_view = $this->get_option_view_by_join( $view, $view_fields ); | |
| 2705 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 2706 | + $view_selects = ',' . $opts_view['selects']; | |
| 2707 | + $view_joins = $opts_view['joins']; | |
| 2708 | + } | |
| 2709 | + } | |
| 2710 | + | |
| 1989 | 2711 | $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' ); |
| 1990 | 2712 | if ( $select_groups ) { |
| 1991 | - return 'SELECT wp.*,wp_sync.*,wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 2713 | + return 'SELECT wp.*,wp_sync.*' . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 1992 | 2714 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 1993 | 2715 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 1994 | 2716 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 1995 | 2717 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 1996 | - JOIN ' . $this->get_option_view_by( $view, $view_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2718 | + ' . $view_joins . ' | |
| 1997 | 2719 | WHERE wp.id = ' . $id . $where . ' |
| 1998 | 2720 | GROUP BY wp.id, wp_sync.sync_id'; |
| 1999 | 2721 | } |
| 2000 | 2722 | |
| 2001 | - return 'SELECT wp.*,wp_sync.*,wp_optionview.* | |
| 2723 | + return 'SELECT wp.*,wp_sync.*' . $view_selects . ' | |
| 2002 | 2724 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 2003 | 2725 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 2004 | - JOIN ' . $this->get_option_view_by( $view, $view_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2726 | + ' . $view_joins . ' | |
| 2005 | 2727 | WHERE id = ' . $id . $where; |
| 2006 | 2728 | } |
| 2007 | 2729 | return null; |
| 2008 | 2730 | } |
| @@ -2038,24 +2760,35 @@ | ||
| 2038 | 2760 | $extra_view = array( 'favi_icon', 'site_info' ); |
| 2039 | 2761 | } |
| 2040 | 2762 | |
| 2041 | 2763 | if ( MainWP_Utility::ctype_digit( $id ) ) { |
| 2764 | + | |
| 2765 | + $view_selects = ''; | |
| 2766 | + $view_joins = ''; | |
| 2767 | + | |
| 2768 | + $opts_view = $this->get_wp_options_join( $extra_view ); | |
| 2769 | + | |
| 2770 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 2771 | + $view_selects = ',' . $opts_view['selects']; | |
| 2772 | + $view_joins = $opts_view['joins']; | |
| 2773 | + } | |
| 2774 | + | |
| 2042 | 2775 | $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' ); |
| 2043 | 2776 | if ( $selectGroups ) { |
| 2044 | - return 'SELECT wp.*,wp_sync.*,wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 2777 | + return 'SELECT wp.*,wp_sync.*' . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 2045 | 2778 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 2046 | 2779 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 2047 | 2780 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 2048 | 2781 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 2049 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2782 | + ' . $view_joins . ' | |
| 2050 | 2783 | WHERE wp.id = ' . $id . $where . ' |
| 2051 | 2784 | GROUP BY wp.id, wp_sync.sync_id'; |
| 2052 | 2785 | } |
| 2053 | 2786 | |
| 2054 | - return 'SELECT wp.*,wp_sync.*,wp_optionview.* | |
| 2787 | + return 'SELECT wp.*,wp_sync.*' . $view_selects . ' | |
| 2055 | 2788 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 2056 | 2789 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 2057 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2790 | + ' . $view_joins . ' | |
| 2058 | 2791 | WHERE id = ' . $id . $where; |
| 2059 | 2792 | } |
| 2060 | 2793 | |
| 2061 | 2794 | return null; |
| @@ -2093,11 +2826,22 @@ | ||
| 2093 | 2826 | return ( is_numeric( $e ) && 0 < $e ) ? true : false; |
| 2094 | 2827 | } |
| 2095 | 2828 | ); |
| 2096 | 2829 | |
| 2097 | - $where = $this->get_sql_where_allow_access_sites(); | |
| 2830 | + $where = $this->get_sql_where_allow_access_sites(); | |
| 2831 | + $table_name = esc_sql( $this->table_name( 'wp' ) ); | |
| 2832 | + $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); | |
| 2833 | + $sql = "SELECT * FROM {$table_name} WHERE id IN ({$placeholders})"; | |
| 2834 | + $params = $ids; | |
| 2098 | 2835 | |
| 2099 | - return $this->wpdb->get_results( 'SELECT * FROM ' . $this->table_name( 'wp' ) . ' WHERE id IN (' . implode( ',', $ids ) . ')' . ( null !== $userId ? ' AND userid = ' . intval( $userId ) : '' ) . $where, OBJECT ); | |
| 2836 | + if ( null !== $userId ) { | |
| 2837 | + $sql .= ' AND userid = %d'; | |
| 2838 | + $params[] = intval( $userId ); | |
| 2839 | + } | |
| 2840 | + | |
| 2841 | + $sql .= ' ' . $where; | |
| 2842 | + | |
| 2843 | + return $this->wpdb->get_results( $this->wpdb->prepare( $sql, ...$params ), OBJECT ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $where fragment is from validated get_sql_where_access_sites() with numeric IDs | |
| 2100 | 2844 | } |
| 2101 | 2845 | |
| 2102 | 2846 | /** |
| 2103 | 2847 | * Get child sites by groups IDs. |
| @@ -2150,17 +2894,107 @@ | ||
| 2150 | 2894 | |
| 2151 | 2895 | /** |
| 2152 | 2896 | * Get child sites by group ID. |
| 2153 | 2897 | * |
| 2898 | + * @param int $id Group ID. | |
| 2899 | + * @param bool $selectgroups Selected groups. Default: false. | |
| 2900 | + * @param string $orderBy Order list by. Default: URL. | |
| 2901 | + * @param bool $offset Query offset. Default: false. | |
| 2902 | + * @param bool $rowcount Row count. Default: falese. | |
| 2903 | + * @param null $where SQL WHERE value. | |
| 2904 | + * @param null $search_site Site search field value. Default: null. | |
| 2905 | + * @param array $others Others params. | |
| 2906 | + * | |
| 2907 | + * @return object|null Database query result or null on failure. | |
| 2908 | + */ | |
| 2909 | + public function get_websites_by_group_id( //phpcs:ignore -- NOSONAR -ok. | |
| 2910 | + $id, | |
| 2911 | + $selectgroups = false, | |
| 2912 | + $orderBy = 'wp.url', | |
| 2913 | + $offset = false, | |
| 2914 | + $rowcount = false, | |
| 2915 | + $where = null, | |
| 2916 | + $search_site = null, | |
| 2917 | + $others = array() | |
| 2918 | + ) { | |
| 2919 | + return $this->get_results_result( | |
| 2920 | + $this->get_sql_websites_by_group_id( | |
| 2921 | + $id, | |
| 2922 | + $selectgroups, | |
| 2923 | + $orderBy, | |
| 2924 | + $offset, | |
| 2925 | + $rowcount, | |
| 2926 | + $where, | |
| 2927 | + $search_site, | |
| 2928 | + $others | |
| 2929 | + ) | |
| 2930 | + ); | |
| 2931 | + } | |
| 2932 | + | |
| 2933 | + /** | |
| 2934 | + * Get count of child sites by group ID. | |
| 2935 | + * | |
| 2936 | + * Uses an efficient COUNT query instead of fetching all rows. | |
| 2937 | + * | |
| 2154 | 2938 | * @param int $id Group ID. |
| 2155 | 2939 | * |
| 2156 | - * @return object|null Database query result or null on failure. | |
| 2940 | + * @return int Number of sites in the group. | |
| 2941 | + * | |
| 2942 | + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit() | |
| 2157 | 2943 | */ |
| 2158 | - public function get_websites_by_group_id( $id ) { | |
| 2159 | - return $this->get_results_result( $this->get_sql_websites_by_group_id( $id ) ); | |
| 2944 | + public function get_websites_count_by_group_id( $id ) { | |
| 2945 | + if ( ! MainWP_Utility::ctype_digit( $id ) ) { | |
| 2946 | + return 0; | |
| 2947 | + } | |
| 2948 | + | |
| 2949 | + // Determine if this is the staging group. | |
| 2950 | + $is_staging = 'no'; | |
| 2951 | + $staging_group = get_option( 'mainwp_stagingsites_group_id' ); | |
| 2952 | + if ( $staging_group && (int) $id === (int) $staging_group ) { | |
| 2953 | + $is_staging = 'yes'; | |
| 2954 | + } | |
| 2955 | + | |
| 2956 | + $where_allowed = $this->get_sql_where_allow_access_sites( 'wp', $is_staging ); | |
| 2957 | + | |
| 2958 | + // Use prepare() for the groupid parameter. | |
| 2959 | + $qry = $this->wpdb->prepare( | |
| 2960 | + 'SELECT COUNT(DISTINCT wp.id) FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 2961 | + JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid | |
| 2962 | + WHERE wpgroup.groupid = %d', | |
| 2963 | + $id | |
| 2964 | + ) . $where_allowed; | |
| 2965 | + | |
| 2966 | + return (int) $this->wpdb->get_var( $qry ); //phpcs:ignore PluginCheck.Security.DirectDB.Unprepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $where_allowed is from validated get_sql_where_access_sites() with numeric IDs. | |
| 2160 | 2967 | } |
| 2161 | 2968 | |
| 2969 | + | |
| 2162 | 2970 | /** |
| 2971 | + * Get child site connection status counts. | |
| 2972 | + * | |
| 2973 | + * @return array|null { | |
| 2974 | + * Connection status counts. | |
| 2975 | + * | |
| 2976 | + * @type int $total_sites Total number of sites. | |
| 2977 | + * @type int $connected_sites Number of connected sites. | |
| 2978 | + * @type int $disconnected_sites Number of disconnected sites. | |
| 2979 | + * } | |
| 2980 | + */ | |
| 2981 | + public function get_sites_connections_status() { | |
| 2982 | + | |
| 2983 | + $is_staging = 'no'; | |
| 2984 | + | |
| 2985 | + $sites_table = $this->table_name( 'wp' ); | |
| 2986 | + $sync_table = $this->table_name( 'wp_sync' ); | |
| 2987 | + | |
| 2988 | + return $this->wpdb->get_row( | |
| 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. | |
| 2990 | + $this->get_sql_where_allow_access_sites( 'wp', $is_staging ), | |
| 2991 | + ARRAY_A | |
| 2992 | + ); | |
| 2993 | + } | |
| 2994 | + | |
| 2995 | + | |
| 2996 | + /** | |
| 2163 | 2997 | * Get child sites by group id via SQL. |
| 2164 | 2998 | * |
| 2165 | 2999 | * @param int $id Group ID. |
| 2166 | 3000 | * @param bool $selectgroups Selected groups. Default: false. |
| @@ -2195,33 +3029,58 @@ | ||
| 2195 | 3029 | } |
| 2196 | 3030 | |
| 2197 | 3031 | $where_search = ''; |
| 2198 | 3032 | if ( ! empty( $search_site ) ) { |
| 2199 | - $search_site = trim( $search_site ); | |
| 2200 | - $where_search .= ' AND (wp.name LIKE "%' . $this->escape( $search_site ) . '%" OR wp.url LIKE "%' . $this->escape( $search_site ) . '%") '; | |
| 3033 | + $search_site = trim( $search_site ); | |
| 3034 | + // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety. | |
| 3035 | + $like_pattern = '%' . $this->wpdb->esc_like( $search_site ) . '%'; | |
| 3036 | + $where_search .= $this->wpdb->prepare( | |
| 3037 | + ' AND (wp.name LIKE %s OR wp.url LIKE %s) ', | |
| 3038 | + $like_pattern, | |
| 3039 | + $like_pattern | |
| 3040 | + ); | |
| 2201 | 3041 | } |
| 2202 | 3042 | |
| 2203 | 3043 | $extra_view = is_array( $others ) && isset( $others['extra_view'] ) && is_array( $others['extra_view'] ) && ! empty( $others['extra_view'] ) ? $others['extra_view'] : array( 'site_info' ); |
| 2204 | 3044 | |
| 3045 | + $view_query = null; | |
| 3046 | + if ( is_array( $others ) && ! empty( $others['view_query'] ) ) { | |
| 3047 | + $view_query = $others['view_query']; | |
| 3048 | + } | |
| 3049 | + | |
| 3050 | + if ( empty( $view_query ) ) { | |
| 3051 | + $view_query = $selectgroups ? 'default' : 'group'; // To compatible. | |
| 3052 | + } | |
| 3053 | + | |
| 3054 | + $view_selects = ''; | |
| 3055 | + $view_joins = ''; | |
| 3056 | + | |
| 3057 | + $opts_view = $this->get_wp_options_join( $extra_view, $view_query ); | |
| 3058 | + | |
| 3059 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 3060 | + $view_selects = ',' . $opts_view['selects']; | |
| 3061 | + $view_joins = $opts_view['joins']; | |
| 3062 | + } | |
| 3063 | + | |
| 2205 | 3064 | if ( MainWP_Utility::ctype_digit( $id ) ) { |
| 2206 | 3065 | $where_allowed = $this->get_sql_where_allow_access_sites( 'wp', $is_staging ); |
| 2207 | 3066 | if ( $selectgroups ) { |
| 2208 | - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.*, GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 3067 | + $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ', GROUP_CONCAT(gr.name ORDER BY gr.name SEPARATOR ",") as wpgroups, GROUP_CONCAT(gr.id ORDER BY gr.name SEPARATOR ",") as wpgroupids, GROUP_CONCAT(gr.color ORDER BY gr.name SEPARATOR ",") as wpgroups_colors | |
| 2209 | 3068 | FROM ' . $this->table_name( 'wp' ) . ' wp |
| 2210 | 3069 | JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid |
| 2211 | 3070 | LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid |
| 2212 | 3071 | LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id |
| 2213 | 3072 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 2214 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 3073 | + ' . $view_joins . ' | |
| 2215 | 3074 | WHERE wpgroup.groupid = ' . $id . ' ' . |
| 2216 | 3075 | ( empty( $where ) ? '' : ' AND ' . $where ) . $where_allowed . $where_search . ' |
| 2217 | 3076 | GROUP BY wp.id, wp_sync.sync_id |
| 2218 | 3077 | ORDER BY ' . $orderBy; |
| 2219 | 3078 | } else { |
| 2220 | - $qry = 'SELECT wp.*,wp_optionview.*, wp_sync.* FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 3079 | + $qry = 'SELECT wp.*' . $view_selects . ', wp_sync.* FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 2221 | 3080 | JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid |
| 2222 | 3081 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 2223 | - JOIN ' . $this->get_option_view( $extra_view, 'group' ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 3082 | + ' . $view_joins . ' | |
| 2224 | 3083 | WHERE wpgroup.groupid = ' . $id . ' ' . $where_allowed . $where_search . |
| 2225 | 3084 | ( empty( $where ) ? '' : ' AND ' . $where ) . ' ORDER BY ' . $orderBy; |
| 2226 | 3085 | } |
| 2227 | 3086 | if ( ( false !== $offset ) && ( false !== $rowcount ) ) { |
| @@ -2270,13 +3129,23 @@ | ||
| 2270 | 3129 | |
| 2271 | 3130 | $userid = $current_user->ID; |
| 2272 | 3131 | } |
| 2273 | 3132 | |
| 2274 | - $sql = 'SELECT wp.*,wp_sync.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 3133 | + $view_selects = ''; | |
| 3134 | + $view_joins = ''; | |
| 3135 | + | |
| 3136 | + $opts_view = $this->get_wp_options_join(); | |
| 3137 | + | |
| 3138 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 3139 | + $view_selects = ',' . $opts_view['selects']; | |
| 3140 | + $view_joins = $opts_view['joins']; | |
| 3141 | + } | |
| 3142 | + | |
| 3143 | + $sql = 'SELECT wp.*,wp_sync.*' . $view_selects . ' FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 2275 | 3144 | INNER JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid |
| 2276 | 3145 | JOIN ' . $this->table_name( 'group' ) . ' g ON wpgroup.groupid = g.id |
| 2277 | 3146 | JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid |
| 2278 | - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 3147 | + ' . $view_joins . ' | |
| 2279 | 3148 | WHERE g.name="' . $this->escape( $groupname ) . '"'; |
| 2280 | 3149 | if ( null !== $userid ) { |
| 2281 | 3150 | $sql .= ' AND g.userid = "' . intval( $userid ) . '"'; |
| 2282 | 3151 | } |
| @@ -2291,9 +3160,10 @@ | ||
| 2291 | 3160 | * |
| 2292 | 3161 | * @return string|null Child site IP address or null on failure. |
| 2293 | 3162 | */ |
| 2294 | 3163 | public function get_wp_ip( $wpid ) { |
| 2295 | - return $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT ip FROM ' . $this->table_name( 'request_log' ) . ' WHERE wpid = %d', $wpid ) ); | |
| 3164 | + $table_name = esc_sql( $this->table_name( 'request_log' ) ); | |
| 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. | |
| 2296 | 3166 | } |
| 2297 | 3167 | |
| 2298 | 3168 | /** |
| 2299 | 3169 | * Add website to the MainWP Dashboard. |
| @@ -2325,9 +3195,9 @@ | ||
| 2325 | 3195 | } |
| 2326 | 3196 | |
| 2327 | 3197 | $groupids = isset( $params['groupids'] ) ? $params['groupids'] : array(); |
| 2328 | 3198 | $groupnames = isset( $params['groupnames'] ) ? $params['groupnames'] : array(); |
| 2329 | - $verifyCertificate = isset( $params['verifyCertificate'] ) ? (int) $params['verifyCertificate'] : 1; | |
| 3199 | + $verifyCertificate = isset( $params['verifyCertificate'] ) ? (int) $params['verifyCertificate'] : 2; | |
| 2330 | 3200 | $uniqueId = isset( $params['uniqueId'] ) ? $params['uniqueId'] : ''; |
| 2331 | 3201 | $http_user = isset( $params['http_user'] ) ? $params['http_user'] : null; |
| 2332 | 3202 | $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null; |
| 2333 | 3203 | $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0; |
| @@ -2332,9 +3202,25 @@ | ||
| 2332 | 3202 | $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null; |
| 2333 | 3203 | $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0; |
| 2334 | 3204 | $wpe = isset( $params['wpe'] ) ? $params['wpe'] : 0; |
| 2335 | 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; | |
| 2336 | 3210 | |
| 3211 | + // MWP-1548: encrypt http_user / http_pass at rest. Empty / null | |
| 3212 | + // values pass through unchanged (encrypt_credential is a no-op | |
| 3213 | + // for those). A non-empty value that fails to encrypt produces | |
| 3214 | + // false here, which we treat as a hard failure -- refuse to | |
| 3215 | + // persist plaintext credentials when the encryption layer is | |
| 3216 | + // unhealthy (missing keyfile, un-writable uploads dir). | |
| 3217 | + $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' ); | |
| 3218 | + $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' ); | |
| 3219 | + if ( false === $encrypted_http_user || false === $encrypted_http_pass ) { | |
| 3220 | + return false; | |
| 3221 | + } | |
| 3222 | + | |
| 2337 | 3223 | if ( MainWP_Utility::ctype_digit( $userid ) ) { |
| 2338 | 3224 | if ( '/' !== substr( $url, - 1 ) ) { |
| 2339 | 3225 | $url .= '/'; |
| 2340 | 3226 | } |
| @@ -2360,8 +3246,9 @@ | ||
| 2360 | 3246 | 'plugin_upgrades' => '', |
| 2361 | 3247 | 'theme_upgrades' => '', |
| 2362 | 3248 | 'translation_upgrades' => '', |
| 2363 | 3249 | 'securityIssues' => '', |
| 3250 | + 'premium_upgrades' => '', | |
| 2364 | 3251 | 'themes' => '', |
| 2365 | 3252 | 'ignored_themes' => '', |
| 2366 | 3253 | 'plugins' => '', |
| 2367 | 3254 | 'ignored_plugins' => '', |
| @@ -2373,14 +3260,18 @@ | ||
| 2373 | 3260 | 'verify_certificate' => intval( $verifyCertificate ), |
| 2374 | 3261 | 'ssl_version' => $sslVersion, |
| 2375 | 3262 | 'uniqueId' => $uniqueId, |
| 2376 | 3263 | 'mainwpdir' => 0, |
| 2377 | - 'http_user' => $http_user, | |
| 2378 | - 'http_pass' => $http_pass, | |
| 3264 | + 'http_user' => $encrypted_http_user, | |
| 3265 | + 'http_pass' => $encrypted_http_pass, | |
| 2379 | 3266 | 'wpe' => $wpe, |
| 2380 | 3267 | 'is_staging' => $isStaging, |
| 2381 | 3268 | ); |
| 2382 | 3269 | |
| 3270 | + if ( null !== $force_use_ipv4 ) { | |
| 3271 | + $values['force_use_ipv4'] = $force_use_ipv4; | |
| 3272 | + } | |
| 3273 | + | |
| 2383 | 3274 | $syncValues = array( |
| 2384 | 3275 | 'dtsSync' => 0, |
| 2385 | 3276 | 'dtsSyncStart' => 0, |
| 2386 | 3277 | 'dtsAutomaticSync' => 0, |
| @@ -2390,11 +3281,13 @@ | ||
| 2390 | 3281 | 'sync_errors' => '', |
| 2391 | 3282 | ); |
| 2392 | 3283 | if ( $this->wpdb->insert( $this->table_name( 'wp' ), $values ) ) { |
| 2393 | 3284 | $websiteid = $this->wpdb->insert_id; |
| 3285 | + MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[Insert site=%s]', $this->get_last_query() ) ); // after: $this->wpdb->insert_id. | |
| 2394 | 3286 | MainWP_Encrypt_Data_Lib::instance()->encrypt_save_keys( $websiteid, $en_pk_data ); |
| 2395 | 3287 | $syncValues['wpid'] = $websiteid; |
| 2396 | 3288 | $this->wpdb->insert( $this->table_name( 'wp_sync' ), $syncValues ); |
| 3289 | + MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[Insert sync data=%s]', $this->get_last_query() ) ); | |
| 2397 | 3290 | $this->wpdb->insert( |
| 2398 | 3291 | $this->table_name( 'wp_settings_backup' ), |
| 2399 | 3292 | array( |
| 2400 | 3293 | 'wpid' => $websiteid, |
| @@ -2423,9 +3316,9 @@ | ||
| 2423 | 3316 | 'groupid' => $groupid, |
| 2424 | 3317 | ) |
| 2425 | 3318 | ); |
| 2426 | 3319 | } |
| 2427 | - | |
| 3320 | + MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache(); | |
| 2428 | 3321 | return $websiteid; |
| 2429 | 3322 | } |
| 2430 | 3323 | } |
| 2431 | 3324 | |
| @@ -2442,14 +3335,15 @@ | ||
| 2442 | 3335 | * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit() |
| 2443 | 3336 | */ |
| 2444 | 3337 | public function remove_website( $websiteid ) { |
| 2445 | 3338 | if ( MainWP_Utility::ctype_digit( $websiteid ) ) { |
| 2446 | - $nr = $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp' ) . ' WHERE id=%d', $websiteid ) ); | |
| 2447 | - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE wpid=%d', $websiteid ) ); | |
| 2448 | - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_sync' ) . ' WHERE wpid=%d', $websiteid ) ); | |
| 2449 | - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid=%d', $websiteid ) ); | |
| 3339 | + $nr = $this->wpdb->delete( $this->table_name( 'wp' ), array( 'id' => $websiteid ) ); | |
| 3340 | + $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) ); | |
| 3341 | + $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) ); | |
| 3342 | + $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) ); | |
| 2450 | 3343 | MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid ); |
| 2451 | 3344 | MainWP_DB_Uptime_Monitoring::instance()->delete_monitor( array( 'wpid' => $websiteid ) ); |
| 3345 | + MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache(); | |
| 2452 | 3346 | return $nr; |
| 2453 | 3347 | } |
| 2454 | 3348 | |
| 2455 | 3349 | return false; |
| @@ -2464,11 +3358,31 @@ | ||
| 2464 | 3358 | * @return int|boolean The number of rows updated, or false on error. |
| 2465 | 3359 | */ |
| 2466 | 3360 | public function update_website_values( $websiteid, $fields ) { |
| 2467 | 3361 | if ( ! empty( $fields ) ) { |
| 3362 | + // MWP-1548: encrypt http_user / http_pass at rest if either | |
| 3363 | + // is present in $fields. Generic-fields updaters (clone | |
| 3364 | + // flow in extensions-handler, callers that pass arbitrary | |
| 3365 | + // column subsets) must go through the same fail-closed | |
| 3366 | + // contract as add_website / update_website. | |
| 3367 | + if ( array_key_exists( 'http_user', $fields ) ) { | |
| 3368 | + $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_user'], 'http_user' ); | |
| 3369 | + if ( false === $encrypted ) { | |
| 3370 | + return false; | |
| 3371 | + } | |
| 3372 | + $fields['http_user'] = $encrypted; | |
| 3373 | + } | |
| 3374 | + if ( array_key_exists( 'http_pass', $fields ) ) { | |
| 3375 | + $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_pass'], 'http_pass' ); | |
| 3376 | + if ( false === $encrypted ) { | |
| 3377 | + return false; | |
| 3378 | + } | |
| 3379 | + $fields['http_pass'] = $encrypted; | |
| 3380 | + } | |
| 2468 | 3381 | // Lock the data stream to prevent other processes from updating at the same time. |
| 2469 | - $sql = $this->wpdb->prepare( | |
| 2470 | - 'SELECT * FROM ' . $this->table_name( 'wp' ) . ' WHERE id = %d FOR UPDATE', | |
| 3382 | + $table_name = esc_sql( $this->table_name( 'wp' ) ); | |
| 3383 | + $sql = $this->wpdb->prepare( | |
| 3384 | + "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd. | |
| 2471 | 3385 | $websiteid |
| 2472 | 3386 | ); |
| 2473 | 3387 | $this->wpdb->get_row( $sql ); |
| 2474 | 3388 | |
| @@ -2515,9 +3429,9 @@ | ||
| 2515 | 3429 | * @param string $http_pass HTTP Basic Authentication password. |
| 2516 | 3430 | * @param int $sslVersion SSL Version. |
| 2517 | 3431 | * @param bool $disableHealthChecking Disable Site health threshold. |
| 2518 | 3432 | * @param int $healthThreshold Site health threshold. |
| 2519 | - * @param int $wpe Is it WP Engine hosted site. | |
| 3433 | + * @param string $backup_method Primary backup method. | |
| 2520 | 3434 | * |
| 2521 | 3435 | * @return boolean ture on success or false on failure. |
| 2522 | 3436 | * |
| 2523 | 3437 | * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website() |
| @@ -2541,25 +3455,67 @@ | ||
| 2541 | 3455 | $http_user = null, |
| 2542 | 3456 | $http_pass = null, |
| 2543 | 3457 | $sslVersion = 0, |
| 2544 | 3458 | $disableHealthChecking = 1, |
| 2545 | - $healthThreshold = 80, | |
| 2546 | - $wpe = 0 | |
| 3459 | + $healthThreshold = 0, | |
| 3460 | + $backup_method = 'global' | |
| 2547 | 3461 | ) { |
| 2548 | 3462 | |
| 3463 | + $wpe = 0; // going to update when sync. | |
| 3464 | + | |
| 2549 | 3465 | if ( MainWP_Utility::ctype_digit( $websiteid ) && MainWP_Utility::ctype_digit( $userid ) ) { |
| 2550 | 3466 | $website = $this->get_website_by_id( $websiteid ); |
| 2551 | 3467 | if ( MainWP_System_Utility::can_edit_website( $website ) ) { |
| 3468 | + // MWP-1548: encrypt http_user / http_pass at rest. Same | |
| 3469 | + // fail-closed contract as add_website -- a non-empty | |
| 3470 | + // value that fails to encrypt aborts the update so we | |
| 3471 | + // never overwrite an existing encrypted row with | |
| 3472 | + // plaintext when the encryption layer is unhealthy. | |
| 3473 | + $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' ); | |
| 3474 | + $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' ); | |
| 3475 | + if ( false === $encrypted_http_user || false === $encrypted_http_pass ) { | |
| 3476 | + return false; | |
| 3477 | + } | |
| 2552 | 3478 | // update admin. |
| 2553 | - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'wp' ) . ' SET url="' . $this->escape( $url ) . '", name="' . $this->escape( wp_strip_all_tags( $name ) ) . '", adminname="' . $this->escape( $siteadmin ) . '",pluginDir="' . $this->escape( $pluginDir ) . '", verify_certificate="' . intval( $verifyCertificate ) . '", ssl_version="' . intval( $sslVersion ) . '", wpe="' . intval( $wpe ) . '", uniqueId="' . $this->escape( $uniqueId ) . '", http_user="' . $this->escape( $http_user ) . '", http_pass="' . $this->escape( $http_pass ) . '", disable_health_check="' . $this->escape( $disableHealthChecking ) . '", health_threshold="' . $this->escape( $healthThreshold ) . '" WHERE id=%d', $websiteid ) ); | |
| 2554 | - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'wp_settings_backup' ) . ' SET archiveFormat = "' . $this->escape( $archiveFormat ) . '" WHERE wpid=%d', $websiteid ) ); | |
| 3479 | + $this->wpdb->update( | |
| 3480 | + $this->table_name( 'wp' ), | |
| 3481 | + array( | |
| 3482 | + 'url' => $url, | |
| 3483 | + 'name' => wp_strip_all_tags( $name ), | |
| 3484 | + 'adminname' => $siteadmin, | |
| 3485 | + 'pluginDir' => $pluginDir, | |
| 3486 | + 'verify_certificate' => intval( $verifyCertificate ), | |
| 3487 | + 'ssl_version' => intval( $sslVersion ), | |
| 3488 | + 'wpe' => intval( $wpe ), | |
| 3489 | + 'uniqueId' => $uniqueId, | |
| 3490 | + 'http_user' => $encrypted_http_user, | |
| 3491 | + 'http_pass' => $encrypted_http_pass, | |
| 3492 | + 'disable_health_check' => $disableHealthChecking, | |
| 3493 | + 'health_threshold' => $healthThreshold, | |
| 3494 | + 'primary_backup_method' => $backup_method, | |
| 3495 | + ), | |
| 3496 | + array( 'id' => $websiteid ) | |
| 3497 | + ); | |
| 3498 | + $this->wpdb->update( | |
| 3499 | + $this->table_name( 'wp_settings_backup' ), | |
| 3500 | + array( 'archiveFormat' => $archiveFormat ), | |
| 3501 | + array( 'wpid' => $websiteid ) | |
| 3502 | + ); | |
| 2555 | 3503 | |
| 2556 | 3504 | if ( get_option( 'mainwp_enableLegacyBackupFeature' ) ) { |
| 2557 | - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'wp' ) . ' SET maximumFileDescriptorsOverride = ' . ( $maximumFileDescriptorsOverride ? 1 : 0 ) . ',maximumFileDescriptorsAuto= ' . ( $maximumFileDescriptorsAuto ? 1 : 0 ) . ',maximumFileDescriptors = ' . $maximumFileDescriptors . ' WHERE id=%d', $websiteid ) ); | |
| 3505 | + $this->wpdb->update( | |
| 3506 | + $this->table_name( 'wp' ), | |
| 3507 | + array( | |
| 3508 | + 'maximumFileDescriptorsOverride' => (int) $maximumFileDescriptorsOverride, | |
| 3509 | + 'maximumFileDescriptorsAuto' => (int) $maximumFileDescriptorsAuto, | |
| 3510 | + 'maximumFileDescriptors' => (int) $maximumFileDescriptors, | |
| 3511 | + ), | |
| 3512 | + array( 'id' => $websiteid ) | |
| 3513 | + ); | |
| 2558 | 3514 | } |
| 2559 | 3515 | |
| 2560 | 3516 | // remove groups. |
| 2561 | - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE wpid=%d', $websiteid ) ); | |
| 3517 | + $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) ); | |
| 2562 | 3518 | // Remove GA stats. |
| 2563 | 3519 | $showErrors = $this->wpdb->hide_errors(); |
| 2564 | 3520 | |
| 2565 | 3521 | /** |
| @@ -2646,9 +3602,11 @@ | ||
| 2646 | 3602 | public function get_websites_by_url( $url ) { |
| 2647 | 3603 | if ( '/' !== substr( $url, - 1 ) ) { |
| 2648 | 3604 | $url .= '/'; |
| 2649 | 3605 | } |
| 2650 | - $results = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s ', $this->escape( $url ) ), OBJECT ); | |
| 3606 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 3607 | + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) ); | |
| 3608 | + $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. | |
| 2651 | 3609 | if ( $results ) { |
| 2652 | 3610 | return $results; |
| 2653 | 3611 | } |
| 2654 | 3612 | |
| @@ -2660,9 +3618,9 @@ | ||
| 2660 | 3618 | $url = str_replace( 'https://', 'https://www.', $url ); |
| 2661 | 3619 | $url = str_replace( 'http://', 'http://www.', $url ); |
| 2662 | 3620 | } |
| 2663 | 3621 | |
| 2664 | - $results = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s ', $this->escape( $url ) ), OBJECT ); | |
| 3622 | + $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. | |
| 2665 | 3623 | if ( $results ) { |
| 2666 | 3624 | return $results; |
| 2667 | 3625 | } |
| 2668 | 3626 | |
| @@ -2667,36 +3625,68 @@ | ||
| 2667 | 3625 | } |
| 2668 | 3626 | |
| 2669 | 3627 | $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url ); |
| 2670 | 3628 | |
| 2671 | - return $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_sync' ) . " wp_sync ON wp.id = wp_sync.wpid WHERE replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') = %s ", $this->escape( $url ) ), OBJECT ); | |
| 3629 | + 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. | |
| 2672 | 3630 | } |
| 2673 | 3631 | |
| 2674 | 3632 | /** |
| 3633 | + * Get recently-synced child sites whose stored url differs from the | |
| 3634 | + * child-reported siteurl. | |
| 2675 | 3635 | * |
| 2676 | - * Get websites offline status. | |
| 3636 | + * The trailing slash must be normalized in SQL: stored urls always end | |
| 3637 | + * with a slash while child-reported siteurl values never do, so a plain | |
| 3638 | + * inequality would match nearly every site. | |
| 2677 | 3639 | * |
| 2678 | - * @deprecated see new compatible uptime monitoring. | |
| 3640 | + * @since 6.2 | |
| 2679 | 3641 | * |
| 2680 | - * @since 5.3. | |
| 3642 | + * @param int $days Freshness gate: only sites synced within this many days. | |
| 2681 | 3643 | * |
| 2682 | - * @return array Child site monitoring status. | |
| 3644 | + * @return array|object|null Rows with id, url, siteurl or null on failure. | |
| 2683 | 3645 | */ |
| 2684 | - public function get_websites_offline_status_to_send_notice() { | |
| 2685 | - $where = $this->get_sql_where_allow_access_sites( 'wp' ); | |
| 2686 | - $extra_view = array( 'monitoring_notification_emails', 'settings_notification_emails' ); | |
| 3646 | + public function get_websites_with_url_siteurl_mismatch( $days = 30 ) { | |
| 3647 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 3648 | + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) ); | |
| 3649 | + $minimum_sync = time() - intval( $days ) * DAY_IN_SECONDS; | |
| 2687 | 3650 | |
| 2688 | - return $this->wpdb->get_results( | |
| 2689 | - 'SELECT wp.*,wp_sync.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 2690 | - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid | |
| 2691 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2692 | - WHERE wp.disable_status_check <> 1 AND wp.offline_check_result <> 1 AND wp.offline_check_result <> 0 AND wp.http_code_noticed = 0' . // http_code_noticed = 0: not noticed yet. | |
| 2693 | - $where, | |
| 2694 | - OBJECT | |
| 2695 | - ); | |
| 3651 | + 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. | |
| 2696 | 3652 | } |
| 2697 | 3653 | |
| 2698 | 3654 | /** |
| 3655 | + * Get IDs of child sites that have a non-empty value for a website option. | |
| 3656 | + * | |
| 3657 | + * @since 6.2 | |
| 3658 | + * | |
| 3659 | + * @param string $option_name Website option name. | |
| 3660 | + * @param int $limit Maximum number of IDs to return. | |
| 3661 | + * | |
| 3662 | + * @return array Site IDs. | |
| 3663 | + */ | |
| 3664 | + public function get_website_ids_with_nonempty_option( $option_name, $limit = 3 ) { | |
| 3665 | + $options_table = esc_sql( $this->table_name( 'wp_options' ) ); | |
| 3666 | + | |
| 3667 | + 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. | |
| 3668 | + } | |
| 3669 | + | |
| 3670 | + /** | |
| 3671 | + * Get one child site whose stored url exactly matches the given URL, | |
| 3672 | + * excluding a site ID. Used as the duplicate guard before a URL correction. | |
| 3673 | + * | |
| 3674 | + * @since 6.2 | |
| 3675 | + * | |
| 3676 | + * @param string $url URL to match ( trailing slash enforced ). | |
| 3677 | + * @param int $exclude_id Site ID to exclude. | |
| 3678 | + * | |
| 3679 | + * @return object|null Matching row ( id, url ) or null. | |
| 3680 | + */ | |
| 3681 | + public function get_website_by_exact_url_excluding_id( $url, $exclude_id ) { | |
| 3682 | + $url = rtrim( (string) $url, '/' ); | |
| 3683 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 3684 | + | |
| 3685 | + 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. | |
| 3686 | + } | |
| 3687 | + | |
| 3688 | + /** | |
| 2699 | 3689 | * Method get_websites_to_notice_health_threshold() |
| 2700 | 3690 | * |
| 2701 | 3691 | * Get websites to notice site health. |
| 2702 | 3692 | * |
| @@ -2717,14 +3707,23 @@ | ||
| 2717 | 3707 | |
| 2718 | 3708 | $where_site_threshold = ' ( wp.health_threshold = 80 AND wp_sync.health_value < 80 ) '; // should-be-improved site health. |
| 2719 | 3709 | $where_site_threshold .= ' OR ( wp.health_threshold = 100 AND wp_sync.health_value >= 80 ) '; // good site health. |
| 2720 | 3710 | |
| 2721 | - return $this->wpdb->get_results( | |
| 2722 | - 'SELECT wp.*,wp_sync.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 2723 | - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid | |
| 2724 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2725 | - 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 ' . | |
| 2726 | - $where, | |
| 3711 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 3712 | + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) ); | |
| 3713 | + | |
| 3714 | + $view_selects = ''; | |
| 3715 | + $view_joins = ''; | |
| 3716 | + | |
| 3717 | + $opts_view = $this->get_wp_options_join( $extra_view ); | |
| 3718 | + | |
| 3719 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 3720 | + $view_selects = ',' . $opts_view['selects']; | |
| 3721 | + $view_joins = $opts_view['joins']; | |
| 3722 | + } | |
| 3723 | + return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery | |
| 3724 | + "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. | |
| 3725 | + $where . ' GROUP BY wp.id ', | |
| 2727 | 3726 | OBJECT |
| 2728 | 3727 | ); |
| 2729 | 3728 | } |
| 2730 | 3729 | |
| @@ -2735,19 +3734,43 @@ | ||
| 2735 | 3734 | */ |
| 2736 | 3735 | public function get_websites_http_check_status() { |
| 2737 | 3736 | $where = $this->get_sql_where_allow_access_sites( 'wp' ); |
| 2738 | 3737 | $extra_view = array( 'settings_notification_emails' ); |
| 3738 | + $wp_table = esc_sql( $this->table_name( 'wp' ) ); | |
| 2739 | 3739 | |
| 3740 | + $view_selects = ''; | |
| 3741 | + $view_joins = ''; | |
| 3742 | + | |
| 3743 | + $opts_view = $this->get_wp_options_join( $extra_view ); | |
| 3744 | + | |
| 3745 | + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) { | |
| 3746 | + $view_selects = ',' . $opts_view['selects']; | |
| 3747 | + $view_joins = $opts_view['joins']; | |
| 3748 | + } | |
| 3749 | + | |
| 3750 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery. | |
| 2740 | 3751 | return $this->wpdb->get_results( |
| 2741 | - 'SELECT wp.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp | |
| 2742 | - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid | |
| 2743 | - WHERE wp.disable_status_check <> 1 AND wp.offline_check_result = -1' . // offline checked status. | |
| 2744 | - $where, | |
| 3752 | + "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. | |
| 3753 | + $where . ' GROUP BY wp.id ', | |
| 2745 | 3754 | OBJECT |
| 2746 | 3755 | ); |
| 2747 | 3756 | } |
| 2748 | 3757 | |
| 2749 | 3758 | /** |
| 3759 | + * Method set_website_noticed_http_check(). | |
| 3760 | + * | |
| 3761 | + * @param array $site_id The site id . | |
| 3762 | + * | |
| 3763 | + * @return void | |
| 3764 | + */ | |
| 3765 | + public function set_website_noticed_http_check( $site_id = array() ) { | |
| 3766 | + if ( empty( $site_id ) ) { | |
| 3767 | + return; | |
| 3768 | + } | |
| 3769 | + $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . esc_sql( $this->table_name( 'wp' ) ) . ' SET http_code_noticed = 1 WHERE id = %d', $site_id ) ); // phpcs:ignore PluginCheck.Security.DirectDB.UnpreparedSQL -- $site_id is an integer that is validated by ctype_digit() before being passed to this method. | |
| 3770 | + } | |
| 3771 | + | |
| 3772 | + /** | |
| 2750 | 3773 | * Get DB Sites. |
| 2751 | 3774 | * |
| 2752 | 3775 | * @since 4.6 |
| 2753 | 3776 | * |
| @@ -2976,18 +3999,28 @@ | ||
| 2976 | 3999 | * |
| 2977 | 4000 | * @return mixed Result |
| 2978 | 4001 | */ |
| 2979 | 4002 | public function insert_lookup_item( $item_name, $item_id, $obj_name, $obj_id ) { |
| 4003 | + // get_lookup_items() and delete_lookup_items() match item_name as given, so a | |
| 4004 | + // name that sanitizing would alter is refused instead of being stored under a | |
| 4005 | + // value the caller can never read back or delete. | |
| 4006 | + if ( ! is_string( $item_name ) || sanitize_text_field( $item_name ) !== $item_name ) { | |
| 4007 | + return false; | |
| 4008 | + } | |
| 2980 | 4009 | if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) { |
| 2981 | 4010 | return false; |
| 2982 | 4011 | } |
| 2983 | 4012 | $data = array( |
| 2984 | - 'item_name' => 'cost', | |
| 4013 | + 'item_name' => $item_name, | |
| 2985 | 4014 | 'item_id' => $item_id, |
| 2986 | 4015 | 'object_name' => $obj_name, |
| 2987 | 4016 | 'object_id' => $obj_id, |
| 2988 | 4017 | ); |
| 2989 | - $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data ); | |
| 4018 | + // wpdb refuses a value past the column length before any SQL runs, and | |
| 4019 | + // insert_id would still hold the previous insert's id. | |
| 4020 | + if ( false === $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data ) ) { | |
| 4021 | + return false; | |
| 4022 | + } | |
| 2990 | 4023 | return $this->wpdb->insert_id; // must return lookup id. |
| 2991 | 4024 | } |
| 2992 | 4025 | |
| 2993 | 4026 | /** |
| @@ -3049,10 +4082,16 @@ | ||
| 3049 | 4082 | } |
| 3050 | 4083 | |
| 3051 | 4084 | |
| 3052 | 4085 | /** |
| 3053 | - * Return the user data for the given consumer_key. | |
| 4086 | + * Insert a new REST API key row and return the credential payload. | |
| 3054 | 4087 | * |
| 4088 | + * Returns an array with key_id, user_id, plaintext consumer_key, | |
| 4089 | + * plaintext consumer_secret, and key_permissions on success. Returns | |
| 4090 | + * false when the wpdb->insert() call fails (no current user, missing | |
| 4091 | + * table, schema mismatch, etc.). Callers must check the return type | |
| 4092 | + * before treating it as an array. | |
| 4093 | + * | |
| 3055 | 4094 | * @param string $consumer_key Consumer key. |
| 3056 | 4095 | * @param string $consumer_secret Secret key. |
| 3057 | 4096 | * @param string $scope scope. |
| 3058 | 4097 | * @param string $description description. |
| @@ -3058,9 +4097,9 @@ | ||
| 3058 | 4097 | * @param string $description description. |
| 3059 | 4098 | * @param int $enabled 1 or 0. |
| 3060 | 4099 | * @param array $others others. |
| 3061 | 4100 | * |
| 3062 | - * @return array | |
| 4101 | + * @return array|false Credential payload on success, false on failure. | |
| 3063 | 4102 | */ |
| 3064 | 4103 | public function insert_rest_api_key( $consumer_key, $consumer_secret, $scope, $description, $enabled, $others = array() ) { |
| 3065 | 4104 | global $current_user; |
| 3066 | 4105 | |
| @@ -3071,18 +4110,19 @@ | ||
| 3071 | 4110 | if ( empty( $user_id ) ) { |
| 3072 | 4111 | return false; |
| 3073 | 4112 | } |
| 3074 | 4113 | |
| 3075 | - if ( ! is_array( $others ) ) { | |
| 3076 | - $others = array(); | |
| 3077 | - } | |
| 4114 | + unset( $others ); // Parameter retained for signature compatibility; key_pass/key_type fields are vestigial after MWP-1544 cleanup. | |
| 3078 | 4115 | |
| 3079 | - $pass = isset( $others['key_pass'] ) ? $others['key_pass'] : ''; | |
| 3080 | - $type = isset( $others['key_type'] ) ? intval( $others['key_type'] ) : 0; | |
| 4116 | + // Hash the consumer_secret with WordPress's password hasher so the | |
| 4117 | + // value at rest is no longer reversible by a DB-read primitive. | |
| 4118 | + // The plaintext is returned to the caller below so it can be shown | |
| 4119 | + // to the admin once at creation time. See MWP-1540. | |
| 4120 | + $hashed_secret = wp_hash_password( $consumer_secret ); | |
| 3081 | 4121 | |
| 3082 | 4122 | // Created API keys. |
| 3083 | - $permissions = in_array( $scope, array( 'read', 'write', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read'; | |
| 3084 | - $this->wpdb->insert( | |
| 4123 | + $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read'; | |
| 4124 | + $inserted = $this->wpdb->insert( | |
| 3085 | 4125 | $this->table_name( 'api_keys' ), |
| 3086 | 4126 | array( |
| 3087 | 4127 | 'user_id' => $user_id, |
| 3088 | 4128 | 'description' => $description, |
| @@ -3087,13 +4127,11 @@ | ||
| 3087 | 4127 | 'user_id' => $user_id, |
| 3088 | 4128 | 'description' => $description, |
| 3089 | 4129 | 'permissions' => $permissions, |
| 3090 | 4130 | 'consumer_key' => mainwp_api_hash( $consumer_key ), |
| 3091 | - 'consumer_secret' => $consumer_secret, | |
| 4131 | + 'consumer_secret' => $hashed_secret, | |
| 3092 | 4132 | 'truncated_key' => substr( $consumer_key, -7 ), |
| 3093 | 4133 | 'enabled' => $enabled, |
| 3094 | - 'key_pass' => $pass, | |
| 3095 | - 'key_type' => $type, | |
| 3096 | 4134 | ), |
| 3097 | 4135 | array( |
| 3098 | 4136 | '%d', |
| 3099 | 4137 | '%s', |
| @@ -3101,20 +4139,29 @@ | ||
| 3101 | 4139 | '%s', |
| 3102 | 4140 | '%s', |
| 3103 | 4141 | '%s', |
| 3104 | 4142 | '%d', |
| 3105 | - '%s', | |
| 3106 | - '%d', | |
| 3107 | 4143 | ), |
| 3108 | 4144 | ); |
| 3109 | 4145 | |
| 3110 | - return array( | |
| 3111 | - 'key_id' => $this->wpdb->insert_id, | |
| 3112 | - 'user_id' => $user_id, | |
| 3113 | - 'consumer_key' => $consumer_key, | |
| 3114 | - 'consumer_secret' => $consumer_secret, | |
| 3115 | - 'key_permissions' => $permissions, | |
| 3116 | - ); | |
| 4146 | + // wpdb->insert() returns false on failure. Without this guard the | |
| 4147 | + // function would still hand back the plaintext consumer_secret plus | |
| 4148 | + // $wpdb->insert_id, but that insert_id is the LAST successful insert | |
| 4149 | + // on the connection (typically from earlier in the same request), | |
| 4150 | + // not this row. The caller's empty-key_id check would pass and the | |
| 4151 | + // operator would receive a credential that was never persisted. | |
| 4152 | + // See MWP-1540 PR review feedback. | |
| 4153 | + if ( false === $inserted ) { | |
| 4154 | + return false; | |
| 4155 | + } | |
| 4156 | + | |
| 4157 | + return array( | |
| 4158 | + 'key_id' => $this->wpdb->insert_id, | |
| 4159 | + 'user_id' => $user_id, | |
| 4160 | + 'consumer_key' => $consumer_key, | |
| 4161 | + 'consumer_secret' => $consumer_secret, | |
| 4162 | + 'key_permissions' => $permissions, | |
| 4163 | + ); | |
| 3117 | 4164 | } |
| 3118 | 4165 | |
| 3119 | 4166 | /** |
| 3120 | 4167 | * Update rest api key. |
| @@ -3126,9 +4173,9 @@ | ||
| 3126 | 4173 | * |
| 3127 | 4174 | * @return array |
| 3128 | 4175 | */ |
| 3129 | 4176 | public function update_rest_api_key( $key_id, $scope, $description, $enabled = 1 ) { |
| 3130 | - $permissions = in_array( $scope, array( 'read', 'write', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read'; | |
| 4177 | + $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read'; | |
| 3131 | 4178 | return $this->wpdb->update( |
| 3132 | 4179 | $this->table_name( 'api_keys' ), |
| 3133 | 4180 | array( |
| 3134 | 4181 | 'description' => $description, |
| @@ -3147,9 +4194,10 @@ | ||
| 3147 | 4194 | * |
| 3148 | 4195 | * @return bool result. |
| 3149 | 4196 | */ |
| 3150 | 4197 | public function is_existed_enabled_rest_key() { |
| 3151 | - $enabled = $this->wpdb->get_row( 'SELECT * FROM ' . $this->table_name( 'api_keys' ) . ' WHERE enabled = 1 LIMIT 1' ); | |
| 4198 | + $table_name = esc_sql( $this->table_name( 'api_keys' ) ); | |
| 4199 | + $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. | |
| 3152 | 4200 | return $enabled ? true : false; |
| 3153 | 4201 | } |
| 3154 | 4202 | |
| 3155 | 4203 | /** |
| @@ -3159,9 +4207,10 @@ | ||
| 3159 | 4207 | * |
| 3160 | 4208 | * @return array |
| 3161 | 4209 | */ |
| 3162 | 4210 | public function get_rest_api_key_by( $id ) { |
| 3163 | - return $this->wpdb->get_row( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'api_keys' ) . ' WHERE key_id = %d ', $id ) ); | |
| 4211 | + $table_name = esc_sql( $this->table_name( 'api_keys' ) ); | |
| 4212 | + 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. | |
| 3164 | 4213 | } |
| 3165 | 4214 | |
| 3166 | 4215 | /** |
| 3167 | 4216 | * Method remove_rest_api_key(). |
| @@ -3170,9 +4219,10 @@ | ||
| 3170 | 4219 | * |
| 3171 | 4220 | * @return array |
| 3172 | 4221 | */ |
| 3173 | 4222 | public function remove_rest_api_key( $id ) { |
| 3174 | - return $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'api_keys' ) . ' WHERE key_id = %s', $id ) ); | |
| 4223 | + $table_name = esc_sql( $this->table_name( 'api_keys' ) ); | |
| 4224 | + 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. | |
| 3175 | 4225 | } |
| 3176 | 4226 | |
| 3177 | 4227 | /** |
| 3178 | 4228 | * Method get_rest_api_keys(). |
| @@ -3179,9 +4229,10 @@ | ||
| 3179 | 4229 | * |
| 3180 | 4230 | * @return array |
| 3181 | 4231 | */ |
| 3182 | 4232 | public function get_rest_api_keys() { |
| 3183 | - return $this->wpdb->get_results( 'SELECT * FROM ' . $this->table_name( 'api_keys' ) . ' ORDER BY key_id DESC' ); | |
| 4233 | + $table_name = esc_sql( $this->table_name( 'api_keys' ) ); | |
| 4234 | + 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. | |
| 3184 | 4235 | } |
| 3185 | 4236 | |
| 3186 | 4237 | |
| 3187 | 4238 | /** |
| @@ -3190,21 +4241,58 @@ | ||
| 3190 | 4241 | * @param array $data process data. |
| 3191 | 4242 | * @return mixed |
| 3192 | 4243 | */ |
| 3193 | 4244 | public function update_regular_process( $data ) { |
| 3194 | - if ( is_array( $data ) ) { | |
| 3195 | - if ( isset( $data['process_id'] ) ) { | |
| 3196 | - $process_id = $data['process_id']; | |
| 3197 | - unset( $data['process_id'] ); | |
| 3198 | - return $this->wpdb->update( $this->table_name( 'schedule_processes' ), $data, array( 'process_id' => $process_id ) ); | |
| 3199 | - } else { | |
| 3200 | - return $this->wpdb->insert( $this->table_name( 'schedule_processes' ), $data ); | |
| 4245 | + if ( isset( $data['process_id'] ) ) { | |
| 4246 | + $process_id = $data['process_id']; | |
| 4247 | + unset( $data['process_id'] ); | |
| 4248 | + return $this->wpdb->update( $this->table_name( 'schedule_processes' ), $data, array( 'process_id' => $process_id ) ); | |
| 4249 | + } elseif ( is_array( $data ) && isset( $data['type'] ) && isset( $data['process_slug'] ) ) { | |
| 4250 | + return $this->wpdb->insert( $this->table_name( 'schedule_processes' ), $data ); | |
| 4251 | + } | |
| 4252 | + return false; | |
| 4253 | + } | |
| 4254 | + | |
| 4255 | + /** | |
| 4256 | + * Delete regular process. | |
| 4257 | + * | |
| 4258 | + * @param int $process_id Process id. | |
| 4259 | + * @param int $item_id Item id. | |
| 4260 | + * @param string $pro_type Process type. | |
| 4261 | + * @param string $pro_slug Process slug. | |
| 4262 | + * | |
| 4263 | + * @return mixed | |
| 4264 | + */ | |
| 4265 | + public function delete_regular_process( $process_id = false, $item_id = false, $pro_type = false, $pro_slug = false ) { | |
| 4266 | + | |
| 4267 | + if ( is_numeric( $process_id ) && ! empty( $process_id ) ) { | |
| 4268 | + return $this->wpdb->delete( | |
| 4269 | + $this->table_name( 'schedule_processes' ), | |
| 4270 | + array( | |
| 4271 | + 'process_id' => $process_id, | |
| 4272 | + ) | |
| 4273 | + ); | |
| 4274 | + } elseif ( ! empty( $pro_type ) || ! empty( $pro_slug ) ) { | |
| 4275 | + | |
| 4276 | + $data = array(); | |
| 4277 | + | |
| 4278 | + if ( ! empty( $pro_type ) ) { | |
| 4279 | + $data['type'] = $pro_type; | |
| 3201 | 4280 | } |
| 4281 | + | |
| 4282 | + if ( ! empty( $pro_slug ) ) { | |
| 4283 | + $data['process_slug'] = $pro_slug; | |
| 4284 | + } | |
| 4285 | + | |
| 4286 | + if ( ! empty( $item_id ) ) { | |
| 4287 | + $data['item_id'] = $item_id; | |
| 4288 | + } | |
| 4289 | + // Bulk delete. | |
| 4290 | + return $this->wpdb->delete( $this->table_name( 'schedule_processes' ), $data ); | |
| 3202 | 4291 | } |
| 3203 | 4292 | return false; |
| 3204 | 4293 | } |
| 3205 | 4294 | |
| 3206 | - | |
| 3207 | 4295 | /** |
| 3208 | 4296 | * Method get_regular_process_by_item_id_type_slug |
| 3209 | 4297 | * |
| 3210 | 4298 | * @param integer $item_id item id. |
| @@ -3213,21 +4301,83 @@ | ||
| 3213 | 4301 | * |
| 3214 | 4302 | * @return mixed result |
| 3215 | 4303 | */ |
| 3216 | 4304 | public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) { |
| 3217 | - return $this->wpdb->get_row( $this->wpdb->prepare( ' SELECT pr.* FROM ' . $this->table_name( 'schedule_processes' ) . ' pr WHERE pr.item_id = %d AND pr.type = %s AND pr.process_slug = %s', $item_id, $type, $process_slug ) ); | |
| 4305 | + $table_name = esc_sql( $this->table_name( 'schedule_processes' ) ); | |
| 4306 | + 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. | |
| 3218 | 4307 | } |
| 3219 | 4308 | |
| 3220 | 4309 | /** |
| 3221 | - * Method log_system_query | |
| 4310 | + * Log SQL queries for debugging via hook. | |
| 3222 | 4311 | * |
| 3223 | - * @param array $params params. | |
| 3224 | - * @param string $sql query. | |
| 4312 | + * This method provides a structured way to log SQL queries for development/debugging. | |
| 4313 | + * It does NOT use error_log() directly - instead, it fires the `mainwp_log_system_query` | |
| 4314 | + * action hook, allowing external listeners (logging plugins, debug tools) to handle | |
| 4315 | + * the log output appropriately. | |
| 4316 | + * | |
| 4317 | + * To enable query logging: | |
| 4318 | + * 1. Set `$params['dev_log_query'] = 1` when calling database methods | |
| 4319 | + * 2. Add a listener to the `mainwp_log_system_query` action hook | |
| 4320 | + * | |
| 4321 | + * Example listener: | |
| 4322 | + * ```php | |
| 4323 | + * add_action( 'mainwp_log_system_query', function( $params, $sql, $caller ) { | |
| 4324 | + * error_log( 'MainWP Query: ' . $sql ); | |
| 4325 | + * }, 10, 3 ); | |
| 4326 | + * ``` | |
| 4327 | + * | |
| 4328 | + * @param array $params Query parameters. Set 'dev_log_query' to enable logging. | |
| 4329 | + * @param string $sql The SQL query string. | |
| 4330 | + * @param mixed $caller Instance of caller class (optional). | |
| 3225 | 4331 | * @return void |
| 3226 | 4332 | */ |
| 3227 | - public function log_system_query( $params, $sql ) { | |
| 4333 | + public function log_system_query( $params, $sql, $caller = false ) { | |
| 4334 | + $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller ); | |
| 3228 | 4335 | if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) { |
| 3229 | - error_log( $sql ); //phpcs:ignore -- NOSONAR - for dev. | |
| 3230 | - do_action( 'mainwp_log_system_query', $params, $sql ); | |
| 4336 | + do_action( 'mainwp_log_system_query', $params, $sql, $caller ); | |
| 3231 | 4337 | } |
| 4338 | + } | |
| 4339 | + | |
| 4340 | + /** | |
| 4341 | + * Method get_core_tables(). | |
| 4342 | + * | |
| 4343 | + * Returns the list of core tables used by MainWP for validation data integrity and other purposes. | |
| 4344 | + * | |
| 4345 | + * @return array Core tables info | |
| 4346 | + */ | |
| 4347 | + public function get_core_tables() { | |
| 4348 | + | |
| 4349 | + $core_tables = array( | |
| 4350 | + $this->table_name( 'wp_clients' ), | |
| 4351 | + $this->table_name( 'wp_clients_fields' ), | |
| 4352 | + $this->table_name( 'wp_clients_field_values' ), | |
| 4353 | + $this->table_name( 'wp_clients_contacts' ), | |
| 4354 | + $this->table_name( 'monitors' ), | |
| 4355 | + $this->table_name( 'monitor_heartbeat' ), | |
| 4356 | + $this->table_name( 'monitor_stat_hourly' ), | |
| 4357 | + $this->table_name( 'wp' ), | |
| 4358 | + $this->table_name( 'wp_sync' ), | |
| 4359 | + $this->table_name( 'wp_options' ), | |
| 4360 | + $this->table_name( 'wp_settings_backup' ), | |
| 4361 | + $this->table_name( 'users' ), | |
| 4362 | + $this->table_name( 'wp_status' ), | |
| 4363 | + $this->table_name( 'group' ), | |
| 4364 | + $this->table_name( 'wp_group' ), | |
| 4365 | + $this->table_name( 'lookup_item_objects' ), | |
| 4366 | + $this->table_name( 'wp_backup_progress' ), | |
| 4367 | + $this->table_name( 'wp_backup' ), | |
| 4368 | + $this->table_name( 'api_keys' ), | |
| 4369 | + $this->table_name( 'action_log' ), | |
| 4370 | + $this->table_name( 'request_log' ), | |
| 4371 | + $this->table_name( 'schedule_processes' ), | |
| 4372 | + $this->table_name( 'cost_tracker' ), | |
| 4373 | + ); | |
| 4374 | + | |
| 4375 | + if ( ! defined( 'MAINWP_MODULE_LOG_ENABLED' ) || MAINWP_MODULE_LOG_ENABLED ) { | |
| 4376 | + $core_tables[] = $this->table_name( 'wp_logs' ); | |
| 4377 | + $core_tables[] = $this->table_name( 'wp_logs_meta' ); | |
| 4378 | + $core_tables[] = $this->table_name( 'wp_logs_meta_archive' ); | |
| 4379 | + } | |
| 4380 | + | |
| 4381 | + return apply_filters( 'mainwp_database_core_tables', $core_tables ); | |
| 3232 | 4382 | } |
| 3233 | 4383 | } |