PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | class/class-mainwp-db.php +1580 -255 5.2.1 → 6.2.2 View file →
@@ -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,16 +83,47 @@
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 *
84 - * @param array $fields Extra option fields.
85 - * @param bool $default_value Whether or not to get default option fields.
118 + * @compatible function.
86 119 *
120 + * @param array $fields Extra option fields.
121 + * @param string $view_query view query.
122 + *
87 123 * @return array wp_options view.
88 124 */
89 - public function get_option_view( $fields = array(), $default_value = true ) {
125 + public function get_option_view( $fields = array(), $view_query = 'default' ) {
90 126
91 127 if ( ! is_array( $fields ) ) {
92 128 $fields = array();
93 129 }
@@ -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_value ) {
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',
@@ -281,8 +507,10 @@
281 507 'wp.backup_before_upgrade',
282 508 'wp.userid',
283 509 'wp.plugins',
284 510 'wp.themes',
511 + 'wp.offline_check_result', // 1 - online, -1 offline.
512 + 'wp.automatic_update',
285 513 );
286 514
287 515 $select = ' wp.*,wp_sync.* ';
288 516 if ( 'base_view' === $view ) {
@@ -298,35 +526,72 @@
298 526 );
299 527 $select = implode( ',', array_merge( $updates_fields, $base_fields ) );
300 528 }
301 529
302 - $select .= ',wp_optionview.* '; // to fix bug.
530 + $view_selects = '';
531 + $view_joins = '';
303 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 +
304 544 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
305 545 if ( $selectgroups ) {
306 - $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,
307 - ' . $select_clients . '
308 - 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
309 550 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
310 551 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
311 552 ' . $join_clients . '
312 553 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
313 - JOIN ' . $this->get_option_view_by( $view, $others_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid
314 - WHERE 1 ' . $where . $connected_sql . '
554 + ' . $view_joins . '
555 + WHERE 1 ' . $where . $connected_sql;
556 + $group_qry = '
315 557 GROUP BY wp.id, wp_sync.sync_id
316 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;
317 576 } else {
318 - $qry = 'SELECT ' . $select .
319 - $select_clients . '
320 - FROM ' . $this->table_name( 'wp' ) . ' wp
577 + $select_qry = 'SELECT ' . $select . $view_selects . $select_clients;
578 + $qry = ' FROM ' . $this->table_name( 'wp' ) . ' wp
321 579 ' . $join_clients . '
322 580 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
323 - JOIN ' . $this->get_option_view_by( $view, $others_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid
324 - WHERE 1 ' . $where . $connected_sql . '
581 + ' . $view_joins . '
582 + WHERE 1 ' . $where . $connected_sql;
583 + $group_qry = '
325 584 GROUP BY wp.id, wp_sync.sync_id
326 585 ORDER BY ' . $orderBy;
327 586 }
328 587
588 + if ( $count_sql ) {
589 + return 'SELECT COUNT(DISTINCT wp.id) ' . $qry;
590 + }
591 +
592 + $qry = $select_qry . $qry . $group_qry;
593 +
329 594 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
330 595 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
331 596 } elseif ( false !== $rowcount ) {
332 597 $qry .= ' LIMIT ' . $rowcount;
@@ -342,14 +607,96 @@
342 607 $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites );
343 608 }
344 609 }
345 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 ) );
346 615 return $qry;
347 616 }
348 617
349 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 + /**
350 695 * Get wp_options database table view.
351 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 + *
352 699 * @param array $view Option view.
353 700 * @param array $other_fields Extra option fields.
354 701 *
355 702 * @return array wp_options view.
@@ -364,16 +711,23 @@
364 711 'added_timestamp',
365 712 'wp_upgrades',
366 713 );
367 714
715 + $fields = array();
716 +
368 717 if ( 'updates_view' === $view ) {
369 718 $fields = array(
370 719 'wp_upgrades',
371 720 'ignored_wp_upgrades',
721 + 'ignored_trans_updates',
372 722 );
373 - } elseif ( 'base_view' === $view ) {
723 + } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) {
374 724 $fields = array();
375 - } else {
725 + if ( 'monitor_view' === $view || 'ping_view' === $view ) {
726 + $fields[] = 'health_site_status';
727 + $fields[] = 'bypass_cache';
728 + }
729 + } elseif ( 'custom_view' !== $view ) {
376 730 $fields = $default;
377 731 }
378 732
379 733 if ( is_array( $other_fields ) && ! empty( $other_fields ) ) {
@@ -379,17 +733,21 @@
379 733 if ( is_array( $other_fields ) && ! empty( $other_fields ) ) {
380 734 $fields = array_unique( array_merge( $fields, $other_fields ) );
381 735 }
382 736
383 - $view = '(SELECT intwp.id AS wpid ';
737 + $view_query = '(SELECT wpid ';
384 738
385 - if ( ! in_array( 'signature_algo', $fields ) ) {
386 - $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 + }
387 747 }
388 748
389 - if ( ! in_array( 'verify_method', $fields ) ) {
390 - $fields[] = 'verify_method';
391 - }
749 + $fields = $this->filter_safe_option_names( $fields );
392 750
393 751 foreach ( $fields as $field ) {
394 752
395 753 if ( empty( $field ) ) {
@@ -395,15 +753,17 @@
395 753 if ( empty( $field ) ) {
396 754 continue;
397 755 }
398 756
399 - $view .= ', ';
400 - $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 );
757 + $view_query .= ', ';
758 + $view_query .= 'MAX(CASE WHEN name = "' . $this->escape( $field ) . '" THEN value END) AS `' . $this->escape( $field ) . '`';
401 759 }
402 760
403 - $view .= ' 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 ) ";
404 764
405 - return $view;
765 + return $view_query;
406 766 }
407 767
408 768 /**
409 769 * Method get_select_groups_belong().
@@ -427,18 +787,20 @@
427 787 *
428 788 * @return array $connected_sites Array of connected sites.
429 789 */
430 790 public function get_connected_websites( $sites_ids = false ) {
431 - $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' ) );
432 794
433 - $sql = 'SELECT wp.*,wp_sync.*
434 - FROM ' . $this->table_name( 'wp' ) . ' wp
435 - 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
436 798 ON wp.id = wp_sync.wpid
437 - 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 = \"\") " .
438 800 $where;
439 801
440 - $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()
441 803 $connected_sites = array();
442 804 if ( $websites ) {
443 805 foreach ( $websites as $website ) {
444 806
@@ -463,18 +825,20 @@
463 825 *
464 826 * @return array $disc_sites Array of disonnected sites.
465 827 */
466 828 public function get_disconnected_websites( $sites_ids = false ) {
467 - $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' ) );
468 832
469 - $sql = 'SELECT wp.*,wp_sync.*
470 - FROM ' . $this->table_name( 'wp' ) . ' wp
471 - 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
472 836 ON wp.id = wp_sync.wpid
473 - 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 <> \"\") " .
474 838 $where;
475 839
476 - $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()
477 841 $disc_sites = array();
478 842 if ( $websites ) {
479 843 foreach ( $websites as $website ) {
480 844
@@ -500,8 +864,12 @@
500 864 *
501 865 * @return int Child site count.
502 866 *
503 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.
504 872 */
505 873 public function get_websites_count( $userId = null, $all_access = false ) {
506 874 static $total_sites;
507 875 if ( null !== $total_sites ) { // NOSONAR -- static value.
@@ -517,15 +885,16 @@
517 885 global $current_user;
518 886
519 887 $userId = $current_user->ID;
520 888 }
521 - $where = ( null === $userId ? '' : ' wp.userid = ' . $userId );
889 + $where = ( null === $userId ? '' : ' wp.userid = ' . intval( $userId ) );
522 890 if ( ! $all_access ) {
523 891 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
524 892 }
525 - $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}";
526 895
527 - $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()
528 897 $total_sites = $total;// NOSONAR -- static value.
529 898 return $total;
530 899 }
531 900
@@ -553,9 +922,9 @@
553 922
554 923 $select_stats = ' ( SELECT COUNT(wp.id) as count_all ';
555 924 if ( ! empty( $params['count_disconnected'] ) ) {
556 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 ';
557 - $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 ';
558 927 }
559 928 if ( ! empty( $params['count_suspended'] ) ) {
560 929 $select_stats .= ',( SELECT COUNT(wp_suspended.id) FROM ' . $this->table_name( 'wp' ) . ' wp_suspended WHERE wp_suspended.suspended = 1 ) as count_suspended ';
561 930 }
@@ -604,9 +973,10 @@
604 973 } else {
605 974 return false;
606 975 }
607 976
608 - $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.
609 979
610 980 if ( null === $value && null !== $default_value ) {
611 981 return $default_value;
612 982 }
@@ -682,13 +1052,12 @@
682 1052 if ( empty( $get_options ) ) {
683 1053 return $arr_options; // all options.
684 1054 }
685 1055
686 - $options_name = implode( "','", $get_options );
687 - $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.
688 1059
689 - $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 ) );
690 -
691 1060 $fill_options = array(
692 1061 'primary_lasttime_backup',
693 1062 );
694 1063
@@ -723,9 +1092,10 @@
723 1092 } else {
724 1093 $site_id = $website->id;
725 1094 }
726 1095
727 - $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.
728 1098 if ( empty( $rslt ) ) {
729 1099 $this->wpdb->insert(
730 1100 $this->table_name( 'wp_options' ),
731 1101 array(
@@ -747,8 +1117,37 @@
747 1117 }
748 1118
749 1119
750 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 + /**
751 1150 * Get general Child site option.
752 1151 *
753 1152 * @param mixed $option Child Site option name.
754 1153 *
@@ -763,9 +1162,10 @@
763 1162 } else {
764 1163 static::$general_options[] = array();
765 1164 }
766 1165
767 - $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.
768 1168
769 1169 static::$general_options[ $option ] = $val;
770 1170 return $val;
771 1171 }
@@ -800,12 +1200,15 @@
800 1200 $diff_options[] = $opt;
801 1201 }
802 1202 }
803 1203
804 - $options_name = implode( "','", $diff_options );
805 - $options_name = "'" . $options_name . "'";
1204 + if ( empty( $diff_options ) ) {
1205 + return $return_options;
1206 + }
806 1207
807 - $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.
808 1211
809 1212 foreach ( (array) $options_db as $o ) {
810 1213 $return_options[ $o->name ] = $o->value;
811 1214 static::$general_options[ $o->name ] = $o->value;
@@ -835,9 +1238,10 @@
835 1238 static::$general_options[] = array();
836 1239 }
837 1240 static::$general_options[ $option ] = $value;
838 1241
839 - $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.
840 1244
841 1245 if ( empty( $rslt ) ) {
842 1246 $this->wpdb->insert(
843 1247 $this->table_name( 'wp_options' ),
@@ -902,48 +1306,23 @@
902 1306 */
903 1307 public function get_sql_websites() {
904 1308 $where = $this->get_sql_where_allow_access_sites( 'wp' );
905 1309
906 - return 'SELECT wp.*,wp_sync.*,wp_optionview.*
907 - FROM ' . $this->table_name( 'wp' ) . ' wp
908 - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
909 - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid
910 - WHERE 1 ' . $where;
911 - }
1310 + $view_selects = '';
1311 + $view_joins = '';
912 1312
913 - /**
914 - * Get child sites to run the status check process.
915 - *
916 - * @param int $last_check Time of the last check.
917 - * @param int $count Number of websites.
918 - *
919 - * @return string SQL string.
920 - */
921 - public function get_sql_websites_to_check_status( $last_check, $count = 20 ) {
922 - $where = $this->get_sql_where_allow_access_sites( 'wp' );
923 - return 'SELECT wp.*
924 - FROM ' . $this->table_name( 'wp' ) . ' wp
925 - WHERE wp.disable_status_check <> 1 AND ( wp.status_check_interval = 0 AND wp.offline_checks_last < ' . intval( $last_check ) . ' )' .
926 - $where . '
927 - LIMIT ' . intval( $count );
928 - }
1313 + $opts_view = $this->get_wp_options_join();
929 1314
1315 + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
1316 + $view_selects = ',' . $opts_view['selects'];
1317 + $view_joins = $opts_view['joins'];
1318 + }
930 1319
931 - /**
932 - * Get child sites to run the status individual check process.
933 - *
934 - * @param int $count Number of websites.
935 - *
936 - * @return string SQL string.
937 - */
938 - public function get_sql_websites_to_check_individual_status( $count = 20 ) {
939 - $where = $this->get_sql_where_allow_access_sites( 'wp' );
940 - $time = time();
941 - return 'SELECT wp.*
1320 + return 'SELECT wp.*,wp_sync.*' . $view_selects . '
942 1321 FROM ' . $this->table_name( 'wp' ) . ' wp
943 - WHERE wp.disable_status_check <> 1 AND ( wp.status_check_interval <> 0 AND ( wp.offline_checks_last + wp.status_check_interval * 60 < ' . intval( $time ) . ' ) )' .
944 - $where . '
945 - LIMIT ' . intval( $count );
1322 + JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1323 + ' . $view_joins . '
1324 + WHERE 1 ' . $where . ' ORDER BY wp.id';
946 1325 }
947 1326
948 1327 /**
949 1328 * Get child sites by user id via SQL.
@@ -968,24 +1347,34 @@
968 1347 }
969 1348
970 1349 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
971 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 +
972 1361 if ( $selectgroups ) {
973 - $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
974 1363 FROM ' . $this->table_name( 'wp' ) . ' wp
975 1364 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
976 1365 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
977 1366 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
978 - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid
1367 + ' . $view_joins . '
979 1368 WHERE wp.userid = ' . $userid . "
980 1369 $where
981 1370 GROUP BY wp.id, wp_sync.sync_id
982 1371 ORDER BY " . $orderBy;
983 1372 } else {
984 - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.*
1373 + $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . '
985 1374 FROM ' . $this->table_name( 'wp' ) . ' wp
986 1375 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
987 - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid
1376 + ' . $view_joins . '
988 1377 WHERE wp.userid = ' . $userid . "
989 1378 $where
990 1379 ORDER BY " . $orderBy;
991 1380 }
@@ -1032,8 +1421,15 @@
1032 1421 $is_staging = 'no',
1033 1422 $params = array()
1034 1423 ) {
1035 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 +
1036 1432 $where = '';
1037 1433 if ( MainWP_System::instance()->is_multi_user() ) {
1038 1434
1039 1435 /**
@@ -1047,9 +1443,15 @@
1047 1443 }
1048 1444
1049 1445 if ( null !== $search_site ) {
1050 1446 $search_site = trim( $search_site );
1051 - $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 + );
1052 1454 }
1053 1455
1054 1456 if ( ! empty( $extraWhere ) ) {
1055 1457 $where .= ' AND ' . $extraWhere . ' ';
@@ -1066,8 +1468,10 @@
1066 1468 } elseif ( is_array( $params ) && isset( $params['connected'] ) && 'no' === $params['connected'] ) {
1067 1469 $connected_sql = ' AND wp_sync.sync_errors <> "" ';
1068 1470 }
1069 1471
1472 + $use_comp_subquery = false;
1473 +
1070 1474 $limit = '';
1071 1475 if ( $params && is_array( $params ) ) {
1072 1476 $s = isset( $params['s'] ) ? $params['s'] : '';
1073 1477 $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
@@ -1075,10 +1479,19 @@
1075 1479 $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array();
1076 1480 $page = isset( $params['page'] ) ? intval( $params['page'] ) : false;
1077 1481 $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false;
1078 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 +
1079 1486 if ( ! empty( $s ) ) {
1080 - $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}' ) ";
1081 1494 }
1082 1495
1083 1496 if ( ! empty( $exclude ) ) {
1084 1497 $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') ';
@@ -1087,14 +1500,19 @@
1087 1500 if ( ! empty( $include ) ) {
1088 1501 $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') ';
1089 1502 }
1090 1503
1504 + if ( ! empty( $_included_cache_ids ) ) {
1505 + $where .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') ';
1506 + }
1507 +
1091 1508 // any, connected, disconnected, suspended, available_update.
1092 1509 if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) {
1093 1510 $status_conds = array();
1094 1511 if ( in_array( 'available_update', $status ) ) {
1095 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 <> '[]' ) ";
1096 - $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.
1097 1515 if ( $results ) {
1098 1516 $wp_ids = array();
1099 1517 foreach ( $results as $item ) {
1100 1518 if ( ! empty( $item->wpid ) ) {
@@ -1109,9 +1527,9 @@
1109 1527 $status_conds[] = ' ( ' . $available_sql . ') ';
1110 1528 }
1111 1529
1112 1530 if ( in_array( 'connected', $status ) ) {
1113 - $status_conds[] = ' ( wp_sync.sync_errors == "" ) ';
1531 + $status_conds[] = ' ( wp_sync.sync_errors = "" ) ';
1114 1532 }
1115 1533 if ( in_array( 'disconnected', $status ) ) {
1116 1534 $status_conds[] = " wp_sync.sync_errors <> '' ";
1117 1535 }
@@ -1122,13 +1540,18 @@
1122 1540
1123 1541 if ( ! empty( $status_conds ) ) {
1124 1542 $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) ';
1125 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 + }
1126 1548 }
1127 1549
1128 1550 if ( ! empty( $page ) && ! empty( $per_page ) ) {
1129 1551 $limit = ( $page - 1 ) * $per_page . ',' . $per_page;
1130 1552 }
1553 + $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
1131 1554 }
1132 1555
1133 1556 if ( 'wp.url' === $orderBy ) {
1134 1557 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
@@ -1133,11 +1556,26 @@
1133 1556 if ( 'wp.url' === $orderBy ) {
1134 1557 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
1135 1558 }
1136 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 +
1137 1575 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1138 1576 if ( $selectgroups ) {
1139 - $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,
1140 1578 wpclient.name as client_name
1141 1579 FROM ' . $this->table_name( 'wp' ) . ' wp
1142 1580 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1143 1581 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
@@ -1142,18 +1580,18 @@
1142 1580 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1143 1581 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1144 1582 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1145 1583 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1146 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
1584 + ' . $view_joins . '
1147 1585 WHERE 1 ' . $where . $connected_sql . '
1148 1586 GROUP BY wp.id, wp_sync.sync_id
1149 1587 ORDER BY ' . $orderBy;
1150 1588 } else {
1151 - $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
1152 1590 FROM ' . $this->table_name( 'wp' ) . ' wp
1153 1591 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1154 1592 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1155 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
1593 + ' . $view_joins . '
1156 1594 WHERE 1 ' . $where . $connected_sql . '
1157 1595 GROUP BY wp.id, wp_sync.sync_id
1158 1596 ORDER BY ' . $orderBy;
1159 1597 }
@@ -1174,13 +1612,17 @@
1174 1612 $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites );
1175 1613 }
1176 1614 }
1177 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 +
1178 1621 return $qry;
1179 1622 }
1180 1623
1181 1624
1182 -
1183 1625 /**
1184 1626 * Get SQL to get wp child sites for current user.
1185 1627 *
1186 1628 * @since 4.3
@@ -1193,17 +1635,18 @@
1193 1635 if ( ! is_array( $params ) ) {
1194 1636 $params = array();
1195 1637 }
1196 1638
1197 - $selectgroups = ! empty( $params['select_groups'] ) ? true : false;
1198 - $search_site = isset( $params['search_site'] ) && ! empty( $params['search_site'] ) ? $params['search_site'] : null;
1199 - $orderBy = isset( $params['order_by'] ) && ! empty( $params['order_by'] ) ? $params['order_by'] : 'wp.url';
1200 - $offset = isset( $params['offset'] ) ? $params['offset'] : false;
1201 - $rowcount = isset( $params['row_count'] ) ? $params['row_count'] : false;
1202 - $for_manager = isset( $params['for_manager'] ) ? $params['for_manager'] : false;
1203 - $extraWhere = isset( $params['extra_where'] ) && ! empty( $params['extra_where'] ) ? $params['extra_where'] : null;
1204 - $extra_view = isset( $params['extra_view'] ) && is_array( $params['extra_view'] ) && ! empty( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
1205 - $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;
1206 1649
1207 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();
1208 1651 $extra_select_sql_fields = isset( $params['extra_select_sql_fields'] ) && ! empty( $params['extra_select_sql_fields'] ) ? $params['extra_select_sql_fields'] : '';
1209 1652
@@ -1213,9 +1656,15 @@
1213 1656 $where = '';
1214 1657
1215 1658 if ( null !== $search_site ) {
1216 1659 $search_site = trim( $search_site );
1217 - $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 + );
1218 1667 }
1219 1668
1220 1669 if ( null !== $extraWhere ) {
1221 1670 $where .= ' AND ' . $extraWhere;
@@ -1234,8 +1683,22 @@
1234 1683 if ( ! empty( $extra_select_sql_fields ) ) {
1235 1684 $extra_select_sql_fields = ',' . $extra_select_sql_fields;
1236 1685 }
1237 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 +
1238 1701 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1239 1702 if ( $selectgroups ) {
1240 1703 if ( $count_only ) {
1241 1704 $select = ' COUNT(DISTINCT(wp.id)) ';
@@ -1241,9 +1704,9 @@
1241 1704 $select = ' COUNT(DISTINCT(wp.id)) ';
1242 1705 } else {
1243 1706 $select = $select_wp_fields . '
1244 1707 ' . $extra_select_sql_fields . '
1245 - ,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 ';
1246 1709 }
1247 1710 $qry = 'SELECT ' . $select . '
1248 1711 FROM ' . $this->table_name( 'wp' ) . ' wp
1249 1712 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
@@ -1249,10 +1712,10 @@
1249 1712 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1250 1713 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1251 1714 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1252 1715 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1253 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid ' .
1254 - $extra_join . '
1716 + ' . $view_joins . '
1717 + ' . $extra_join . '
1255 1718 WHERE 1 ' . $where;
1256 1719 if ( ! $count_only ) {
1257 1720 $qry .= ' GROUP BY wp.id, wp_sync.sync_id';
1258 1721 }
@@ -1262,16 +1725,16 @@
1262 1725 $select = ' COUNT(DISTINCT(wp.id)) ';
1263 1726 } else {
1264 1727 $select = $select_wp_fields . '
1265 1728 ' . $extra_select_sql_fields . '
1266 - ,wp_sync.sync_errors,wp_optionview.*, wpclient.name as client_name ';
1729 + ,wp_sync.sync_errors' . $view_selects . ', wpclient.name as client_name ';
1267 1730 }
1268 1731 $qry = 'SELECT ' . $select . '
1269 1732 FROM ' . $this->table_name( 'wp' ) . ' wp
1270 1733 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1271 1734 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1272 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid ' .
1273 - $extra_join . '
1735 + ' . $view_joins . '
1736 + ' . $extra_join . '
1274 1737 WHERE 1 ' . $where;
1275 1738 if ( ! $count_only ) {
1276 1739 $qry .= ' GROUP BY wp.id, wp_sync.sync_id';
1277 1740 }
@@ -1299,13 +1762,11 @@
1299 1762 public function get_sql_select_wp_valid_fields( $other_fields = array() ) {
1300 1763
1301 1764 $allow_other_fields = array(
1302 1765 'offline_checks_last',
1303 - 'offline_check_result',
1766 + 'offline_check_result', // 1 - online, -1 offline.
1304 1767 'http_response_code',
1305 - 'disable_status_check',
1306 1768 'disable_health_check',
1307 - 'status_check_interval',
1308 1769 'health_threshold',
1309 1770 'note',
1310 1771 'statsUpdate',
1311 1772 'directories',
@@ -1384,9 +1845,9 @@
1384 1845 $format = isset( $params['format'] ) ? $params['format'] : '';
1385 1846 $clients = isset( $params['client'] ) ? $params['client'] : '';
1386 1847 $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array();
1387 1848
1388 - $for_manager = false;
1849 + $for_manager = isset( $params['no_perm_check'] ) && true === $params['no_perm_check'];
1389 1850
1390 1851 $urlsWhere = '';
1391 1852
1392 1853 if ( isset( $params['urls'] ) && ! empty( $params['urls'] ) ) {
@@ -1447,13 +1908,11 @@
1447 1908 'id',
1448 1909 'url',
1449 1910 'name',
1450 1911 'offline_checks_last',
1451 - 'offline_check_result',
1912 + 'offline_check_result', // 1 - online, -1 offline.
1452 1913 'http_response_code',
1453 - 'disable_status_check',
1454 1914 'disable_health_check',
1455 - 'status_check_interval',
1456 1915 'health_threshold',
1457 1916 'note',
1458 1917 'dbsize',
1459 1918 'plugin_upgrades',
@@ -1503,9 +1962,12 @@
1503 1962 $data = array_unique( array_merge( $fields, $data ) ); // to prevent difference fields name.
1504 1963 }
1505 1964
1506 1965 $dbwebsites = array();
1507 - $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 +
1508 1970 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
1509 1971
1510 1972 $obj_data = MainWP_Utility::map_site( $website, $data );
1511 1973
@@ -1510,9 +1972,12 @@
1510 1972 $obj_data = MainWP_Utility::map_site( $website, $data );
1511 1973
1512 1974 if ( $full_data ) {
1513 1975 $sum_upgrades = 0;
1514 - 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 ) ) {
1515 1980 $plugin_upgrades = json_decode( $obj_data->plugin_upgrades, true );
1516 1981 if ( is_array( $plugin_upgrades ) ) {
1517 1982 $sum_upgrades += count( $plugin_upgrades );
1518 1983 }
@@ -1517,9 +1982,9 @@
1517 1982 $sum_upgrades += count( $plugin_upgrades );
1518 1983 }
1519 1984 }
1520 1985
1521 - if ( '' !== $obj_data->theme_upgrades ) {
1986 + if ( ! empty( $obj_data->theme_upgrades ) ) {
1522 1987 $theme_upgrades = json_decode( $obj_data->theme_upgrades, true );
1523 1988 if ( is_array( $theme_upgrades ) ) {
1524 1989 $sum_upgrades += count( $theme_upgrades );
1525 1990 }
@@ -1524,9 +1989,9 @@
1524 1989 $sum_upgrades += count( $theme_upgrades );
1525 1990 }
1526 1991 }
1527 1992
1528 - if ( '' !== $obj_data->wp_upgrades ) {
1993 + if ( ! empty( $obj_data->wp_upgrades ) ) {
1529 1994 $wp_upgrades = json_decode( $obj_data->wp_upgrades, true );
1530 1995 if ( is_array( $wp_upgrades ) ) {
1531 1996 $sum_upgrades += count( $wp_upgrades );
1532 1997 }
@@ -1544,8 +2009,126 @@
1544 2009 return $dbwebsites;
1545 2010 }
1546 2011
1547 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 + /**
1548 2131 * Get the child sites the current user has searched for.
1549 2132 *
1550 2133 * @param array $params Query parameters.
1551 2134 *
@@ -1558,14 +2141,15 @@
1558 2141 if ( ! is_array( $params ) ) {
1559 2142 $params = array();
1560 2143 }
1561 2144
2145 + $view = isset( $params['view'] ) ? $params['view'] : 'default'; // must be default.
1562 2146 $selectgroups = isset( $params['selectgroups'] ) && $params['selectgroups'] ? true : false;
1563 - $search_site = isset( $params['search'] ) ? $this->escape( trim( $params['search'] ) ) : null;
2147 + $search_site = isset( $params['search'] ) ? trim( $params['search'] ) : null;
1564 2148 $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url';
1565 2149 $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false;
1566 2150 $rowcount = isset( $params['rowcount'] ) ? intval( $params['rowcount'] ) : false;
1567 - $extraWhere = isset( $params['extra_where'] ) ? $params['extra_where'] : null;
2151 + $extraWhere = isset( $params['extra_where'] ) ? $params['extra_where'] : null; // without AND prefix.
1568 2152 $for_manager = isset( $params['for_manager'] ) && $params['for_manager'] ? true : false;
1569 2153 $extra_view = isset( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
1570 2154 $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no';
1571 2155 $is_count = isset( $params['count_only'] ) && $params['count_only'] ? true : false;
@@ -1570,11 +2154,16 @@
1570 2154 $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no';
1571 2155 $is_count = isset( $params['count_only'] ) && $params['count_only'] ? true : false;
1572 2156 $group_ids = isset( $params['group_id'] ) && ! empty( $params['group_id'] ) ? $params['group_id'] : array();
1573 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';
1574 2159 $is_not = isset( $params['isnot'] ) && ! empty( $params['isnot'] ) ? true : false;
1575 2160 $selected_sites = isset( $params['selected_sites'] ) ? $params['selected_sites'] : array();
1576 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 +
1577 2166 if ( ! is_array( $group_ids ) ) {
1578 2167 $group_ids = array();
1579 2168 }
1580 2169
@@ -1632,19 +2221,46 @@
1632 2221
1633 2222 $where .= ' AND wp.userid = ' . $current_user->ID . ' ';
1634 2223 }
1635 2224
1636 - if ( ! empty( $selected_sites ) ) {
1637 - $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 + }
1638 2235 }
1639 2236
1640 - // for searching.
2237 + // Search filtering.
1641 2238 if ( null !== $search_site && '' !== $search_site ) {
1642 - $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 + }
1643 2258 }
1644 2259
1645 - if ( null !== $extraWhere ) {
1646 - $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';
1647 2263 }
1648 2264
1649 2265 if ( ! $for_manager ) {
1650 2266 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
@@ -1659,10 +2275,11 @@
1659 2275 if ( ! empty( $orderBy ) ) {
1660 2276 $orderBy = ' ORDER BY ' . $orderBy;
1661 2277 }
1662 2278
1663 - $join_group = '';
1664 - $where_group = '';
2279 + $join_group = '';
2280 + $where_group = '';
2281 + $having_group = '';
1665 2282
1666 2283 if ( in_array( 'nogroups', $group_ids ) ) {
1667 2284 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
1668 2285 $group_ids = array_filter(
@@ -1671,14 +2288,21 @@
1671 2288 return 'nogroups' !== $e;
1672 2289 }
1673 2290 );
1674 2291 if ( ! empty( $group_ids ) ) {
1675 - $groups = implode( ',', $group_ids );
2292 + $groups = implode( ',', $group_ids );
2293 + $groups_count = count( $group_ids );
1676 2294 if ( $is_not ) {
1677 - $where_group = ' AND wpgroup.groupid IS NOT NULL AND wpgroup.groupid NOT IN (' . $groups . ') ';
1678 - // to fix.
1679 - $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 . ') ';
1680 - $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 ';
1681 2305 } else {
1682 2306 $where_group = ' AND ( wpgroup.groupid IS NULL OR wpgroup.groupid IN (' . $groups . ') ) ';
1683 2307 }
1684 2308 } elseif ( $is_not ) {
@@ -1686,18 +2310,26 @@
1686 2310 } else {
1687 2311 $where_group = ' AND wpgroup.groupid IS NULL ';
1688 2312 }
1689 2313 } elseif ( $group_ids ) {
1690 - $groups = implode( ',', $group_ids );
2314 + $groups = implode( ',', $group_ids );
2315 + $groups_count = count( $group_ids );
1691 2316 if ( $is_not ) {
1692 2317 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
1693 - $where_group = ' AND ( wpgroup.groupid NOT IN (' . $groups . ') OR wpgroup.groupid IS NULL ) ';
1694 - // to fix.
1695 - $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 . ') ';
1696 - $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 + }
1697 2326 } else {
1698 2327 $join_group = ' JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
1699 2328 $where_group = ' AND wpgroup.groupid IN (' . $groups . ') ';
2329 + if ( 'and' === $group_logic ) {
2330 + $having_group = 'COUNT(DISTINCT wpgroup.groupid) = ' . $groups_count;
2331 + }
1700 2332 }
1701 2333 }
1702 2334
1703 2335 $select_groups_belong = '';
@@ -1707,9 +2339,16 @@
1707 2339 }
1708 2340
1709 2341 $join_client = '';
1710 2342 $where_client = '';
1711 -
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 + }
1712 2351 if ( in_array( 'noclients', $client_ids ) ) {
1713 2352 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
1714 2353 $client_ids = array_filter(
1715 2354 $client_ids,
@@ -1742,8 +2381,89 @@
1742 2381
1743 2382 if ( '' === $join_client ) {
1744 2383 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
1745 2384 }
2385 +
2386 + $light_fields = array(
2387 + 'wp.id',
2388 + 'wp.url',
2389 + 'wp.name',
2390 + 'wp.client_id',
2391 + 'wp.verify_certificate',
2392 + 'wp.http_user',
2393 + 'wp.http_pass',
2394 + 'wp.ssl_version',
2395 + 'wp.adminname',
2396 + 'wp.privkey',
2397 + 'wp.pubkey',
2398 + 'wp.wpe',
2399 + 'wp.is_staging',
2400 + 'wp.force_use_ipv4',
2401 + 'wp.siteurl',
2402 + 'wp.suspended',
2403 + 'wp.mainwpdir',
2404 + 'wp.is_ignoreCoreUpdates',
2405 + 'wp.is_ignorePluginUpdates',
2406 + 'wp.is_ignoreThemeUpdates',
2407 + 'wp.backup_before_upgrade',
2408 + 'wp.userid',
2409 + 'wp_sync.sync_errors',
2410 + );
2411 +
2412 + $legacy_status_fields = array(
2413 + 'wp.offline_check_result', // 1 - online, -1 offline.
2414 + 'wp.http_response_code',
2415 + 'wp.offline_checks_last',
2416 + );
2417 +
2418 + $light_fields = array_merge( $light_fields, $legacy_status_fields );
2419 +
2420 + $join_monitors = '';
2421 +
2422 + $select_fields = array(
2423 + 'wp.*',
2424 + 'wp_sync.*',
2425 + );
2426 +
2427 + if ( 'light_view' === $view ) {
2428 + $select_fields = $light_fields;
2429 + } elseif ( 'monitor_view' === $view ) {
2430 + $select_fields = $light_fields;
2431 + $select_fields[] = 'mo.*';
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 +
2452 + }
2453 +
2454 + $select = implode( ',', $select_fields );
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 +
1746 2466 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1747 2467 if ( $selectgroups ) {
1748 2468
1749 2469 if ( empty( $join_group ) ) {
@@ -1749,38 +2469,45 @@
1749 2469 if ( empty( $join_group ) ) {
1750 2470 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
1751 2471 }
1752 2472
1753 - $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, 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 ' .
1754 2474 $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' .
1755 2475 $join_client . ' ' .
1756 - $join_group . '
2476 + $join_group .
2477 + $join_monitors . '
1757 2478 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgroup.groupid = gr.id
1758 2479
1759 2480 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1760 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
1761 - WHERE 1 ' . $where . $where_group . $where_client . '
1762 - GROUP BY wp.id, wp_sync.sync_id ' .
2481 + ' . $view_joins . '
2482 + WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by .
1763 2483 $orderBy;
1764 2484 } else {
1765 - $qry = 'SELECT wp.*,wp_sync.*,wp_optionview.*, wpclient.name as client_name ' .
2485 + $qry = 'SELECT ' . $select . $view_selects . ', wpclient.name as client_name ' .
1766 2486 $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' .
1767 2487 $join_group . ' ' .
1768 - $join_client . '
2488 + $join_client .
2489 + $join_monitors . '
1769 2490 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1770 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
1771 - WHERE 1 ' . $where . $where_group . $where_client . '
1772 - GROUP BY wp.id, wp_sync.sync_id ' .
2491 + ' . $view_joins . '
2492 + WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by .
1773 2493 $orderBy;
1774 2494 }
1775 2495
1776 2496 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1777 - $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;
1778 2501 } elseif ( false !== $rowcount ) {
1779 2502 $qry .= ' LIMIT ' . $rowcount;
1780 2503 }
1781 - //phpcs:ignore
1782 - // error_log( $qry ); // NOSONAR - for dev.
2504 +
2505 + if ( ! empty( $_included_cache_ids ) ) {
2506 + MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql search websites=%s]', $qry ) );
2507 + }
2508 + MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql search websites=%s]', $qry ) );
2509 +
1783 2510 return $qry;
1784 2511 }
1785 2512
1786 2513 /**
@@ -1923,19 +2650,98 @@
1923 2650 return ' AND 0 ';
1924 2651 }
1925 2652 }
1926 2653
2654 +
1927 2655 /**
2656 + * Get child site by id and params.
2657 + *
2658 + * @param int $id Child site ID.
2659 + * @param array $params params.
2660 + * @param string $obj OBJECT|ARRAY_A.
2661 + *
2662 + * @return object|null Database query results or null on failure.
2663 + */
2664 + public function get_website_by_id_params( $id, $params = array(), $obj = OBJECT ) {
2665 + return $this->get_row_result( $this->get_sql_website_by_params( $id, $params ), $obj );
2666 + }
2667 +
2668 + /**
2669 + * Get sql child site by id and params.
2670 + *
2671 + * @param int $id Child site ID.
2672 + * @param array $params params.
2673 + *
2674 + * @return object|null Database query results or null on failure.
2675 + */
2676 + public function get_sql_website_by_params( $id, $params = array() ) {
2677 +
2678 + if ( ! is_array( $params ) ) {
2679 + $params = array();
2680 + }
2681 +
2682 + $select_groups = ! empty( $params['select_groups'] ) ? true : false;
2683 +
2684 + $view = ! empty( $params['view'] ) ? $params['view'] : 'simple_view';
2685 + $view_fields = isset( $params['view_fields'] ) ? $params['view_fields'] : array();
2686 +
2687 + if ( is_string( $view_fields ) ) {
2688 + $view_fields = (array) $view_fields;
2689 + } elseif ( ! is_array( $view_fields ) ) {
2690 + $view_fields = array();
2691 + }
2692 +
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 +
2711 + $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' );
2712 + if ( $select_groups ) {
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
2714 + FROM ' . $this->table_name( 'wp' ) . ' wp
2715 + LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
2716 + LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
2717 + JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2718 + ' . $view_joins . '
2719 + WHERE wp.id = ' . $id . $where . '
2720 + GROUP BY wp.id, wp_sync.sync_id';
2721 + }
2722 +
2723 + return 'SELECT wp.*,wp_sync.*' . $view_selects . '
2724 + FROM ' . $this->table_name( 'wp' ) . ' wp
2725 + JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2726 + ' . $view_joins . '
2727 + WHERE id = ' . $id . $where;
2728 + }
2729 + return null;
2730 + }
2731 +
2732 + /**
1928 2733 * Get child site by id.
1929 2734 *
1930 2735 * @param int $id Child site ID.
1931 2736 * @param array $selectGroups Select groups.
1932 2737 * @param array $extra_view Get extra option fields.
2738 + * @param int $obj OBJECT|ARRAY_A.
1933 2739 *
1934 2740 * @return object|null Database query results or null on failure.
1935 2741 */
1936 - public function get_website_by_id( $id, $selectGroups = false, $extra_view = array() ) {
1937 - return $this->get_row_result( $this->get_sql_website_by_id( $id, $selectGroups, $extra_view ) );
2742 + public function get_website_by_id( $id, $selectGroups = false, $extra_view = array(), $obj = OBJECT ) {
2743 + return $this->get_row_result( $this->get_sql_website_by_id( $id, $selectGroups, $extra_view ), $obj );
1938 2744 }
1939 2745
1940 2746 /**
1941 2747 * Get child site by id via SQL.
@@ -1954,24 +2760,35 @@
1954 2760 $extra_view = array( 'favi_icon', 'site_info' );
1955 2761 }
1956 2762
1957 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 +
1958 2775 $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' );
1959 2776 if ( $selectGroups ) {
1960 - 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
1961 2778 FROM ' . $this->table_name( 'wp' ) . ' wp
1962 2779 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1963 2780 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1964 2781 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1965 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
2782 + ' . $view_joins . '
1966 2783 WHERE wp.id = ' . $id . $where . '
1967 2784 GROUP BY wp.id, wp_sync.sync_id';
1968 2785 }
1969 2786
1970 - return 'SELECT wp.*,wp_sync.*,wp_optionview.*
2787 + return 'SELECT wp.*,wp_sync.*' . $view_selects . '
1971 2788 FROM ' . $this->table_name( 'wp' ) . ' wp
1972 2789 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1973 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
2790 + ' . $view_joins . '
1974 2791 WHERE id = ' . $id . $where;
1975 2792 }
1976 2793
1977 2794 return null;
@@ -2009,11 +2826,22 @@
2009 2826 return ( is_numeric( $e ) && 0 < $e ) ? true : false;
2010 2827 }
2011 2828 );
2012 2829
2013 - $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;
2014 2835
2015 - 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
2016 2844 }
2017 2845
2018 2846 /**
2019 2847 * Get child sites by groups IDs.
@@ -2066,17 +2894,107 @@
2066 2894
2067 2895 /**
2068 2896 * Get child sites by group ID.
2069 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 + *
2070 2938 * @param int $id Group ID.
2071 2939 *
2072 - * @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()
2073 2943 */
2074 - public function get_websites_by_group_id( $id ) {
2075 - 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.
2076 2967 }
2077 2968
2969 +
2078 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 + /**
2079 2997 * Get child sites by group id via SQL.
2080 2998 *
2081 2999 * @param int $id Group ID.
2082 3000 * @param bool $selectgroups Selected groups. Default: false.
@@ -2111,33 +3029,58 @@
2111 3029 }
2112 3030
2113 3031 $where_search = '';
2114 3032 if ( ! empty( $search_site ) ) {
2115 - $search_site = trim( $search_site );
2116 - $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 + );
2117 3041 }
2118 3042
2119 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' );
2120 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 +
2121 3064 if ( MainWP_Utility::ctype_digit( $id ) ) {
2122 3065 $where_allowed = $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
2123 3066 if ( $selectgroups ) {
2124 - $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
2125 3068 FROM ' . $this->table_name( 'wp' ) . ' wp
2126 3069 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
2127 3070 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
2128 3071 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
2129 3072 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2130 - JOIN ' . $this->get_option_view( $extra_view, true ) . ' wp_optionview ON wp.id = wp_optionview.wpid
3073 + ' . $view_joins . '
2131 3074 WHERE wpgroup.groupid = ' . $id . ' ' .
2132 3075 ( empty( $where ) ? '' : ' AND ' . $where ) . $where_allowed . $where_search . '
2133 3076 GROUP BY wp.id, wp_sync.sync_id
2134 3077 ORDER BY ' . $orderBy;
2135 3078 } else {
2136 - $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
2137 3080 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
2138 3081 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2139 - JOIN ' . $this->get_option_view( $extra_view, false ) . ' wp_optionview ON wp.id = wp_optionview.wpid
3082 + ' . $view_joins . '
2140 3083 WHERE wpgroup.groupid = ' . $id . ' ' . $where_allowed . $where_search .
2141 3084 ( empty( $where ) ? '' : ' AND ' . $where ) . ' ORDER BY ' . $orderBy;
2142 3085 }
2143 3086 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
@@ -2186,13 +3129,23 @@
2186 3129
2187 3130 $userid = $current_user->ID;
2188 3131 }
2189 3132
2190 - $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
2191 3144 INNER JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
2192 3145 JOIN ' . $this->table_name( 'group' ) . ' g ON wpgroup.groupid = g.id
2193 3146 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2194 - JOIN ' . $this->get_option_view() . ' wp_optionview ON wp.id = wp_optionview.wpid
3147 + ' . $view_joins . '
2195 3148 WHERE g.name="' . $this->escape( $groupname ) . '"';
2196 3149 if ( null !== $userid ) {
2197 3150 $sql .= ' AND g.userid = "' . intval( $userid ) . '"';
2198 3151 }
@@ -2207,9 +3160,10 @@
2207 3160 *
2208 3161 * @return string|null Child site IP address or null on failure.
2209 3162 */
2210 3163 public function get_wp_ip( $wpid ) {
2211 - 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.
2212 3166 }
2213 3167
2214 3168 /**
2215 3169 * Add website to the MainWP Dashboard.
@@ -2241,9 +3195,9 @@
2241 3195 }
2242 3196
2243 3197 $groupids = isset( $params['groupids'] ) ? $params['groupids'] : array();
2244 3198 $groupnames = isset( $params['groupnames'] ) ? $params['groupnames'] : array();
2245 - $verifyCertificate = isset( $params['verifyCertificate'] ) ? (int) $params['verifyCertificate'] : 1;
3199 + $verifyCertificate = isset( $params['verifyCertificate'] ) ? (int) $params['verifyCertificate'] : 2;
2246 3200 $uniqueId = isset( $params['uniqueId'] ) ? $params['uniqueId'] : '';
2247 3201 $http_user = isset( $params['http_user'] ) ? $params['http_user'] : null;
2248 3202 $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null;
2249 3203 $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0;
@@ -2248,13 +3202,33 @@
2248 3202 $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null;
2249 3203 $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0;
2250 3204 $wpe = isset( $params['wpe'] ) ? $params['wpe'] : 0;
2251 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;
2252 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 +
2253 3223 if ( MainWP_Utility::ctype_digit( $userid ) ) {
2254 3224 if ( '/' !== substr( $url, - 1 ) ) {
2255 3225 $url .= '/';
2256 3226 }
3227 +
3228 + $en_pk_data = MainWP_Encrypt_Data_Lib::instance()->encrypt_privkey( base64_decode( $privkey ) ); // phpcs:ignore -- NOSONAR - base64_encode trust.
3229 + $en_privkey = isset( $en_pk_data['en_data'] ) ? $en_pk_data['en_data'] : '';
3230 +
2257 3231 $values = array(
2258 3232 'userid' => $userid,
2259 3233 'adminname' => $this->escape( $admin ),
2260 3234 'name' => $this->escape( wp_strip_all_tags( $name ) ),
@@ -2259,9 +3233,9 @@
2259 3233 'adminname' => $this->escape( $admin ),
2260 3234 'name' => $this->escape( wp_strip_all_tags( $name ) ),
2261 3235 'url' => $this->escape( $url ),
2262 3236 'pubkey' => $this->escape( $pubkey ),
2263 - 'privkey' => $this->escape( $privkey ),
3237 + 'privkey' => $this->escape( base64_encode( $en_privkey ) ), // phpcs:ignore -- NOSONAR - trust.
2264 3238 'siteurl' => '',
2265 3239 'ga_id' => '',
2266 3240 'gas_id' => 0,
2267 3241 'offline_checks_last' => 0,
@@ -2272,8 +3246,9 @@
2272 3246 'plugin_upgrades' => '',
2273 3247 'theme_upgrades' => '',
2274 3248 'translation_upgrades' => '',
2275 3249 'securityIssues' => '',
3250 + 'premium_upgrades' => '',
2276 3251 'themes' => '',
2277 3252 'ignored_themes' => '',
2278 3253 'plugins' => '',
2279 3254 'ignored_plugins' => '',
@@ -2285,14 +3260,18 @@
2285 3260 'verify_certificate' => intval( $verifyCertificate ),
2286 3261 'ssl_version' => $sslVersion,
2287 3262 'uniqueId' => $uniqueId,
2288 3263 'mainwpdir' => 0,
2289 - 'http_user' => $http_user,
2290 - 'http_pass' => $http_pass,
3264 + 'http_user' => $encrypted_http_user,
3265 + 'http_pass' => $encrypted_http_pass,
2291 3266 'wpe' => $wpe,
2292 3267 'is_staging' => $isStaging,
2293 3268 );
2294 3269
3270 + if ( null !== $force_use_ipv4 ) {
3271 + $values['force_use_ipv4'] = $force_use_ipv4;
3272 + }
3273 +
2295 3274 $syncValues = array(
2296 3275 'dtsSync' => 0,
2297 3276 'dtsSyncStart' => 0,
2298 3277 'dtsAutomaticSync' => 0,
@@ -2301,11 +3280,14 @@
2301 3280 'extauth' => '',
2302 3281 'sync_errors' => '',
2303 3282 );
2304 3283 if ( $this->wpdb->insert( $this->table_name( 'wp' ), $values ) ) {
2305 - $websiteid = $this->wpdb->insert_id;
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.
3286 + MainWP_Encrypt_Data_Lib::instance()->encrypt_save_keys( $websiteid, $en_pk_data );
2306 3287 $syncValues['wpid'] = $websiteid;
2307 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() ) );
2308 3290 $this->wpdb->insert(
2309 3291 $this->table_name( 'wp_settings_backup' ),
2310 3292 array(
2311 3293 'wpid' => $websiteid,
@@ -2335,8 +3317,26 @@
2335 3317 )
2336 3318 );
2337 3319 }
2338 3320
3321 + // Add primary monitor, use global settings.
3322 + $success = MainWP_DB_Uptime_Monitoring::instance()->update_wp_monitor(
3323 + array(
3324 + 'wpid' => $websiteid,
3325 + 'suburl' => '',
3326 + )
3327 + );
3328 +
3329 + $opt_name = 'mainwp_primary_monitor_repair_attempts_for_wp_' . $websiteid;
3330 +
3331 + if ( ! $success ) {
3332 + update_option( $opt_name, 1 );
3333 + MainWP_Utility::unhide_mainwp_message( 'failed-attempt-create-primary-monitor-' . $websiteid );
3334 + } elseif ( get_option( $opt_name ) ) {
3335 + delete_option( $opt_name );
3336 + }
3337 +
3338 + MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
2339 3339 return $websiteid;
2340 3340 }
2341 3341 }
2342 3342
@@ -2353,14 +3353,15 @@
2353 3353 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
2354 3354 */
2355 3355 public function remove_website( $websiteid ) {
2356 3356 if ( MainWP_Utility::ctype_digit( $websiteid ) ) {
2357 - $nr = $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp' ) . ' WHERE id=%d', $websiteid ) );
2358 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE wpid=%d', $websiteid ) );
2359 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_sync' ) . ' WHERE wpid=%d', $websiteid ) );
2360 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_options' ) . ' WHERE wpid=%d', $websiteid ) );
2361 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_status' ) . ' WHERE wpid=%d', $websiteid ) );
2362 -
3357 + $nr = $this->wpdb->delete( $this->table_name( 'wp' ), array( 'id' => $websiteid ) );
3358 + $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3359 + $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) );
3360 + $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) );
3361 + MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid );
3362 + MainWP_DB_Uptime_Monitoring::instance()->delete_site_monitors( $websiteid );
3363 + MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
2363 3364 return $nr;
2364 3365 }
2365 3366
2366 3367 return false;
@@ -2375,8 +3376,35 @@
2375 3376 * @return int|boolean The number of rows updated, or false on error.
2376 3377 */
2377 3378 public function update_website_values( $websiteid, $fields ) {
2378 3379 if ( ! empty( $fields ) ) {
3380 + // MWP-1548: encrypt http_user / http_pass at rest if either
3381 + // is present in $fields. Generic-fields updaters (clone
3382 + // flow in extensions-handler, callers that pass arbitrary
3383 + // column subsets) must go through the same fail-closed
3384 + // contract as add_website / update_website.
3385 + if ( array_key_exists( 'http_user', $fields ) ) {
3386 + $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_user'], 'http_user' );
3387 + if ( false === $encrypted ) {
3388 + return false;
3389 + }
3390 + $fields['http_user'] = $encrypted;
3391 + }
3392 + if ( array_key_exists( 'http_pass', $fields ) ) {
3393 + $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_pass'], 'http_pass' );
3394 + if ( false === $encrypted ) {
3395 + return false;
3396 + }
3397 + $fields['http_pass'] = $encrypted;
3398 + }
3399 + // Lock the data stream to prevent other processes from updating at the same time.
3400 + $table_name = esc_sql( $this->table_name( 'wp' ) );
3401 + $sql = $this->wpdb->prepare(
3402 + "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3403 + $websiteid
3404 + );
3405 + $this->wpdb->get_row( $sql );
3406 +
2379 3407 return $this->wpdb->update( $this->table_name( 'wp' ), $fields, array( 'id' => $websiteid ) );
2380 3408 }
2381 3409
2382 3410 return false;
@@ -2417,13 +3445,11 @@
2417 3445 * @param string $uniqueId Unique security ID.
2418 3446 * @param string $http_user HTTP Basic Authentication username.
2419 3447 * @param string $http_pass HTTP Basic Authentication password.
2420 3448 * @param int $sslVersion SSL Version.
2421 - * @param int $disableChecking Wether or not disable sites status checking.
2422 - * @param int $checkInterval Status checking interval.
2423 3449 * @param bool $disableHealthChecking Disable Site health threshold.
2424 3450 * @param int $healthThreshold Site health threshold.
2425 - * @param int $wpe Is it WP Engine hosted site.
3451 + * @param string $backup_method Primary backup method.
2426 3452 *
2427 3453 * @return boolean ture on success or false on failure.
2428 3454 *
2429 3455 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
@@ -2446,28 +3472,68 @@
2446 3472 $uniqueId = '',
2447 3473 $http_user = null,
2448 3474 $http_pass = null,
2449 3475 $sslVersion = 0,
2450 - $disableChecking = 1,
2451 - $checkInterval = 1440,
2452 3476 $disableHealthChecking = 1,
2453 - $healthThreshold = 80,
2454 - $wpe = 0
3477 + $healthThreshold = 0,
3478 + $backup_method = 'global'
2455 3479 ) {
2456 3480
3481 + $wpe = 0; // going to update when sync.
3482 +
2457 3483 if ( MainWP_Utility::ctype_digit( $websiteid ) && MainWP_Utility::ctype_digit( $userid ) ) {
2458 3484 $website = $this->get_website_by_id( $websiteid );
2459 3485 if ( MainWP_System_Utility::can_edit_website( $website ) ) {
3486 + // MWP-1548: encrypt http_user / http_pass at rest. Same
3487 + // fail-closed contract as add_website -- a non-empty
3488 + // value that fails to encrypt aborts the update so we
3489 + // never overwrite an existing encrypted row with
3490 + // plaintext when the encryption layer is unhealthy.
3491 + $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' );
3492 + $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' );
3493 + if ( false === $encrypted_http_user || false === $encrypted_http_pass ) {
3494 + return false;
3495 + }
2460 3496 // update admin.
2461 - $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_status_check="' . $this->escape( $disableChecking ) . '", status_check_interval="' . $this->escape( $checkInterval ) . '", disable_health_check="' . $this->escape( $disableHealthChecking ) . '", health_threshold="' . $this->escape( $healthThreshold ) . '" WHERE id=%d', $websiteid ) );
2462 - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'wp_settings_backup' ) . ' SET archiveFormat = "' . $this->escape( $archiveFormat ) . '" WHERE wpid=%d', $websiteid ) );
3497 + $this->wpdb->update(
3498 + $this->table_name( 'wp' ),
3499 + array(
3500 + 'url' => $url,
3501 + 'name' => wp_strip_all_tags( $name ),
3502 + 'adminname' => $siteadmin,
3503 + 'pluginDir' => $pluginDir,
3504 + 'verify_certificate' => intval( $verifyCertificate ),
3505 + 'ssl_version' => intval( $sslVersion ),
3506 + 'wpe' => intval( $wpe ),
3507 + 'uniqueId' => $uniqueId,
3508 + 'http_user' => $encrypted_http_user,
3509 + 'http_pass' => $encrypted_http_pass,
3510 + 'disable_health_check' => $disableHealthChecking,
3511 + 'health_threshold' => $healthThreshold,
3512 + 'primary_backup_method' => $backup_method,
3513 + ),
3514 + array( 'id' => $websiteid )
3515 + );
3516 + $this->wpdb->update(
3517 + $this->table_name( 'wp_settings_backup' ),
3518 + array( 'archiveFormat' => $archiveFormat ),
3519 + array( 'wpid' => $websiteid )
3520 + );
2463 3521
2464 3522 if ( get_option( 'mainwp_enableLegacyBackupFeature' ) ) {
2465 - $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 ) );
3523 + $this->wpdb->update(
3524 + $this->table_name( 'wp' ),
3525 + array(
3526 + 'maximumFileDescriptorsOverride' => (int) $maximumFileDescriptorsOverride,
3527 + 'maximumFileDescriptorsAuto' => (int) $maximumFileDescriptorsAuto,
3528 + 'maximumFileDescriptors' => (int) $maximumFileDescriptors,
3529 + ),
3530 + array( 'id' => $websiteid )
3531 + );
2466 3532 }
2467 3533
2468 3534 // remove groups.
2469 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE wpid=%d', $websiteid ) );
3535 + $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
2470 3536 // Remove GA stats.
2471 3537 $showErrors = $this->wpdb->hide_errors();
2472 3538
2473 3539 /**
@@ -2554,9 +3620,11 @@
2554 3620 public function get_websites_by_url( $url ) {
2555 3621 if ( '/' !== substr( $url, - 1 ) ) {
2556 3622 $url .= '/';
2557 3623 }
2558 - $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 );
3624 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
3625 + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3626 + $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
2559 3627 if ( $results ) {
2560 3628 return $results;
2561 3629 }
2562 3630
@@ -2568,9 +3636,9 @@
2568 3636 $url = str_replace( 'https://', 'https://www.', $url );
2569 3637 $url = str_replace( 'http://', 'http://www.', $url );
2570 3638 }
2571 3639
2572 - $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 );
3640 + $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
2573 3641 if ( $results ) {
2574 3642 return $results;
2575 3643 }
2576 3644
@@ -2575,31 +3643,68 @@
2575 3643 }
2576 3644
2577 3645 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
2578 3646
2579 - 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 );
3647 + return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
2580 3648 }
2581 3649
2582 3650 /**
2583 - * Get websites offline status.
3651 + * Get recently-synced child sites whose stored url differs from the
3652 + * child-reported siteurl.
2584 3653 *
2585 - * @return array Child site monitoring status.
3654 + * The trailing slash must be normalized in SQL: stored urls always end
3655 + * with a slash while child-reported siteurl values never do, so a plain
3656 + * inequality would match nearly every site.
3657 + *
3658 + * @since 6.2
3659 + *
3660 + * @param int $days Freshness gate: only sites synced within this many days.
3661 + *
3662 + * @return array|object|null Rows with id, url, siteurl or null on failure.
2586 3663 */
2587 - public function get_websites_offline_status_to_send_notice() {
2588 - $where = $this->get_sql_where_allow_access_sites( 'wp' );
2589 - $extra_view = array( 'monitoring_notification_emails', 'settings_notification_emails' );
3664 + public function get_websites_with_url_siteurl_mismatch( $days = 30 ) {
3665 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
3666 + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3667 + $minimum_sync = time() - intval( $days ) * DAY_IN_SECONDS;
2590 3668
2591 - return $this->wpdb->get_results(
2592 - 'SELECT wp.*,wp_sync.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp
2593 - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2594 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
2595 - 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.
2596 - $where,
2597 - OBJECT
2598 - );
3669 + return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT wp.id, wp.url, wp.siteurl FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.siteurl <> '' AND TRIM(TRAILING '/' FROM wp.url) <> TRIM(TRAILING '/' FROM wp.siteurl) AND wp_sync.dtsSync >= %d", $minimum_sync ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
2599 3670 }
2600 3671
2601 3672 /**
3673 + * Get IDs of child sites that have a non-empty value for a website option.
3674 + *
3675 + * @since 6.2
3676 + *
3677 + * @param string $option_name Website option name.
3678 + * @param int $limit Maximum number of IDs to return.
3679 + *
3680 + * @return array Site IDs.
3681 + */
3682 + public function get_website_ids_with_nonempty_option( $option_name, $limit = 3 ) {
3683 + $options_table = esc_sql( $this->table_name( 'wp_options' ) );
3684 +
3685 + return $this->wpdb->get_col( $this->wpdb->prepare( "SELECT wpid FROM {$options_table} WHERE name = %s AND value <> '' ORDER BY wpid ASC LIMIT %d", $option_name, max( 1, intval( $limit ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3686 + }
3687 +
3688 + /**
3689 + * Get one child site whose stored url exactly matches the given URL,
3690 + * excluding a site ID. Used as the duplicate guard before a URL correction.
3691 + *
3692 + * @since 6.2
3693 + *
3694 + * @param string $url URL to match ( trailing slash enforced ).
3695 + * @param int $exclude_id Site ID to exclude.
3696 + *
3697 + * @return object|null Matching row ( id, url ) or null.
3698 + */
3699 + public function get_website_by_exact_url_excluding_id( $url, $exclude_id ) {
3700 + $url = rtrim( (string) $url, '/' );
3701 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
3702 +
3703 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT id, url FROM {$wp_table} WHERE TRIM(TRAILING '/' FROM url) = %s AND id <> %d", $url, intval( $exclude_id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3704 + }
3705 +
3706 + /**
2602 3707 * Method get_websites_to_notice_health_threshold()
2603 3708 *
2604 3709 * Get websites to notice site health.
2605 3710 *
@@ -2620,14 +3725,23 @@
2620 3725
2621 3726 $where_site_threshold = ' ( wp.health_threshold = 80 AND wp_sync.health_value < 80 ) '; // should-be-improved site health.
2622 3727 $where_site_threshold .= ' OR ( wp.health_threshold = 100 AND wp_sync.health_value >= 80 ) '; // good site health.
2623 3728
2624 - return $this->wpdb->get_results(
2625 - 'SELECT wp.*,wp_sync.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp
2626 - JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2627 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
2628 - 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 ' .
2629 - $where,
3729 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
3730 + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3731 +
3732 + $view_selects = '';
3733 + $view_joins = '';
3734 +
3735 + $opts_view = $this->get_wp_options_join( $extra_view );
3736 +
3737 + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3738 + $view_selects = ',' . $opts_view['selects'];
3739 + $view_joins = $opts_view['joins'];
3740 + }
3741 + return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery
3742 + "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid {$view_joins} WHERE wp.disable_health_check <> 1 AND wp.offline_check_result = 1 AND ( {$where_global_threshold} OR{$where_site_threshold} ) AND wp_sync.health_site_noticed = 0 " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd, the view fragments come from get_wp_options_join(), which allowlists option names, and both threshold fragments are literals picked above.
3743 + $where . ' GROUP BY wp.id ',
2630 3744 OBJECT
2631 3745 );
2632 3746 }
2633 3747
@@ -2635,22 +3749,46 @@
2635 3749 * Get websites offline status.
2636 3750 *
2637 3751 * @return array Sites with offline status.
2638 3752 */
2639 - public function get_websites_offline_check_status() {
3753 + public function get_websites_http_check_status() {
2640 3754 $where = $this->get_sql_where_allow_access_sites( 'wp' );
2641 3755 $extra_view = array( 'settings_notification_emails' );
3756 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
2642 3757
3758 + $view_selects = '';
3759 + $view_joins = '';
3760 +
3761 + $opts_view = $this->get_wp_options_join( $extra_view );
3762 +
3763 + if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3764 + $view_selects = ',' . $opts_view['selects'];
3765 + $view_joins = $opts_view['joins'];
3766 + }
3767 +
3768 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery.
2643 3769 return $this->wpdb->get_results(
2644 - 'SELECT wp.*,wp_optionview.* FROM ' . $this->table_name( 'wp' ) . ' wp
2645 - JOIN ' . $this->get_option_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid
2646 - WHERE wp.disable_status_check <> 1 AND wp.offline_check_result = -1' . // offline checked status.
2647 - $where,
3770 + "SELECT wp.*{$view_selects} FROM {$wp_table} wp {$view_joins}" . ' WHERE wp.suspended = 0 AND wp.http_code_noticed = 0 AND wp.offline_check_result = -1 ' . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the view fragments come from get_wp_options_join(), which allowlists option names.
3771 + $where . ' GROUP BY wp.id ',
2648 3772 OBJECT
2649 3773 );
2650 3774 }
2651 3775
2652 3776 /**
3777 + * Method set_website_noticed_http_check().
3778 + *
3779 + * @param array $site_id The site id .
3780 + *
3781 + * @return void
3782 + */
3783 + public function set_website_noticed_http_check( $site_id = array() ) {
3784 + if ( empty( $site_id ) ) {
3785 + return;
3786 + }
3787 + $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.
3788 + }
3789 +
3790 + /**
2653 3791 * Get DB Sites.
2654 3792 *
2655 3793 * @since 4.6
2656 3794 *
@@ -2760,9 +3898,9 @@
2760 3898 if ( ! MainWP_System_Utility::can_edit_website( $website ) ) {
2761 3899 return false;
2762 3900 }
2763 3901
2764 - if ( ! mainwp_current_user_have_right( 'site', $websiteid ) ) {
3902 + if ( ! \mainwp_current_user_can( 'site', $websiteid ) ) {
2765 3903 return false;
2766 3904 }
2767 3905
2768 3906 return array(
@@ -2879,18 +4017,28 @@
2879 4017 *
2880 4018 * @return mixed Result
2881 4019 */
2882 4020 public function insert_lookup_item( $item_name, $item_id, $obj_name, $obj_id ) {
4021 + // get_lookup_items() and delete_lookup_items() match item_name as given, so a
4022 + // name that sanitizing would alter is refused instead of being stored under a
4023 + // value the caller can never read back or delete.
4024 + if ( ! is_string( $item_name ) || sanitize_text_field( $item_name ) !== $item_name ) {
4025 + return false;
4026 + }
2883 4027 if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) {
2884 4028 return false;
2885 4029 }
2886 4030 $data = array(
2887 - 'item_name' => 'cost',
4031 + 'item_name' => $item_name,
2888 4032 'item_id' => $item_id,
2889 4033 'object_name' => $obj_name,
2890 4034 'object_id' => $obj_id,
2891 4035 );
2892 - $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data );
4036 + // wpdb refuses a value past the column length before any SQL runs, and
4037 + // insert_id would still hold the previous insert's id.
4038 + if ( false === $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data ) ) {
4039 + return false;
4040 + }
2893 4041 return $this->wpdb->insert_id; // must return lookup id.
2894 4042 }
2895 4043
2896 4044 /**
@@ -2952,19 +4100,26 @@
2952 4100 }
2953 4101
2954 4102
2955 4103 /**
2956 - * Return the user data for the given consumer_key.
4104 + * Insert a new REST API key row and return the credential payload.
2957 4105 *
4106 + * Returns an array with key_id, user_id, plaintext consumer_key,
4107 + * plaintext consumer_secret, and key_permissions on success. Returns
4108 + * false when the wpdb->insert() call fails (no current user, missing
4109 + * table, schema mismatch, etc.). Callers must check the return type
4110 + * before treating it as an array.
4111 + *
2958 4112 * @param string $consumer_key Consumer key.
2959 4113 * @param string $consumer_secret Secret key.
2960 4114 * @param string $scope scope.
2961 4115 * @param string $description description.
2962 4116 * @param int $enabled 1 or 0.
4117 + * @param array $others others.
2963 4118 *
2964 - * @return array
4119 + * @return array|false Credential payload on success, false on failure.
2965 4120 */
2966 - public function insert_rest_api_key( $consumer_key, $consumer_secret, $scope, $description, $enabled ) {
4121 + public function insert_rest_api_key( $consumer_key, $consumer_secret, $scope, $description, $enabled, $others = array() ) {
2967 4122 global $current_user;
2968 4123
2969 4124 if ( $current_user ) {
2970 4125 $user_id = $current_user->ID;
@@ -2973,11 +4128,19 @@
2973 4128 if ( empty( $user_id ) ) {
2974 4129 return false;
2975 4130 }
2976 4131
4132 + unset( $others ); // Parameter retained for signature compatibility; key_pass/key_type fields are vestigial after MWP-1544 cleanup.
4133 +
4134 + // Hash the consumer_secret with WordPress's password hasher so the
4135 + // value at rest is no longer reversible by a DB-read primitive.
4136 + // The plaintext is returned to the caller below so it can be shown
4137 + // to the admin once at creation time. See MWP-1540.
4138 + $hashed_secret = wp_hash_password( $consumer_secret );
4139 +
2977 4140 // Created API keys.
2978 - $permissions = in_array( $scope, array( 'read', 'write', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
2979 - $this->wpdb->insert(
4141 + $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4142 + $inserted = $this->wpdb->insert(
2980 4143 $this->table_name( 'api_keys' ),
2981 4144 array(
2982 4145 'user_id' => $user_id,
2983 4146 'description' => $description,
@@ -2982,9 +4145,9 @@
2982 4145 'user_id' => $user_id,
2983 4146 'description' => $description,
2984 4147 'permissions' => $permissions,
2985 4148 'consumer_key' => mainwp_api_hash( $consumer_key ),
2986 - 'consumer_secret' => $consumer_secret,
4149 + 'consumer_secret' => $hashed_secret,
2987 4150 'truncated_key' => substr( $consumer_key, -7 ),
2988 4151 'enabled' => $enabled,
2989 4152 ),
2990 4153 array(
@@ -2994,11 +4157,22 @@
2994 4157 '%s',
2995 4158 '%s',
2996 4159 '%s',
2997 4160 '%d',
2998 - )
4161 + ),
2999 4162 );
3000 4163
4164 + // wpdb->insert() returns false on failure. Without this guard the
4165 + // function would still hand back the plaintext consumer_secret plus
4166 + // $wpdb->insert_id, but that insert_id is the LAST successful insert
4167 + // on the connection (typically from earlier in the same request),
4168 + // not this row. The caller's empty-key_id check would pass and the
4169 + // operator would receive a credential that was never persisted.
4170 + // See MWP-1540 PR review feedback.
4171 + if ( false === $inserted ) {
4172 + return false;
4173 + }
4174 +
3001 4175 return array(
3002 4176 'key_id' => $this->wpdb->insert_id,
3003 4177 'user_id' => $user_id,
3004 4178 'consumer_key' => $consumer_key,
@@ -3017,9 +4191,9 @@
3017 4191 *
3018 4192 * @return array
3019 4193 */
3020 4194 public function update_rest_api_key( $key_id, $scope, $description, $enabled = 1 ) {
3021 - $permissions = in_array( $scope, array( 'read', 'write', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4195 + $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
3022 4196 return $this->wpdb->update(
3023 4197 $this->table_name( 'api_keys' ),
3024 4198 array(
3025 4199 'description' => $description,
@@ -3038,9 +4212,10 @@
3038 4212 *
3039 4213 * @return bool result.
3040 4214 */
3041 4215 public function is_existed_enabled_rest_key() {
3042 - $enabled = $this->wpdb->get_row( 'SELECT * FROM ' . $this->table_name( 'api_keys' ) . ' WHERE enabled = 1 LIMIT 1' );
4216 + $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4217 + $enabled = $this->wpdb->get_row( "SELECT * FROM {$table_name} WHERE enabled = 1 LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3043 4218 return $enabled ? true : false;
3044 4219 }
3045 4220
3046 4221 /**
@@ -3050,9 +4225,10 @@
3050 4225 *
3051 4226 * @return array
3052 4227 */
3053 4228 public function get_rest_api_key_by( $id ) {
3054 - return $this->wpdb->get_row( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'api_keys' ) . ' WHERE key_id = %d ', $id ) );
4229 + $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4230 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table_name} WHERE key_id = %d", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3055 4231 }
3056 4232
3057 4233 /**
3058 4234 * Method remove_rest_api_key().
@@ -3061,9 +4237,10 @@
3061 4237 *
3062 4238 * @return array
3063 4239 */
3064 4240 public function remove_rest_api_key( $id ) {
3065 - return $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'api_keys' ) . ' WHERE key_id = %s', $id ) );
4241 + $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4242 + return $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE key_id = %s", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3066 4243 }
3067 4244
3068 4245 /**
3069 4246 * Method get_rest_api_keys().
@@ -3070,7 +4247,155 @@
3070 4247 *
3071 4248 * @return array
3072 4249 */
3073 4250 public function get_rest_api_keys() {
3074 - return $this->wpdb->get_results( 'SELECT * FROM ' . $this->table_name( 'api_keys' ) . ' ORDER BY key_id DESC' );
4251 + $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4252 + return $this->wpdb->get_results( "SELECT * FROM {$table_name} ORDER BY key_id DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4253 + }
4254 +
4255 +
4256 + /**
4257 + * Update regular process.
4258 + *
4259 + * @param array $data process data.
4260 + * @return mixed
4261 + */
4262 + public function update_regular_process( $data ) {
4263 + if ( isset( $data['process_id'] ) ) {
4264 + $process_id = $data['process_id'];
4265 + unset( $data['process_id'] );
4266 + return $this->wpdb->update( $this->table_name( 'schedule_processes' ), $data, array( 'process_id' => $process_id ) );
4267 + } elseif ( is_array( $data ) && isset( $data['type'] ) && isset( $data['process_slug'] ) ) {
4268 + return $this->wpdb->insert( $this->table_name( 'schedule_processes' ), $data );
4269 + }
4270 + return false;
4271 + }
4272 +
4273 + /**
4274 + * Delete regular process.
4275 + *
4276 + * @param int $process_id Process id.
4277 + * @param int $item_id Item id.
4278 + * @param string $pro_type Process type.
4279 + * @param string $pro_slug Process slug.
4280 + *
4281 + * @return mixed
4282 + */
4283 + public function delete_regular_process( $process_id = false, $item_id = false, $pro_type = false, $pro_slug = false ) {
4284 +
4285 + if ( is_numeric( $process_id ) && ! empty( $process_id ) ) {
4286 + return $this->wpdb->delete(
4287 + $this->table_name( 'schedule_processes' ),
4288 + array(
4289 + 'process_id' => $process_id,
4290 + )
4291 + );
4292 + } elseif ( ! empty( $pro_type ) || ! empty( $pro_slug ) ) {
4293 +
4294 + $data = array();
4295 +
4296 + if ( ! empty( $pro_type ) ) {
4297 + $data['type'] = $pro_type;
4298 + }
4299 +
4300 + if ( ! empty( $pro_slug ) ) {
4301 + $data['process_slug'] = $pro_slug;
4302 + }
4303 +
4304 + if ( ! empty( $item_id ) ) {
4305 + $data['item_id'] = $item_id;
4306 + }
4307 + // Bulk delete.
4308 + return $this->wpdb->delete( $this->table_name( 'schedule_processes' ), $data );
4309 + }
4310 + return false;
4311 + }
4312 +
4313 + /**
4314 + * Method get_regular_process_by_item_id_type_slug
4315 + *
4316 + * @param integer $item_id item id.
4317 + * @param string $type type.
4318 + * @param string $process_slug process slug.
4319 + *
4320 + * @return mixed result
4321 + */
4322 + public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) {
4323 + $table_name = esc_sql( $this->table_name( 'schedule_processes' ) );
4324 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT pr.* FROM {$table_name} pr WHERE pr.item_id = %d AND pr.type = %s AND pr.process_slug = %s", $item_id, $type, $process_slug ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4325 + }
4326 +
4327 + /**
4328 + * Log SQL queries for debugging via hook.
4329 + *
4330 + * This method provides a structured way to log SQL queries for development/debugging.
4331 + * It does NOT use error_log() directly - instead, it fires the `mainwp_log_system_query`
4332 + * action hook, allowing external listeners (logging plugins, debug tools) to handle
4333 + * the log output appropriately.
4334 + *
4335 + * To enable query logging:
4336 + * 1. Set `$params['dev_log_query'] = 1` when calling database methods
4337 + * 2. Add a listener to the `mainwp_log_system_query` action hook
4338 + *
4339 + * Example listener:
4340 + * ```php
4341 + * add_action( 'mainwp_log_system_query', function( $params, $sql, $caller ) {
4342 + * error_log( 'MainWP Query: ' . $sql );
4343 + * }, 10, 3 );
4344 + * ```
4345 + *
4346 + * @param array $params Query parameters. Set 'dev_log_query' to enable logging.
4347 + * @param string $sql The SQL query string.
4348 + * @param mixed $caller Instance of caller class (optional).
4349 + * @return void
4350 + */
4351 + public function log_system_query( $params, $sql, $caller = false ) {
4352 + $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller );
4353 + if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) {
4354 + do_action( 'mainwp_log_system_query', $params, $sql, $caller );
4355 + }
4356 + }
4357 +
4358 + /**
4359 + * Method get_core_tables().
4360 + *
4361 + * Returns the list of core tables used by MainWP for validation data integrity and other purposes.
4362 + *
4363 + * @return array Core tables info
4364 + */
4365 + public function get_core_tables() {
4366 +
4367 + $core_tables = array(
4368 + $this->table_name( 'wp_clients' ),
4369 + $this->table_name( 'wp_clients_fields' ),
4370 + $this->table_name( 'wp_clients_field_values' ),
4371 + $this->table_name( 'wp_clients_contacts' ),
4372 + $this->table_name( 'monitors' ),
4373 + $this->table_name( 'monitor_heartbeat' ),
4374 + $this->table_name( 'monitor_stat_hourly' ),
4375 + $this->table_name( 'wp' ),
4376 + $this->table_name( 'wp_sync' ),
4377 + $this->table_name( 'wp_options' ),
4378 + $this->table_name( 'wp_settings_backup' ),
4379 + $this->table_name( 'users' ),
4380 + $this->table_name( 'wp_status' ),
4381 + $this->table_name( 'group' ),
4382 + $this->table_name( 'wp_group' ),
4383 + $this->table_name( 'lookup_item_objects' ),
4384 + $this->table_name( 'wp_backup_progress' ),
4385 + $this->table_name( 'wp_backup' ),
4386 + $this->table_name( 'api_keys' ),
4387 + $this->table_name( 'action_log' ),
4388 + $this->table_name( 'request_log' ),
4389 + $this->table_name( 'schedule_processes' ),
4390 + $this->table_name( 'cost_tracker' ),
4391 + );
4392 +
4393 + if ( ! defined( 'MAINWP_MODULE_LOG_ENABLED' ) || MAINWP_MODULE_LOG_ENABLED ) {
4394 + $core_tables[] = $this->table_name( 'wp_logs' );
4395 + $core_tables[] = $this->table_name( 'wp_logs_meta' );
4396 + $core_tables[] = $this->table_name( 'wp_logs_meta_archive' );
4397 + }
4398 +
4399 + return apply_filters( 'mainwp_database_core_tables', $core_tables );
3075 4400 }
3076 4401 }