PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / trunk
MainWP Dashboard: Self-hosted WordPress Management for Agencies vtrunk
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 5.2.2 5.3 All 153 releases
mainwp / class / class-mainwp-db.php

class-mainwp-db.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies trunk, at class/class-mainwp-db.php

4,384 lines 178.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Database Controller
4 *
5 * This file handles all interactions with the DB.
6 *
7 * @package MainWP/Dashboard
8 */
9
10 namespace MainWP\Dashboard;
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class MainWP_DB
19 *
20 * @package MainWP\Dashboard
21 *
22 * @uses \MainWP\Dashboard\MainWP_DB_Base
23 */
24 class MainWP_DB extends MainWP_DB_Base { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
25
26 // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.DB.PreparedSQL.NotPrepared, Generic.Metrics.CyclomaticComplexity -- This is the only way to achieve desired results, pull request solutions appreciated.
27
28 /**
29 * Private static variable to hold the single instance of the class.
30 *
31 * @static
32 *
33 * @var mixed Default null
34 */
35 private static $instance = null;
36
37 /**
38 * Private static variable to hold the single instance.
39 *
40 * @static
41 *
42 * @var mixed Default null
43 */
44 private static $general_options = null;
45
46 /**
47 * Possible options.
48 *
49 * @var array $possible_options
50 */
51 private static $possible_options = array(
52 'plugin_upgrades',
53 'theme_upgrades',
54 'premium_upgrades',
55 'plugins',
56 'themes',
57 'dtsSync',
58 'version',
59 'sync_errors',
60 'ignored_plugins',
61 'wp_upgrades',
62 'site_info',
63 'client',
64 'signature_algo',
65 'verify_method',
66 'pubkey',
67 );
68
69 /**
70 * Create public static instance.
71 *
72 * @static
73 *
74 * @return MainWP_DB
75 */
76 public static function instance() {
77 if ( null === static::$instance ) {
78 static::$instance = new self();
79 }
80
81 static::$instance->test_connection();
82
83 return static::$instance;
84 }
85
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 /**
116 * Get wp_options database table view.
117 *
118 * @compatible function.
119 *
120 * @param array $fields Extra option fields.
121 * @param string $view_query view query.
122 *
123 * @return array wp_options view.
124 */
125 public function get_option_view( $fields = array(), $view_query = 'default' ) {
126
127 if ( ! is_array( $fields ) ) {
128 $fields = array();
129 }
130
131 $view = '(SELECT intwp.id AS wpid ';
132
133 $included_opts = array();
134
135 if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) {
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,
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,
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,
139 (SELECT phpversion.value FROM ' . $this->table_name( 'wp_options' ) . ' phpversion WHERE phpversion.wpid = intwp.id AND phpversion.name = "phpversion" LIMIT 1) AS phpversion,
140 (SELECT added_timestamp.value FROM ' . $this->table_name( 'wp_options' ) . ' added_timestamp WHERE added_timestamp.wpid = intwp.id AND added_timestamp.name = "added_timestamp" LIMIT 1) AS added_timestamp,
141 (SELECT wp_upgrades.value FROM ' . $this->table_name( 'wp_options' ) . ' wp_upgrades WHERE wp_upgrades.wpid = intwp.id AND wp_upgrades.name = "wp_upgrades" LIMIT 1) AS wp_upgrades ';
142 $included_opts = array( 'recent_comments', 'recent_posts', 'recent_pages', 'phpversion', 'added_timestamp', 'wp_upgrades' );
143 }
144
145 if ( ! in_array( 'signature_algo', $fields ) ) {
146 $fields[] = 'signature_algo';
147 }
148
149 if ( ! in_array( 'verify_method', $fields ) ) {
150 $fields[] = 'verify_method';
151 }
152
153 if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) {
154 $fields[] = 'cust_site_icon_info';
155 }
156
157 $fields = $this->filter_safe_option_names( $fields );
158
159 if ( is_array( $fields ) ) {
160 foreach ( $fields as $field ) {
161 if ( empty( $field ) ) {
162 continue;
163 }
164 if ( in_array( $field, $included_opts ) ) {
165 continue;
166 }
167 $view .= ', ';
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 ) . '`';
169 }
170 }
171
172 $view .= ' FROM ' . $this->table_name( 'wp' ) . ' intwp)';
173
174 return $view;
175 }
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.
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() ) {
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
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 /**
319 * Get SQL to get child sites for current user.
320 *
321 * @since 5.2.
322 * @param array $params other params.
323 *
324 * @return object|null Database query results or null on failure.
325 */
326 public function get_sql_websites_for_current_user_by_params( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
327
328 if ( ! is_array( $params ) ) {
329 $params = array();
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
339 $view = isset( $params['view'] ) ? $params['view'] : 'default';
340 $with_clients = isset( $params['with_clients'] ) && $params['with_clients'] ? true : false;
341
342 // legacy support.
343 $selectgroups = isset( $params['with_tags'] ) && $params['with_tags'] ? true : false;
344 $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url';
345 $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false;
346 $rowcount = isset( $params['rowcount'] ) ? (int) $params['rowcount'] : false;
347 $count_sql = isset( $params['count_sql'] ) && $params['count_sql'] ? true : false;
348 $extraWhere = isset( $params['where'] ) ? $params['where'] : null; // NOTE: without 'AND' at begining and ending of 'where'.
349 $for_manager = isset( $params['for_manager'] ) && $params['for_manager'] ? true : false;
350 $others_fields = isset( $params['others_fields'] ) && is_array( $params['others_fields'] ) ? $params['others_fields'] : array( 'favi_icon' );
351 $is_staging = isset( $params['is_staging'] ) && in_array( $params['is_staging'], array( 'yes', 'no' ) ) ? $params['is_staging'] : 'no';
352 $limit = isset( $params['limit'] ) ? intval( $params['limit'] ) : '';
353
354 $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
355
356 $s = isset( $params['s'] ) ? $params['s'] : '';
357 $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
358 $include = isset( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array();
359 $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array();
360 $page = isset( $params['page'] ) ? intval( $params['page'] ) : false;
361 $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false;
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
369 $where = '';
370
371 if ( ! empty( $extraWhere ) ) {
372 $where .= ' AND ' . $extraWhere;
373 }
374
375 if ( ! $for_manager ) {
376 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
377 }
378
379 $connected_sql = '';
380
381 if ( is_array( $params ) && isset( $params['connected'] ) && 'yes' === $params['connected'] ) {
382 $connected_sql = ' AND wp_sync.sync_errors = "" ';
383 } elseif ( is_array( $params ) && isset( $params['connected'] ) && 'no' === $params['connected'] ) {
384 $connected_sql = ' AND wp_sync.sync_errors <> "" ';
385 }
386
387 if ( ! empty( $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 );
397 }
398
399 if ( ! empty( $exclude ) ) {
400 $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') ';
401 }
402
403 if ( ! empty( $include ) ) {
404 $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') ';
405 }
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
427 // any, connected, disconnected, suspended, available_update.
428 if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) {
429 $status_conds = array();
430 if ( in_array( 'available_update', $status ) ) {
431 $available_sql = " ( wp.plugin_upgrades <> '' && wp.plugin_upgrades <> '[]' ) OR ( wp.theme_upgrades <> '' && wp.theme_upgrades <> '[]' ) OR ( wp.translation_upgrades <> '' && wp.translation_upgrades <> '[]' ) OR ( wp.premium_upgrades <> '' && wp.premium_upgrades <> '[]' ) ";
432 $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.
434 if ( $results ) {
435 $wp_ids = array();
436 foreach ( $results as $item ) {
437 if ( ! empty( $item->wpid ) ) {
438 $wp_ids[] = $item->wpid;
439 }
440 }
441 $wp_ids = ! empty( $wp_ids ) ? array_unique( $wp_ids ) : array();
442 if ( ! empty( $wp_ids ) ) {
443 $available_sql .= ' OR wp.id IN ( ' . implode( ',', $wp_ids ) . ' )';
444 }
445 }
446 $status_conds[] = ' ( ' . $available_sql . ') ';
447 }
448
449 if ( in_array( 'connected', $status ) ) {
450 $status_conds[] = ' ( wp_sync.sync_errors = "" ) ';
451 }
452 if ( in_array( 'disconnected', $status ) ) {
453 $status_conds[] = " wp_sync.sync_errors <> '' ";
454 }
455
456 if ( in_array( 'suspended', $status ) ) {
457 $status_conds[] = ' wp.suspended = 1 ';
458 }
459
460 if ( ! empty( $status_conds ) ) {
461 $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) ';
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 }
466 }
467
468 if ( ! empty( $page ) && ! empty( $per_page ) ) {
469 $limit = ( $page - 1 ) * $per_page . ',' . $per_page;
470 }
471
472 if ( 'wp.url' === $orderBy ) {
473 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
474 }
475
476 $select_clients = '';
477 $join_clients = '';
478
479 if ( $with_clients ) {
480 $select_clients = ', wpclient.name as client_name ';
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 ";
483 }
484
485 $base_fields = array(
486 'wp.id',
487 'wp.url',
488 'wp.name',
489 'wp.client_id',
490 'wp.verify_certificate',
491 'wp.http_user',
492 'wp.http_pass',
493 'wp.ssl_version',
494 'wp.adminname',
495 'wp.privkey',
496 'wp.pubkey',
497 'wp.wpe',
498 'wp.is_staging',
499 'wp.force_use_ipv4',
500 'wp.siteurl',
501 'wp.suspended',
502 'wp.mainwpdir',
503 'wp.is_ignoreCoreUpdates',
504 'wp.is_ignorePluginUpdates',
505 'wp.is_ignoreThemeUpdates',
506 'wp_sync.sync_errors',
507 'wp.backup_before_upgrade',
508 'wp.userid',
509 'wp.plugins',
510 'wp.themes',
511 'wp.offline_check_result', // 1 - online, -1 offline.
512 'wp.automatic_update',
513 );
514
515 $select = ' wp.*,wp_sync.* ';
516 if ( 'base_view' === $view ) {
517 $select = implode( ',', $base_fields );
518 } elseif ( 'updates_view' === $view ) {
519 $updates_fields = array(
520 'wp.plugin_upgrades',
521 'wp.theme_upgrades',
522 'wp.translation_upgrades',
523 'wp.premium_upgrades',
524 'wp.ignored_themes',
525 'wp.ignored_plugins',
526 );
527 $select = implode( ',', array_merge( $updates_fields, $base_fields ) );
528 }
529
530 $view_selects = '';
531 $view_joins = '';
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
544 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
545 if ( $selectgroups ) {
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
550 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
551 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
552 ' . $join_clients . '
553 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
554 ' . $view_joins . '
555 WHERE 1 ' . $where . $connected_sql;
556 $group_qry = '
557 GROUP BY wp.id, wp_sync.sync_id
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;
576 } else {
577 $select_qry = 'SELECT ' . $select . $view_selects . $select_clients;
578 $qry = ' FROM ' . $this->table_name( 'wp' ) . ' wp
579 ' . $join_clients . '
580 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
581 ' . $view_joins . '
582 WHERE 1 ' . $where . $connected_sql;
583 $group_qry = '
584 GROUP BY wp.id, wp_sync.sync_id
585 ORDER BY ' . $orderBy;
586 }
587
588 if ( $count_sql ) {
589 return 'SELECT COUNT(DISTINCT wp.id) ' . $qry;
590 }
591
592 $qry = $select_qry . $qry . $group_qry;
593
594 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
595 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
596 } elseif ( false !== $rowcount ) {
597 $qry .= ' LIMIT ' . $rowcount;
598 } elseif ( ! empty( $limit ) ) {
599 $qry .= ' LIMIT ' . $limit;
600 } else {
601 // load all sites so check to support limit sites loading.
602 $limit_sites = ! empty( $params['limit_sites'] ) ? intval( $params['limit_sites'] ) : 0;
603 if ( ! empty( $limit_sites ) ) {
604 $current_page = (int) get_option( 'mainwp_manage_updates_limit_current_page', 0 );
605 $current_page = $current_page > 0 ? $current_page - 1 : 0;
606 $start = $current_page * $limit_sites;
607 $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites );
608 }
609 }
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 ) );
615 return $qry;
616 }
617
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 /**
695 * Get wp_options database table view.
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 *
699 * @param array $view Option view.
700 * @param array $other_fields Extra option fields.
701 *
702 * @return array wp_options view.
703 */
704 public function get_option_view_by( $view = '', $other_fields = array() ) {
705
706 $default = array(
707 'recent_comments',
708 'recent_posts',
709 'recent_pages',
710 'phpversion',
711 'added_timestamp',
712 'wp_upgrades',
713 );
714
715 $fields = array();
716
717 if ( 'updates_view' === $view ) {
718 $fields = array(
719 'wp_upgrades',
720 'ignored_wp_upgrades',
721 'ignored_trans_updates',
722 );
723 } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) {
724 $fields = array();
725 if ( 'monitor_view' === $view || 'ping_view' === $view ) {
726 $fields[] = 'health_site_status';
727 $fields[] = 'bypass_cache';
728 }
729 } elseif ( 'custom_view' !== $view ) {
730 $fields = $default;
731 }
732
733 if ( is_array( $other_fields ) && ! empty( $other_fields ) ) {
734 $fields = array_unique( array_merge( $fields, $other_fields ) );
735 }
736
737 $view_query = '(SELECT wpid ';
738
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 }
747 }
748
749 $fields = $this->filter_safe_option_names( $fields );
750
751 foreach ( $fields as $field ) {
752
753 if ( empty( $field ) ) {
754 continue;
755 }
756
757 $view_query .= ', ';
758 $view_query .= 'MAX(CASE WHEN name = "' . $this->escape( $field ) . '" THEN value END) AS `' . $this->escape( $field ) . '`';
759 }
760
761 $view_query .= ' FROM ' . $this->table_name( 'wp_options' ) .
762 " WHERE name IN ('" . implode( "','", $fields ) . "')
763 GROUP BY wpid ) ";
764
765 return $view_query;
766 }
767
768 /**
769 * Method get_select_groups_belong().
770 *
771 * @return string sql.
772 */
773 public function get_select_groups_belong() {
774 return ', ( SELECT GROUP_CONCAT(grbl.name ORDER BY grbl.name SEPARATOR ",")
775 FROM ' . $this->table_name( 'wp_group' ) . ' wpgrbl
776 JOIN ' . $this->table_name( 'group' ) . ' grbl ON grbl.id = wpgrbl.groupid WHERE wpgrbl.wpid = wp.id ) as wpgroups_belong,
777 ( SELECT GROUP_CONCAT(grbl.id ORDER BY grbl.name SEPARATOR ",") FROM ' . $this->table_name( 'wp_group' ) . ' wpgrbl
778 JOIN ' . $this->table_name( 'group' ) . ' grbl ON grbl.id = wpgrbl.groupid WHERE wpgrbl.wpid = wp.id ) as wpgroupids_belong,
779 ( SELECT GROUP_CONCAT(grbl.color ORDER BY grbl.name SEPARATOR ",") FROM ' . $this->table_name( 'wp_group' ) . ' wpgrbl
780 JOIN ' . $this->table_name( 'group' ) . ' grbl ON grbl.id = wpgrbl.groupid WHERE wpgrbl.wpid = wp.id ) as wpgroupcolors_belong ';
781 }
782
783 /**
784 * Get connected child sites.
785 *
786 * @param array $sites_ids Websites ids - option field.
787 *
788 * @return array $connected_sites Array of connected sites.
789 */
790 public function get_connected_websites( $sites_ids = false ) {
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' ) );
794
795 $sql = "SELECT wp.*,wp_sync.*
796 FROM {$wp_table} wp
797 JOIN {$wp_sync_table} wp_sync
798 ON wp.id = wp_sync.wpid
799 WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors = \"\") " .
800 $where;
801
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()
803 $connected_sites = array();
804 if ( $websites ) {
805 foreach ( $websites as $website ) {
806
807 if ( ! empty( $sites_ids ) && ! in_array( $website->id, $sites_ids ) ) {
808 continue;
809 }
810
811 $connected_sites[] = array(
812 'id' => $website->id,
813 'name' => $website->name,
814 'url' => $website->url,
815 );
816 }
817 }
818 return $connected_sites;
819 }
820
821 /**
822 * Get disconnected child sites.
823 *
824 * @param array $sites_ids Websites ids - option field.
825 *
826 * @return array $disc_sites Array of disonnected sites.
827 */
828 public function get_disconnected_websites( $sites_ids = false ) {
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' ) );
832
833 $sql = "SELECT wp.*,wp_sync.*
834 FROM {$wp_table} wp
835 JOIN {$wp_sync_table} wp_sync
836 ON wp.id = wp_sync.wpid
837 WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors <> \"\") " .
838 $where;
839
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()
841 $disc_sites = array();
842 if ( $websites ) {
843 foreach ( $websites as $website ) {
844
845 if ( ! empty( $sites_ids ) && ! in_array( $website->id, $sites_ids ) ) {
846 continue;
847 }
848
849 $disc_sites[] = array(
850 'id' => $website->id,
851 'name' => $website->name,
852 'url' => $website->url,
853 );
854 }
855 }
856 return $disc_sites;
857 }
858
859 /**
860 * Get child site count.
861 *
862 * @param null $userId Current user ID.
863 * @param bool $all_access Check if user has access to all sites.
864 *
865 * @return int Child site count.
866 *
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.
872 */
873 public function get_websites_count( $userId = null, $all_access = false ) {
874 static $total_sites;
875 if ( null !== $total_sites ) { // NOSONAR -- static value.
876 return $total_sites;
877 }
878 if ( ( null === $userId ) && MainWP_System::instance()->is_multi_user() ) {
879
880 /**
881 * Current user global.
882 *
883 * @global string
884 */
885 global $current_user;
886
887 $userId = $current_user->ID;
888 }
889 $where = ( null === $userId ? '' : ' wp.userid = ' . intval( $userId ) );
890 if ( ! $all_access ) {
891 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
892 }
893 $table_name = esc_sql( $this->table_name( 'wp' ) );
894 $qry = "SELECT COUNT(wp.id) FROM {$table_name} wp WHERE 1 {$where}";
895
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()
897 $total_sites = $total;// NOSONAR -- static value.
898 return $total;
899 }
900
901
902 /**
903 * Get child sites stats count.
904 *
905 * @param array $params Params.
906 */
907 public function get_websites_stats_count( $params = array() ) {
908 if ( ! is_array( $params ) ) {
909 $params = array();
910 }
911
912 if ( isset( $params['all_access'] ) ) {
913 $all_access = ! empty( $params['all_access'] ) ? true : false;
914 } else {
915 $all_access = true;
916 }
917
918 $where = '';
919 if ( ! $all_access ) {
920 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
921 }
922
923 $select_stats = ' ( SELECT COUNT(wp.id) as count_all ';
924 if ( ! empty( $params['count_disconnected'] ) ) {
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 ';
926 $select_stats .= ' ON wp_disconnected.id = wp_sync.wpid WHERE wp_sync.sync_errors <> "" ) as count_disconnected ';
927 }
928 if ( ! empty( $params['count_suspended'] ) ) {
929 $select_stats .= ',( SELECT COUNT(wp_suspended.id) FROM ' . $this->table_name( 'wp' ) . ' wp_suspended WHERE wp_suspended.suspended = 1 ) as count_suspended ';
930 }
931 $qry = 'SELECT * FROM ' . $select_stats;
932 $qry .= ' FROM ' . $this->table_name( 'wp' ) . ' wp ' . $where . ' ) as wp_stats ';
933
934 return $this->wpdb->get_row( $qry, ARRAY_A ); //phpcs:ignore -- ok.
935 }
936
937 /**
938 * Get Child site wp_options database table.
939 *
940 * @param array $website Child Site array.
941 * @param mixed $option Child Site wp_options table name.
942 * @param mixed $default_value default value.
943 * @param mixed $json_format Is json format value.
944 *
945 * @return string|null Database query result (as string), or null on failure.
946 */
947 public function get_website_option( $website, $option, $default_value = null, $json_format = false ) { //phpcs:ignore -- NOSONAR - complex.
948
949 if ( is_array( $website ) ) {
950 if ( isset( $website[ $option ] ) ) {
951 $value = $website[ $option ];
952 if ( true === $json_format ) {
953 $value = ! empty( $value ) ? json_decode( $value, true ) : array();
954 return is_array( $value ) ? $value : array();
955 } else {
956 return $value;
957 }
958 }
959 $site_id = $website['id'];
960 } elseif ( is_object( $website ) ) {
961 if ( property_exists( $website, $option ) ) {
962 $value = $website->{$option};
963 if ( true === $json_format ) {
964 $value = ! empty( $value ) ? json_decode( $value, true ) : array();
965 return is_array( $value ) ? $value : array();
966 } else {
967 return $value;
968 }
969 }
970 $site_id = $website->id;
971 } elseif ( is_numeric( $website ) ) { // to support $site_id = 0, for global options.
972 $site_id = $website;
973 } else {
974 return false;
975 }
976
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.
979
980 if ( null === $value && null !== $default_value ) {
981 return $default_value;
982 }
983
984 if ( true === $json_format ) {
985 $value = ! empty( $value ) ? json_decode( $value, true ) : array();
986 return is_array( $value ) ? $value : array();
987 } else {
988 return $value;
989 }
990 }
991
992 /**
993 * Get Child site wp_options json value.
994 *
995 * @since 5.1.1
996 *
997 * @param array $website Child Site array.
998 * @param mixed $option Child Site wp_options table name.
999 * @param mixed $default_value default value.
1000 *
1001 * @return string|null Database query result (as string), or null on failure.
1002 */
1003 public function get_json_website_option( $website, $option, $default_value = null ) {
1004 return $this->get_website_option( $website, $option, $default_value, true );
1005 }
1006
1007 /**
1008 * Get child site options.
1009 *
1010 * @param array $website Child site.
1011 * @param mixed $options Child site options name.
1012 *
1013 * @return string|null Database query result (as string), or null on failure.
1014 */
1015 public function get_website_options_array( &$website, $options ) { // phpcs:ignore -- NOSONAR - complex.
1016
1017 if ( ! is_array( $options ) || empty( $options ) ) {
1018 return array();
1019 }
1020
1021 if ( is_array( $website ) ) {
1022 $site_id = $website['id'];
1023 } elseif ( is_object( $website ) ) {
1024 $site_id = $website->id;
1025 } elseif ( is_numeric( $website ) ) { // to support $site_id = 0 for global options.
1026 $site_id = $website;
1027 } else {
1028 return array();
1029 }
1030
1031 $arr_options = array();
1032 $get_options = array();
1033
1034 foreach ( $options as $option ) {
1035 if ( is_array( $website ) ) {
1036 if ( isset( $website[ $option ] ) ) {
1037 $arr_options[ $option ] = $website[ $option ];
1038 } else {
1039 $get_options[] = $option;
1040 }
1041 } elseif ( is_object( $website ) ) {
1042 if ( property_exists( $website, $option ) ) {
1043 $arr_options[ $option ] = $website->{$option};
1044 } else {
1045 $get_options[] = $option;
1046 }
1047 } else {
1048 $get_options[] = $option;
1049 }
1050 }
1051
1052 if ( empty( $get_options ) ) {
1053 return $arr_options; // all options.
1054 }
1055
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.
1059
1060 $fill_options = array(
1061 'primary_lasttime_backup',
1062 );
1063
1064 foreach ( (array) $options_db as $o ) {
1065 $arr_options[ $o->name ] = $o->value;
1066 if ( in_array( $o->name, $fill_options ) ) {
1067 if ( is_array( $website ) ) {
1068 if ( ! isset( $website[ $o->name ] ) ) {
1069 $website[ $o->name ] = $o->value;
1070 }
1071 } elseif ( is_object( $website ) ) {
1072 if ( ! property_exists( $website, $o->name ) ) {
1073 $website->{$o->name} = $o->value;
1074 }
1075 }
1076 }
1077 }
1078 return $arr_options;
1079 }
1080
1081 /**
1082 * Update child site options.
1083 *
1084 * @param object $website Child site object.
1085 * @param mixed $option Option to update.
1086 * @param mixed $value Value to update with.
1087 */
1088 public function update_website_option( $website, $option, $value ) {
1089
1090 if ( is_numeric( $website ) ) {
1091 $site_id = intval( $website );
1092 } else {
1093 $site_id = $website->id;
1094 }
1095
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.
1098 if ( empty( $rslt ) ) {
1099 $this->wpdb->insert(
1100 $this->table_name( 'wp_options' ),
1101 array(
1102 'wpid' => $site_id,
1103 'name' => $option,
1104 'value' => $value,
1105 )
1106 );
1107 } else {
1108 $this->wpdb->update(
1109 $this->table_name( 'wp_options' ),
1110 array( 'value' => $value ),
1111 array(
1112 'wpid' => $site_id,
1113 'name' => $option,
1114 )
1115 );
1116 }
1117 }
1118
1119
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 /**
1150 * Get general Child site option.
1151 *
1152 * @param mixed $option Child Site option name.
1153 *
1154 * @return string|null Database query result (as string), or null on failure.
1155 */
1156 private function get_general_website_option( $option ) {
1157
1158 if ( null !== static::$general_options ) {
1159 if ( isset( static::$general_options[ $option ] ) ) {
1160 return static::$general_options[ $option ];
1161 }
1162 } else {
1163 static::$general_options[] = array();
1164 }
1165
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.
1168
1169 static::$general_options[ $option ] = $val;
1170 return $val;
1171 }
1172
1173 /**
1174 * Get child site options.
1175 *
1176 * @param mixed $options Child site options name.
1177 *
1178 * @return string|null Database query result (as string), or null on failure.
1179 */
1180 public function get_general_options_array( $options ) {
1181
1182 if ( ! is_array( $options ) || empty( $options ) ) {
1183 return array();
1184 }
1185
1186 $return_options = array();
1187 if ( null !== static::$general_options ) {
1188 foreach ( static::$general_options as $opt => $val ) {
1189 if ( in_array( $opt, $options ) ) {
1190 $return_options[ $opt ] = $val;
1191 }
1192 }
1193 } else {
1194 static::$general_options[] = array();
1195 }
1196
1197 $diff_options = array();
1198 foreach ( $options as $opt ) {
1199 if ( ! isset( $return_options[ $opt ] ) ) {
1200 $diff_options[] = $opt;
1201 }
1202 }
1203
1204 if ( empty( $diff_options ) ) {
1205 return $return_options;
1206 }
1207
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.
1211
1212 foreach ( (array) $options_db as $o ) {
1213 $return_options[ $o->name ] = $o->value;
1214 static::$general_options[ $o->name ] = $o->value;
1215 }
1216 return $return_options;
1217 }
1218
1219 /**
1220 * Update general site options.
1221 *
1222 * @param mixed $option Option to update.
1223 * @param mixed $value Value to update with.
1224 * @param string $type_value Type values: single|array.
1225 */
1226 public function update_general_option( $option, $value, $type_value = 'single' ) {
1227
1228 if ( 'array' === $type_value ) {
1229 if ( empty( $value ) ) {
1230 $value = array();
1231 } elseif ( ! is_array( $value ) ) {
1232 return false;
1233 }
1234 $value = wp_json_encode( $value );
1235 }
1236
1237 if ( null === static::$general_options ) {
1238 static::$general_options[] = array();
1239 }
1240 static::$general_options[ $option ] = $value;
1241
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.
1244
1245 if ( empty( $rslt ) ) {
1246 $this->wpdb->insert(
1247 $this->table_name( 'wp_options' ),
1248 array(
1249 'wpid' => 0,
1250 'name' => $option,
1251 'value' => $value,
1252 )
1253 );
1254 } else {
1255 $this->wpdb->update(
1256 $this->table_name( 'wp_options' ),
1257 array( 'value' => $value ),
1258 array(
1259 'wpid' => 0,
1260 'name' => $option,
1261 )
1262 );
1263 }
1264 return true;
1265 }
1266
1267 /**
1268 * Get general Child site option.
1269 *
1270 * @param mixed $opt Child Site option name.
1271 * @param string $type_value Type values: single|array.
1272 *
1273 * @return string|null Database query result (as string), or null on failure.
1274 */
1275 public function get_general_option( $opt, $type_value = 'single' ) {
1276 if ( 'single' === $type_value ) {
1277 return $this->get_general_website_option( $opt );
1278 } elseif ( 'array' === $type_value ) {
1279 $json_value = $this->get_general_website_option( $opt );
1280 if ( empty( $json_value ) ) {
1281 return array();
1282 }
1283 return json_decode( $json_value, true );
1284 }
1285 return false;
1286 }
1287
1288 /**
1289 * Get child sites by user ID.
1290 *
1291 * @param int $userid User ID.
1292 * @param bool $selectgroups Selected groups.
1293 * @param null $search_site Site search field value.
1294 * @param string $orderBy Order list by. Default: URL.
1295 *
1296 * @return array|object|null Database query results or null on failer.
1297 */
1298 public function get_websites_by_user_id( $userid, $selectgroups = false, $search_site = null, $orderBy = 'wp.url' ) {
1299 return $this->get_results_result( $this->get_sql_websites_by_user_id( $userid, $selectgroups, $search_site, $orderBy ) );
1300 }
1301
1302 /**
1303 * Get child sites.
1304 *
1305 * @return string SQL string.
1306 */
1307 public function get_sql_websites() {
1308 $where = $this->get_sql_where_allow_access_sites( 'wp' );
1309
1310 $view_selects = '';
1311 $view_joins = '';
1312
1313 $opts_view = $this->get_wp_options_join();
1314
1315 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
1316 $view_selects = ',' . $opts_view['selects'];
1317 $view_joins = $opts_view['joins'];
1318 }
1319
1320 return 'SELECT wp.*,wp_sync.*' . $view_selects . '
1321 FROM ' . $this->table_name( 'wp' ) . ' wp
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';
1325 }
1326
1327 /**
1328 * Get child sites by user id via SQL.
1329 *
1330 * @param int $userid Given user ID.
1331 * @param bool $selectgroups Selected groups. Default: false.
1332 * @param null $search_site Site search field value. Default: null.
1333 * @param string $orderBy Order list by. Default: URL.
1334 * @param bool $offset Query offset. Default: false.
1335 * @param bool $rowcount Row count. Default: falese.
1336 *
1337 * @return object|null Return database query or null on failure.
1338 *
1339 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
1340 */
1341 public function get_sql_websites_by_user_id( $userid, $selectgroups = false, $search_site = null, $orderBy = 'wp.url', $offset = false, $rowcount = false ) {
1342 if ( MainWP_Utility::ctype_digit( $userid ) ) {
1343 $where = '';
1344 if ( null !== $search_site ) {
1345 $search_site = trim( $search_site );
1346 $where = ' AND (wp.name LIKE "%' . $search_site . '%" OR wp.url LIKE "%' . $search_site . '%") ';
1347 }
1348
1349 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
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
1361 if ( $selectgroups ) {
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
1363 FROM ' . $this->table_name( 'wp' ) . ' wp
1364 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1365 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1366 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1367 ' . $view_joins . '
1368 WHERE wp.userid = ' . $userid . "
1369 $where
1370 GROUP BY wp.id, wp_sync.sync_id
1371 ORDER BY " . $orderBy;
1372 } else {
1373 $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . '
1374 FROM ' . $this->table_name( 'wp' ) . ' wp
1375 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1376 ' . $view_joins . '
1377 WHERE wp.userid = ' . $userid . "
1378 $where
1379 ORDER BY " . $orderBy;
1380 }
1381
1382 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1383 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
1384 } elseif ( false !== $rowcount ) {
1385 $qry .= ' LIMIT ' . $rowcount;
1386 }
1387
1388 return $qry;
1389 }
1390
1391 return null;
1392 }
1393
1394 /**
1395 * Get SQL to get child sites for current user.
1396 *
1397 * @param bool $selectgroups Selected groups. Default: false.
1398 * @param null $search_site Site search field value. Default: null.
1399 * @param string $orderBy Order list by. Default: URL.
1400 * @param bool $offset Query offset. Default: false.
1401 * @param bool $rowcount Row count. Default: false.
1402 * @param null $extraWhere Extra WHERE. Default: null.
1403 * @param bool $for_manager For role manager. Default: false.
1404 * @param mixed $extra_view Extra view. Default favi_icon.
1405 * @param string $is_staging yes|no Is child site a staging site.
1406 * @param array $params other params.
1407 *
1408 * @return object|null Database query results or null on failure.
1409 *
1410 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
1411 */
1412 public function get_sql_websites_for_current_user( // phpcs:ignore -- NOSONAR - complex.
1413 $selectgroups = false,
1414 $search_site = null,
1415 $orderBy = 'wp.url',
1416 $offset = false,
1417 $rowcount = false,
1418 $extraWhere = null,
1419 $for_manager = false,
1420 $extra_view = array( 'favi_icon' ),
1421 $is_staging = 'no',
1422 $params = array()
1423 ) {
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
1432 $where = '';
1433 if ( MainWP_System::instance()->is_multi_user() ) {
1434
1435 /**
1436 * Current user global.
1437 *
1438 * @global string
1439 */
1440 global $current_user;
1441
1442 $where .= ' AND wp.userid = ' . $current_user->ID . ' ';
1443 }
1444
1445 if ( null !== $search_site ) {
1446 $search_site = trim( $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 );
1454 }
1455
1456 if ( ! empty( $extraWhere ) ) {
1457 $where .= ' AND ' . $extraWhere . ' ';
1458 }
1459
1460 if ( ! $for_manager ) {
1461 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
1462 }
1463
1464 $connected_sql = '';
1465
1466 if ( is_array( $params ) && isset( $params['connected'] ) && 'yes' === $params['connected'] ) {
1467 $connected_sql = ' AND wp_sync.sync_errors = "" ';
1468 } elseif ( is_array( $params ) && isset( $params['connected'] ) && 'no' === $params['connected'] ) {
1469 $connected_sql = ' AND wp_sync.sync_errors <> "" ';
1470 }
1471
1472 $use_comp_subquery = false;
1473
1474 $limit = '';
1475 if ( $params && is_array( $params ) ) {
1476 $s = isset( $params['s'] ) ? $params['s'] : '';
1477 $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
1478 $include = isset( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array();
1479 $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array();
1480 $page = isset( $params['page'] ) ? intval( $params['page'] ) : false;
1481 $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false;
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
1486 if ( ! empty( $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}' ) ";
1494 }
1495
1496 if ( ! empty( $exclude ) ) {
1497 $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') ';
1498 }
1499
1500 if ( ! empty( $include ) ) {
1501 $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') ';
1502 }
1503
1504 if ( ! empty( $_included_cache_ids ) ) {
1505 $where .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') ';
1506 }
1507
1508 // any, connected, disconnected, suspended, available_update.
1509 if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) {
1510 $status_conds = array();
1511 if ( in_array( 'available_update', $status ) ) {
1512 $available_sql = " ( wp.plugin_upgrades <> '' && wp.plugin_upgrades <> '[]' ) OR ( wp.theme_upgrades <> '' && wp.theme_upgrades <> '[]' ) OR ( wp.translation_upgrades <> '' && wp.translation_upgrades <> '[]' ) OR ( wp.premium_upgrades <> '' && wp.premium_upgrades <> '[]' ) ";
1513 $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.
1515 if ( $results ) {
1516 $wp_ids = array();
1517 foreach ( $results as $item ) {
1518 if ( ! empty( $item->wpid ) ) {
1519 $wp_ids[] = $item->wpid;
1520 }
1521 }
1522 $wp_ids = ! empty( $wp_ids ) ? array_unique( $wp_ids ) : array();
1523 if ( ! empty( $wp_ids ) ) {
1524 $available_sql .= ' OR wp.id IN ( ' . implode( ',', $wp_ids ) . ' )';
1525 }
1526 }
1527 $status_conds[] = ' ( ' . $available_sql . ') ';
1528 }
1529
1530 if ( in_array( 'connected', $status ) ) {
1531 $status_conds[] = ' ( wp_sync.sync_errors = "" ) ';
1532 }
1533 if ( in_array( 'disconnected', $status ) ) {
1534 $status_conds[] = " wp_sync.sync_errors <> '' ";
1535 }
1536
1537 if ( in_array( 'suspended', $status ) ) {
1538 $status_conds[] = ' wp.suspended = 1 ';
1539 }
1540
1541 if ( ! empty( $status_conds ) ) {
1542 $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) ';
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 }
1548 }
1549
1550 if ( ! empty( $page ) && ! empty( $per_page ) ) {
1551 $limit = ( $page - 1 ) * $per_page . ',' . $per_page;
1552 }
1553 $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
1554 }
1555
1556 if ( 'wp.url' === $orderBy ) {
1557 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
1558 }
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
1575 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1576 if ( $selectgroups ) {
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,
1578 wpclient.name as client_name
1579 FROM ' . $this->table_name( 'wp' ) . ' wp
1580 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1581 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1582 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1583 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1584 ' . $view_joins . '
1585 WHERE 1 ' . $where . $connected_sql . '
1586 GROUP BY wp.id, wp_sync.sync_id
1587 ORDER BY ' . $orderBy;
1588 } else {
1589 $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ', wpclient.name as client_name
1590 FROM ' . $this->table_name( 'wp' ) . ' wp
1591 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1592 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1593 ' . $view_joins . '
1594 WHERE 1 ' . $where . $connected_sql . '
1595 GROUP BY wp.id, wp_sync.sync_id
1596 ORDER BY ' . $orderBy;
1597 }
1598
1599 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1600 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
1601 } elseif ( false !== $rowcount ) {
1602 $qry .= ' LIMIT ' . $rowcount;
1603 } elseif ( ! empty( $limit ) ) {
1604 $qry .= ' LIMIT ' . $limit;
1605 } else {
1606 // load all sites so check to support limit sites loading.
1607 $limit_sites = ! empty( $params['limit_sites'] ) ? intval( $params['limit_sites'] ) : 0;
1608 if ( ! empty( $limit_sites ) ) {
1609 $current_page = (int) get_option( 'mainwp_manage_updates_limit_current_page', 0 );
1610 $current_page = $current_page > 0 ? $current_page - 1 : 0;
1611 $start = $current_page * $limit_sites;
1612 $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites );
1613 }
1614 }
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
1621 return $qry;
1622 }
1623
1624
1625 /**
1626 * Get SQL to get wp child sites for current user.
1627 *
1628 * @since 4.3
1629 *
1630 * @param array $params params .
1631 *
1632 * @return object|null Database query results or null on failure.
1633 */
1634 public function get_sql_wp_for_current_user( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
1635 if ( ! is_array( $params ) ) {
1636 $params = array();
1637 }
1638
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;
1649
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();
1651 $extra_select_sql_fields = isset( $params['extra_select_sql_fields'] ) && ! empty( $params['extra_select_sql_fields'] ) ? $params['extra_select_sql_fields'] : '';
1652
1653 $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no';
1654 $count_only = isset( $params['count_only'] ) && $params['count_only'] ? true : false;
1655
1656 $where = '';
1657
1658 if ( null !== $search_site ) {
1659 $search_site = trim( $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 );
1667 }
1668
1669 if ( null !== $extraWhere ) {
1670 $where .= ' AND ' . $extraWhere;
1671 }
1672
1673 if ( ! $for_manager ) {
1674 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
1675 }
1676
1677 if ( 'wp.url' === $orderBy ) {
1678 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
1679 }
1680
1681 $select_wp_fields = $this->get_sql_select_wp_valid_fields( $extra_select_wp_fields );
1682
1683 if ( ! empty( $extra_select_sql_fields ) ) {
1684 $extra_select_sql_fields = ',' . $extra_select_sql_fields;
1685 }
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
1701 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1702 if ( $selectgroups ) {
1703 if ( $count_only ) {
1704 $select = ' COUNT(DISTINCT(wp.id)) ';
1705 } else {
1706 $select = $select_wp_fields . '
1707 ' . $extra_select_sql_fields . '
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 ';
1709 }
1710 $qry = 'SELECT ' . $select . '
1711 FROM ' . $this->table_name( 'wp' ) . ' wp
1712 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1713 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1714 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1715 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1716 ' . $view_joins . '
1717 ' . $extra_join . '
1718 WHERE 1 ' . $where;
1719 if ( ! $count_only ) {
1720 $qry .= ' GROUP BY wp.id, wp_sync.sync_id';
1721 }
1722 $qry .= ' ORDER BY ' . $orderBy;
1723 } else {
1724 if ( $count_only ) {
1725 $select = ' COUNT(DISTINCT(wp.id)) ';
1726 } else {
1727 $select = $select_wp_fields . '
1728 ' . $extra_select_sql_fields . '
1729 ,wp_sync.sync_errors' . $view_selects . ', wpclient.name as client_name ';
1730 }
1731 $qry = 'SELECT ' . $select . '
1732 FROM ' . $this->table_name( 'wp' ) . ' wp
1733 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1734 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1735 ' . $view_joins . '
1736 ' . $extra_join . '
1737 WHERE 1 ' . $where;
1738 if ( ! $count_only ) {
1739 $qry .= ' GROUP BY wp.id, wp_sync.sync_id';
1740 }
1741 $qry .= ' ORDER BY ' . $orderBy;
1742 }
1743
1744 if ( ! $count_only ) {
1745 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1746 $qry .= ' LIMIT ' . intval( $offset ) . ', ' . intval( $rowcount );
1747 } elseif ( false !== $rowcount ) {
1748 $qry .= ' LIMIT ' . intval( $rowcount );
1749 }
1750 }
1751 return $qry;
1752 }
1753 /**
1754 * Get SQL select websites fields.
1755 *
1756 * @since 4.3
1757 *
1758 * @param array $other_fields extra select wp fields .
1759 *
1760 * @return string sql string.
1761 */
1762 public function get_sql_select_wp_valid_fields( $other_fields = array() ) {
1763
1764 $allow_other_fields = array(
1765 'offline_checks_last',
1766 'offline_check_result', // 1 - online, -1 offline.
1767 'http_response_code',
1768 'disable_health_check',
1769 'health_threshold',
1770 'note',
1771 'statsUpdate',
1772 'directories',
1773 'plugin_upgrades',
1774 'theme_upgrades',
1775 'translation_upgrades',
1776 'premium_upgrades',
1777 'securityIssues',
1778 'themes',
1779 'ignored_themes',
1780 'plugins',
1781 'ignored_plugins',
1782 'users',
1783 'categories',
1784 'pluginDir',
1785 'automatic_update',
1786 'backup_before_upgrade',
1787 'mainwpdir',
1788 'is_ignoreCoreUpdates',
1789 'is_ignorePluginUpdates',
1790 'is_ignoreThemeUpdates',
1791 'verify_certificate',
1792 'force_use_ipv4',
1793 'ssl_version',
1794 'http_user',
1795 'http_pass',
1796 'wpe',
1797 'is_staging',
1798 'client_id',
1799 );
1800
1801 $default_fields = array( 'id', 'url', 'name', 'adminname', 'verify_certificate', 'ssl_version', 'http_user', 'http_pass', 'suspended' );
1802
1803 $select = ' ';
1804
1805 foreach ( $default_fields as $field ) {
1806 $select .= 'wp.' . $this->escape( $field ) . ',';
1807 }
1808 foreach ( $other_fields as $field ) {
1809 if ( ! in_array( $field, $allow_other_fields ) ) {
1810 continue;
1811 }
1812 if ( in_array( $field, $default_fields ) ) {
1813 continue;
1814 }
1815 $select .= 'wp.' . $this->escape( $field ) . ',';
1816 }
1817 $select = rtrim( $select, ',' );
1818 return $select;
1819 }
1820
1821 /**
1822 * Get child sites for current user.
1823 *
1824 * @param array $params to get sites. Default: array().
1825 *
1826 * @return array Results or null on failure.
1827 *
1828 * @uses \MainWP\Dashboard\MainWP_Utility::map_site()
1829 */
1830 public function get_websites_for_current_user( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
1831 if ( ! is_array( $params ) ) {
1832 $params = array();
1833 }
1834
1835 $selectgroups = isset( $params['selectgroups'] ) ? $params['selectgroups'] : false;
1836 $search_site = isset( $params['search_site'] ) ? $params['search_site'] : null;
1837 $orderBy = isset( $params['order_by'] ) ? $params['order_by'] : 'wp.url';
1838 $offset = isset( $params['offset'] ) ? $params['offset'] : false;
1839 $rowcount = isset( $params['rowcount'] ) ? $params['rowcount'] : false;
1840 $extraWhere = isset( $params['where'] ) ? $params['where'] : null;
1841 $extra_view = isset( $params['extra_view'] ) && is_array( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
1842 $is_staging = isset( $params['is_staging'] ) ? $params['is_staging'] : 'no';
1843 $full_data = isset( $params['full_data'] ) && $params['full_data'] && ( 'no' !== $params['full_data'] ) ? true : false;
1844 $select_data = isset( $params['select_data'] ) && is_array( $params['select_data'] ) ? $params['select_data'] : false;
1845 $format = isset( $params['format'] ) ? $params['format'] : '';
1846 $clients = isset( $params['client'] ) ? $params['client'] : '';
1847 $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array();
1848
1849 $for_manager = isset( $params['no_perm_check'] ) && true === $params['no_perm_check'];
1850
1851 $urlsWhere = '';
1852
1853 if ( isset( $params['urls'] ) && ! empty( $params['urls'] ) ) {
1854 $urls = explode( ';', $params['urls'] );
1855 foreach ( $urls as $url ) {
1856 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
1857 if ( '/' !== substr( $url, - 1 ) ) {
1858 $url .= '/';
1859 }
1860 $urlsWhere .= '"' . $this->escape( $url ) . '", ';
1861 }
1862 $urlsWhere = rtrim( $urlsWhere, ', ' );
1863 }
1864
1865 if ( ! empty( $urlsWhere ) ) {
1866 $urlsWhere = " ( replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') IN ( " . $urlsWhere . ') ) ';
1867
1868 if ( empty( $extraWhere ) ) {
1869 $extraWhere = $urlsWhere;
1870 } else {
1871 $extraWhere = $extraWhere . ' AND ' . $urlsWhere;
1872 }
1873 }
1874
1875 $clientWhere = '';
1876 if ( ! empty( $clients ) ) {
1877 $clients = explode( ';', $clients );
1878 foreach ( $clients as $client ) {
1879 if ( is_numeric( $client ) ) {
1880 $clientWhere .= intval( $client ) . ', ';
1881 }
1882 }
1883 $clientWhere = rtrim( $clientWhere, ', ' );
1884 }
1885
1886 if ( ! empty( $clientWhere ) ) {
1887 $clientWhere = ' ( wp.client_id IN ( ' . $clientWhere . ') ) ';
1888 if ( empty( $extraWhere ) ) {
1889 $extraWhere = $clientWhere;
1890 } else {
1891 $extraWhere = $extraWhere . ' AND ' . $clientWhere;
1892 }
1893 }
1894
1895 $args = array(
1896 's' => isset( $params['s'] ) ? $params['s'] : '',
1897 'exclude' => isset( $params['exclude'] ) && ! empty( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array(),
1898 'include' => isset( $params['include'] ) && ! empty( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array(),
1899 'status' => isset( $params['status'] ) && ! empty( $params['status'] ) ? wp_parse_list( $params['status'] ) : '',
1900 'page' => isset( $params['paged'] ) ? intval( $params['paged'] ) : false,
1901 'per_page' => isset( $params['items_per_page'] ) ? intval( $params['items_per_page'] ) : false,
1902 );
1903
1904 $data = array( 'id', 'url', 'name', 'client_id' );
1905
1906 if ( $full_data ) {
1907 $data = array(
1908 'id',
1909 'url',
1910 'name',
1911 'offline_checks_last',
1912 'offline_check_result', // 1 - online, -1 offline.
1913 'http_response_code',
1914 'disable_health_check',
1915 'health_threshold',
1916 'note',
1917 'dbsize',
1918 'plugin_upgrades',
1919 'theme_upgrades',
1920 'translation_upgrades',
1921 'securityIssues',
1922 'themes',
1923 'plugins',
1924 'automatic_update',
1925 'sync_errors',
1926 'dtsAutomaticSync',
1927 'dtsAutomaticSyncStart',
1928 'dtsSync',
1929 'dtsSyncStart',
1930 'last_post_gmt',
1931 'health_value',
1932 'phpversion',
1933 'wp_upgrades',
1934 'security_stats',
1935 'client_id',
1936 'adminname',
1937 'privkey',
1938 'http_user',
1939 'http_pass',
1940 'ssl_version',
1941 'signature_algo',
1942 'verify_method',
1943 'verify_certificate',
1944 'suspended',
1945 );
1946
1947 if ( ! in_array( 'security_stats', $extra_view ) ) {
1948 $extra_view[] = 'security_stats';
1949 }
1950 }
1951
1952 if ( ! empty( $select_data ) && is_array( $select_data ) ) {
1953 $data = $select_data;
1954 }
1955
1956 if ( $selectgroups ) {
1957 $data[] = 'wpgroups';
1958 $data[] = 'wpgroupids';
1959 }
1960
1961 if ( ! empty( $fields ) ) {
1962 $data = array_unique( array_merge( $fields, $data ) ); // to prevent difference fields name.
1963 }
1964
1965 $dbwebsites = array();
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
1970 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
1971
1972 $obj_data = MainWP_Utility::map_site( $website, $data );
1973
1974 if ( $full_data ) {
1975 $sum_upgrades = 0;
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 ) ) {
1980 $plugin_upgrades = json_decode( $obj_data->plugin_upgrades, true );
1981 if ( is_array( $plugin_upgrades ) ) {
1982 $sum_upgrades += count( $plugin_upgrades );
1983 }
1984 }
1985
1986 if ( ! empty( $obj_data->theme_upgrades ) ) {
1987 $theme_upgrades = json_decode( $obj_data->theme_upgrades, true );
1988 if ( is_array( $theme_upgrades ) ) {
1989 $sum_upgrades += count( $theme_upgrades );
1990 }
1991 }
1992
1993 if ( ! empty( $obj_data->wp_upgrades ) ) {
1994 $wp_upgrades = json_decode( $obj_data->wp_upgrades, true );
1995 if ( is_array( $wp_upgrades ) ) {
1996 $sum_upgrades += count( $wp_upgrades );
1997 }
1998 }
1999 $obj_data->sum_of_upgrades = $sum_upgrades;
2000 }
2001
2002 if ( 'array' === $format ) {
2003 $dbwebsites[] = $obj_data;
2004 } else {
2005 $dbwebsites[ $website->id ] = $obj_data;
2006 }
2007 }
2008 static::free_result( $websites );
2009 return $dbwebsites;
2010 }
2011
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 /**
2131 * Get the child sites the current user has searched for.
2132 *
2133 * @param array $params Query parameters.
2134 *
2135 * @return boolean|null $qry Database query results or null on failure.
2136 *
2137 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
2138 */
2139 public function get_sql_search_websites_for_current_user( $params ) { // phpcs:ignore -- NOSONAR - complex.
2140
2141 if ( ! is_array( $params ) ) {
2142 $params = array();
2143 }
2144
2145 $view = isset( $params['view'] ) ? $params['view'] : 'default'; // must be default.
2146 $selectgroups = isset( $params['selectgroups'] ) && $params['selectgroups'] ? true : false;
2147 $search_site = isset( $params['search'] ) ? trim( $params['search'] ) : null;
2148 $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url';
2149 $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false;
2150 $rowcount = isset( $params['rowcount'] ) ? intval( $params['rowcount'] ) : false;
2151 $extraWhere = isset( $params['extra_where'] ) ? $params['extra_where'] : null; // without AND prefix.
2152 $for_manager = isset( $params['for_manager'] ) && $params['for_manager'] ? true : false;
2153 $extra_view = isset( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
2154 $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no';
2155 $is_count = isset( $params['count_only'] ) && $params['count_only'] ? true : false;
2156 $group_ids = isset( $params['group_id'] ) && ! empty( $params['group_id'] ) ? $params['group_id'] : array();
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';
2159 $is_not = isset( $params['isnot'] ) && ! empty( $params['isnot'] ) ? true : false;
2160 $selected_sites = isset( $params['selected_sites'] ) ? $params['selected_sites'] : array();
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
2166 if ( ! is_array( $group_ids ) ) {
2167 $group_ids = array();
2168 }
2169
2170 // valid group ids.
2171 $group_ids = array_filter(
2172 $group_ids,
2173 function ( $e ) {
2174 if ( 'nogroups' === $e ) {
2175 return true;
2176 }
2177 return ( is_numeric( $e ) && 0 < $e ) ? true : false;
2178 }
2179 );
2180
2181 if ( ! is_array( $client_ids ) ) {
2182 $client_ids = array();
2183 }
2184
2185 // valid group ids.
2186 $client_ids = array_filter(
2187 $client_ids,
2188 function ( $e ) {
2189 if ( 'noclients' === $e ) {
2190 return true;
2191 }
2192 return is_numeric( $e ) && ! empty( $e ) ? true : false; // to valid client ids.
2193 }
2194 );
2195
2196 if ( $selectgroups ) {
2197 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
2198 if ( $staging_group && in_array( $staging_group, $group_ids ) ) {
2199 if ( empty( $group_ids ) ) {
2200 $is_staging = 'yes';
2201 } else {
2202 $is_staging = 'nocheckstaging';
2203 }
2204 }
2205 }
2206
2207 if ( ! is_array( $selected_sites ) ) {
2208 $selected_sites = array();
2209 }
2210 $selected_sites = MainWP_Utility::array_numeric_filter( $selected_sites );
2211
2212 $where = '';
2213 if ( MainWP_System::instance()->is_multi_user() ) {
2214
2215 /**
2216 * Current user global.
2217 *
2218 * @global string
2219 */
2220 global $current_user;
2221
2222 $where .= ' AND wp.userid = ' . $current_user->ID . ' ';
2223 }
2224
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 }
2235 }
2236
2237 // Search filtering.
2238 if ( null !== $search_site && '' !== $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 }
2258 }
2259
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';
2263 }
2264
2265 if ( ! $for_manager ) {
2266 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
2267 }
2268
2269 if ( $is_count ) {
2270 $orderBy = '';
2271 } elseif ( 'wp.url' === $orderBy ) {
2272 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
2273 }
2274
2275 if ( ! empty( $orderBy ) ) {
2276 $orderBy = ' ORDER BY ' . $orderBy;
2277 }
2278
2279 $join_group = '';
2280 $where_group = '';
2281 $having_group = '';
2282
2283 if ( in_array( 'nogroups', $group_ids ) ) {
2284 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2285 $group_ids = array_filter(
2286 $group_ids,
2287 function ( $e ) {
2288 return 'nogroups' !== $e;
2289 }
2290 );
2291 if ( ! empty( $group_ids ) ) {
2292 $groups = implode( ',', $group_ids );
2293 $groups_count = count( $group_ids );
2294 if ( $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 ';
2305 } else {
2306 $where_group = ' AND ( wpgroup.groupid IS NULL OR wpgroup.groupid IN (' . $groups . ') ) ';
2307 }
2308 } elseif ( $is_not ) {
2309 $where_group = ' AND wpgroup.groupid IS NOT NULL ';
2310 } else {
2311 $where_group = ' AND wpgroup.groupid IS NULL ';
2312 }
2313 } elseif ( $group_ids ) {
2314 $groups = implode( ',', $group_ids );
2315 $groups_count = count( $group_ids );
2316 if ( $is_not ) {
2317 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
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 }
2326 } else {
2327 $join_group = ' JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2328 $where_group = ' AND wpgroup.groupid IN (' . $groups . ') ';
2329 if ( 'and' === $group_logic ) {
2330 $having_group = 'COUNT(DISTINCT wpgroup.groupid) = ' . $groups_count;
2331 }
2332 }
2333 }
2334
2335 $select_groups_belong = '';
2336
2337 if ( ! $is_count && $group_ids ) {
2338 $select_groups_belong = $this->get_select_groups_belong();
2339 }
2340
2341 $join_client = '';
2342 $where_client = '';
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 }
2351 if ( in_array( 'noclients', $client_ids ) ) {
2352 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2353 $client_ids = array_filter(
2354 $client_ids,
2355 function ( $e ) {
2356 return 'noclients' !== $e;
2357 }
2358 );
2359 if ( ! empty( $client_ids ) ) {
2360 $clients = implode( ',', $client_ids );
2361 if ( $is_not ) {
2362 $where_client = ' AND wpclient.client_id IS NOT NULL AND wp.client_id NOT IN (' . $clients . ') ';
2363 } else {
2364 $where_client = ' AND wpclient.client_id IN (' . $clients . ') ';
2365 }
2366 } elseif ( $is_not ) {
2367 $where_client = ' AND wpclient.client_id IS NOT NULL ';
2368 } else {
2369 $where_client = ' AND wpclient.client_id IS NULL ';
2370 }
2371 } elseif ( $client_ids && ! empty( $client_ids ) ) {
2372 $clients = implode( ',', $client_ids );
2373 if ( $is_not ) {
2374 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2375 $where_client = ' AND ( wpclient.client_id NOT IN (' . $clients . ') OR wpclient.client_id IS NULL ) ';
2376 } else {
2377 $join_client = ' JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2378 $where_client = ' AND wpclient.client_id IN (' . $clients . ') ';
2379 }
2380 }
2381
2382 if ( '' === $join_client ) {
2383 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
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
2466 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
2467 if ( $selectgroups ) {
2468
2469 if ( empty( $join_group ) ) {
2470 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2471 }
2472
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 ' .
2474 $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' .
2475 $join_client . ' ' .
2476 $join_group .
2477 $join_monitors . '
2478 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgroup.groupid = gr.id
2479
2480 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2481 ' . $view_joins . '
2482 WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by .
2483 $orderBy;
2484 } else {
2485 $qry = 'SELECT ' . $select . $view_selects . ', wpclient.name as client_name ' .
2486 $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' .
2487 $join_group . ' ' .
2488 $join_client .
2489 $join_monitors . '
2490 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2491 ' . $view_joins . '
2492 WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by .
2493 $orderBy;
2494 }
2495
2496 if ( ( false !== $offset ) && ( false !== $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;
2501 } elseif ( false !== $rowcount ) {
2502 $qry .= ' LIMIT ' . $rowcount;
2503 }
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
2510 return $qry;
2511 }
2512
2513 /**
2514 * Get child sites where allowed access via SQL.
2515 *
2516 * @param string $site_table_alias Child site table alias.
2517 * @param string $is_staging yes|no Is child site a staging site.
2518 *
2519 * @return boolean|null $_where Database query results or null on failure.
2520 */
2521 public function get_sql_where_allow_access_sites( $site_table_alias = '', $is_staging = 'no' ) { // phpcs:ignore -- NOSONAR - complex.
2522
2523 if ( empty( $site_table_alias ) ) {
2524 $site_table_alias = $this->table_name( 'wp' );
2525 }
2526
2527 // check to filter the staging sites.
2528 $where_staging = ' AND ' . $site_table_alias . '.is_staging = 0 ';
2529 if ( 'no' === $is_staging ) {
2530 $where_staging = ' AND ' . $site_table_alias . '.is_staging = 0 ';
2531 } elseif ( 'yes' === $is_staging ) {
2532 $where_staging = ' AND ' . $site_table_alias . '.is_staging = 1 ';
2533 } elseif ( 'nocheckstaging' === $is_staging ) {
2534 $where_staging = '';
2535 }
2536 // end staging filter.
2537
2538 $_where = $where_staging;
2539 // To fix bug run from cron job.
2540 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
2541 return $_where;
2542 }
2543
2544 // To fix bug run from wp cli.
2545 if ( defined( 'WP_CLI' ) && WP_CLI ) {
2546 return $_where;
2547 }
2548
2549 // Run from Rest Api.
2550 if ( defined( 'MAINWP_REST_API_DOING' ) && MAINWP_REST_API_DOING ) {
2551 return $_where;
2552 }
2553
2554 /**
2555 * Filter: mainwp_currentuserallowedaccesssites
2556 *
2557 * Filters allowed sites for the current user.
2558 *
2559 * @since Unknown
2560 */
2561 $allowed_sites = apply_filters( 'mainwp_currentuserallowedaccesssites', 'all' );
2562
2563 if ( 'all' === $allowed_sites ) {
2564 return $_where;
2565 }
2566
2567 if ( is_array( $allowed_sites ) && ! empty( $allowed_sites ) ) {
2568 // valid group ids.
2569 $allowed_sites = array_filter(
2570 $allowed_sites,
2571 function ( $e ) {
2572 return is_numeric( $e ) ? true : false;
2573 }
2574 );
2575 $_where .= ' AND ' . $site_table_alias . '.id IN (' . implode( ',', $allowed_sites ) . ') ';
2576 } else {
2577 $_where .= ' AND 0 ';
2578 }
2579
2580 return $_where;
2581 }
2582
2583 /**
2584 * Get groupd where allowed access via SQL.
2585 *
2586 * @param string $group_table_alias Child site table alias.
2587 * @param string $with_staging yes|no Is child site a staging site.
2588 *
2589 * @return boolean|null $_where Database query results or null on failer.
2590 */
2591 public function get_sql_where_allow_groups( $group_table_alias = '', $with_staging = 'no' ) { // phpcs:ignore -- NOSONAR - complex.
2592
2593 if ( empty( $group_table_alias ) ) {
2594 $group_table_alias = $this->table_name( 'group' );
2595 }
2596
2597 // check to filter the staging group.
2598 $where_staging_group = '';
2599 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
2600 if ( $staging_group ) {
2601 $where_staging_group = ' AND ' . $group_table_alias . '.id <> ' . $staging_group . ' ';
2602 if ( 'yes' === $with_staging ) {
2603 $where_staging_group = '';
2604 }
2605 }
2606
2607 // end staging filter.
2608 $_where = $where_staging_group;
2609
2610 // To fix bug run from cron job.
2611 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
2612 return $_where;
2613 }
2614
2615 // Run from wp cli.
2616 if ( defined( 'WP_CLI' ) && WP_CLI ) {
2617 return $_where;
2618 }
2619
2620 // Run from Rest Api.
2621 if ( defined( 'MAINWP_REST_API_DOING' ) && MAINWP_REST_API_DOING ) {
2622 return $_where;
2623 }
2624
2625 /**
2626 * Filter: mainwp_currentuserallowedaccessgroups
2627 *
2628 * Filters allowed groups for the current user.
2629 *
2630 * @since Unknown
2631 */
2632 $allowed_groups = apply_filters( 'mainwp_currentuserallowedaccessgroups', 'all' );
2633
2634 if ( 'all' === $allowed_groups ) {
2635 return $_where;
2636 }
2637
2638 if ( is_array( $allowed_groups ) && ! empty( $allowed_groups ) ) {
2639
2640 // valid group ids.
2641 $allowed_groups = array_filter(
2642 $allowed_groups,
2643 function ( $e ) {
2644 return is_numeric( $e ) ? true : false;
2645 }
2646 );
2647
2648 return ' AND ' . $group_table_alias . '.id IN (' . implode( ',', $allowed_groups ) . ') ' . $_where;
2649 } else {
2650 return ' AND 0 ';
2651 }
2652 }
2653
2654
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 /**
2733 * Get child site by id.
2734 *
2735 * @param int $id Child site ID.
2736 * @param array $selectGroups Select groups.
2737 * @param array $extra_view Get extra option fields.
2738 * @param int $obj OBJECT|ARRAY_A.
2739 *
2740 * @return object|null Database query results or null on failure.
2741 */
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 );
2744 }
2745
2746 /**
2747 * Get child site by id via SQL.
2748 *
2749 * @param int $id Child site ID.
2750 * @param bool $selectGroups Selected groups.
2751 * @param mixed $extra_view Extra view value.
2752 *
2753 * @return object|null Database query result or null on failure.
2754 *
2755 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
2756 */
2757 public function get_sql_website_by_id( $id, $selectGroups = false, $extra_view = array() ) {
2758
2759 if ( ! is_array( $extra_view ) || empty( $extra_view ) ) {
2760 $extra_view = array( 'favi_icon', 'site_info' );
2761 }
2762
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
2775 $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' );
2776 if ( $selectGroups ) {
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
2778 FROM ' . $this->table_name( 'wp' ) . ' wp
2779 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
2780 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
2781 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2782 ' . $view_joins . '
2783 WHERE wp.id = ' . $id . $where . '
2784 GROUP BY wp.id, wp_sync.sync_id';
2785 }
2786
2787 return 'SELECT wp.*,wp_sync.*' . $view_selects . '
2788 FROM ' . $this->table_name( 'wp' ) . ' wp
2789 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2790 ' . $view_joins . '
2791 WHERE id = ' . $id . $where;
2792 }
2793
2794 return null;
2795 }
2796
2797 /**
2798 * Method get_websites_by_ids()
2799 *
2800 * Get child sites by child site IDs.
2801 *
2802 * @param array $ids Child site IDs.
2803 * @param int $userId User ID.
2804 *
2805 * @return object|null Database query result or null on failure.
2806 *
2807 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
2808 */
2809 public function get_websites_by_ids( $ids, $userId = null ) {
2810 if ( ( null === $userId ) && MainWP_System::instance()->is_multi_user() ) {
2811
2812 /**
2813 * Current user global.
2814 *
2815 * @global string
2816 */
2817 global $current_user;
2818
2819 $userId = $current_user->ID;
2820 }
2821
2822 // valid group ids.
2823 $ids = array_filter(
2824 $ids,
2825 function ( $e ) {
2826 return ( is_numeric( $e ) && 0 < $e ) ? true : false;
2827 }
2828 );
2829
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;
2835
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
2844 }
2845
2846 /**
2847 * Get child sites by groups IDs.
2848 *
2849 * @param array $ids Groups IDs.
2850 * @param int $userId User ID.
2851 * @param array $fields array fields .
2852 *
2853 * @return object|null Database query result or null on failure.
2854 *
2855 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
2856 */
2857 public function get_websites_by_group_ids( $ids, $userId = null, $fields = array() ) {
2858 if ( empty( $ids ) ) {
2859 return array();
2860 }
2861 if ( ( null === $userId ) && MainWP_System::instance()->is_multi_user() ) {
2862
2863 /**
2864 * Current user global.
2865 *
2866 * @global string
2867 */
2868 global $current_user;
2869
2870 $userId = $current_user->ID;
2871 }
2872
2873 // valid group ids.
2874 $group_ids = array_filter(
2875 $ids,
2876 function ( $e ) {
2877 return is_numeric( $e ) ? true : false;
2878 }
2879 );
2880
2881 $select = '*';
2882 if ( ! empty( $fields ) && is_array( $fields ) ) {
2883 $fields = array_filter( array_map( 'trim', $fields ) );
2884 if ( $fields ) {
2885 $select = '';
2886 foreach ( $fields as $field ) {
2887 $select .= $this->escape( $field ) . ',';
2888 }
2889 $select = rtrim( $select, ',' );
2890 }
2891 }
2892 return $this->wpdb->get_results( 'SELECT ' . $select . ' FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid WHERE wpgroup.groupid IN (' . implode( ',', $group_ids ) . ') ' . ( null !== $userId ? ' AND wp.userid = ' . intval( $userId ) : '' ), OBJECT );
2893 }
2894
2895 /**
2896 * Get child sites by group ID.
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 *
2938 * @param int $id Group ID.
2939 *
2940 * @return int Number of sites in the group.
2941 *
2942 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
2943 */
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.
2967 }
2968
2969
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 /**
2997 * Get child sites by group id via SQL.
2998 *
2999 * @param int $id Group ID.
3000 * @param bool $selectgroups Selected groups. Default: false.
3001 * @param string $orderBy Order list by. Default: URL.
3002 * @param bool $offset Query offset. Default: false.
3003 * @param bool $rowcount Row count. Default: falese.
3004 * @param null $where SQL WHERE value.
3005 * @param null $search_site Site search field value. Default: null.
3006 * @param array $others Others params.
3007 *
3008 * @return object|null Return database query or null on failure.
3009 *
3010 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3011 */
3012 public function get_sql_websites_by_group_id( // phpcs:ignore -- NOSONAR - complex.
3013 $id,
3014 $selectgroups = false,
3015 $orderBy = 'wp.url',
3016 $offset = false,
3017 $rowcount = false,
3018 $where = null,
3019 $search_site = null,
3020 $others = array()
3021 ) {
3022
3023 $is_staging = 'no';
3024 if ( $selectgroups ) {
3025 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
3026 if ( $staging_group && $id === $staging_group ) {
3027 $is_staging = 'yes';
3028 }
3029 }
3030
3031 $where_search = '';
3032 if ( ! empty( $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 );
3041 }
3042
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' );
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
3064 if ( MainWP_Utility::ctype_digit( $id ) ) {
3065 $where_allowed = $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
3066 if ( $selectgroups ) {
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
3068 FROM ' . $this->table_name( 'wp' ) . ' wp
3069 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
3070 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
3071 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
3072 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
3073 ' . $view_joins . '
3074 WHERE wpgroup.groupid = ' . $id . ' ' .
3075 ( empty( $where ) ? '' : ' AND ' . $where ) . $where_allowed . $where_search . '
3076 GROUP BY wp.id, wp_sync.sync_id
3077 ORDER BY ' . $orderBy;
3078 } else {
3079 $qry = 'SELECT wp.*' . $view_selects . ', wp_sync.* FROM ' . $this->table_name( 'wp' ) . ' wp
3080 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
3081 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
3082 ' . $view_joins . '
3083 WHERE wpgroup.groupid = ' . $id . ' ' . $where_allowed . $where_search .
3084 ( empty( $where ) ? '' : ' AND ' . $where ) . ' ORDER BY ' . $orderBy;
3085 }
3086 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
3087 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
3088 } elseif ( false !== $rowcount ) {
3089 $qry .= ' LIMIT ' . $rowcount;
3090 }
3091
3092 return $qry;
3093 }
3094
3095 return null;
3096 }
3097
3098 /**
3099 * Get child sites by group name.
3100 *
3101 * @param int $userid Current user ID.
3102 * @param string $groupname Group name.
3103 *
3104 * @return object|null Database query result or null on failure.
3105 */
3106 public function get_websites_by_group_name( $userid, $groupname ) {
3107 return $this->get_results_result( $this->get_sql_websites_by_group_name( $groupname, $userid ) );
3108 }
3109
3110 /**
3111 * Get child sites by group name.
3112 *
3113 * @param string $groupname Group name.
3114 * @param int $userid Current user ID.
3115 *
3116 * @return object|null Database query result or null on failure.
3117 *
3118 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
3119 */
3120 public function get_sql_websites_by_group_name( $groupname, $userid = null ) {
3121 if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
3122
3123 /**
3124 * Current user global.
3125 *
3126 * @global string
3127 */
3128 global $current_user;
3129
3130 $userid = $current_user->ID;
3131 }
3132
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
3144 INNER JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
3145 JOIN ' . $this->table_name( 'group' ) . ' g ON wpgroup.groupid = g.id
3146 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
3147 ' . $view_joins . '
3148 WHERE g.name="' . $this->escape( $groupname ) . '"';
3149 if ( null !== $userid ) {
3150 $sql .= ' AND g.userid = "' . intval( $userid ) . '"';
3151 }
3152
3153 return $sql;
3154 }
3155
3156 /**
3157 * Get child site IP address.
3158 *
3159 * @param int $wpid Child site ID.
3160 *
3161 * @return string|null Child site IP address or null on failure.
3162 */
3163 public function get_wp_ip( $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.
3166 }
3167
3168 /**
3169 * Add website to the MainWP Dashboard.
3170 *
3171 * @param int $userid Current user ID.
3172 * @param string $name Child site name.
3173 * @param string $url Child site URL.
3174 * @param string $admin Child site administrator username.
3175 * @param string $pubkey OpenSSL public key.
3176 * @param string $privkey OpenSSL private key.
3177 * @param array $params Other params.
3178 *
3179 * @return int|false Child site ID or false on failure.
3180 *
3181 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3182 */
3183 public function add_website( // phpcs:ignore -- NOSONAR - complex.
3184 $userid,
3185 $name,
3186 $url,
3187 $admin,
3188 $pubkey,
3189 $privkey,
3190 $params = array()
3191 ) {
3192
3193 if ( ! is_array( $params ) ) {
3194 $params = array();
3195 }
3196
3197 $groupids = isset( $params['groupids'] ) ? $params['groupids'] : array();
3198 $groupnames = isset( $params['groupnames'] ) ? $params['groupnames'] : array();
3199 $verifyCertificate = isset( $params['verifyCertificate'] ) ? (int) $params['verifyCertificate'] : 2;
3200 $uniqueId = isset( $params['uniqueId'] ) ? $params['uniqueId'] : '';
3201 $http_user = isset( $params['http_user'] ) ? $params['http_user'] : null;
3202 $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null;
3203 $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0;
3204 $wpe = isset( $params['wpe'] ) ? $params['wpe'] : 0;
3205 $isStaging = isset( $params['isStaging'] ) ? $params['isStaging'] : 0;
3206 // 2 means "use the global mainwp_forceUseIPv4 option" and mainwp_string_to_bool()
3207 // collapses it to 0, so it is kept before the boolean conversion. Null or absent
3208 // stays null and leaves the NOT NULL column at its default.
3209 $force_use_ipv4 = isset( $params['force_use_ipv4'] ) ? ( 2 === (int) $params['force_use_ipv4'] ? 2 : (int) mainwp_string_to_bool( $params['force_use_ipv4'] ) ) : null;
3210
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
3223 if ( MainWP_Utility::ctype_digit( $userid ) ) {
3224 if ( '/' !== substr( $url, - 1 ) ) {
3225 $url .= '/';
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
3231 $values = array(
3232 'userid' => $userid,
3233 'adminname' => $this->escape( $admin ),
3234 'name' => $this->escape( wp_strip_all_tags( $name ) ),
3235 'url' => $this->escape( $url ),
3236 'pubkey' => $this->escape( $pubkey ),
3237 'privkey' => $this->escape( base64_encode( $en_privkey ) ), // phpcs:ignore -- NOSONAR - trust.
3238 'siteurl' => '',
3239 'ga_id' => '',
3240 'gas_id' => 0,
3241 'offline_checks_last' => 0,
3242 'offline_check_result' => 0,
3243 'note' => '',
3244 'statsUpdate' => 0,
3245 'directories' => '',
3246 'plugin_upgrades' => '',
3247 'theme_upgrades' => '',
3248 'translation_upgrades' => '',
3249 'securityIssues' => '',
3250 'premium_upgrades' => '',
3251 'themes' => '',
3252 'ignored_themes' => '',
3253 'plugins' => '',
3254 'ignored_plugins' => '',
3255 'users' => '',
3256 'categories' => '',
3257 'pluginDir' => '',
3258 'automatic_update' => 0,
3259 'backup_before_upgrade' => 2,
3260 'verify_certificate' => intval( $verifyCertificate ),
3261 'ssl_version' => $sslVersion,
3262 'uniqueId' => $uniqueId,
3263 'mainwpdir' => 0,
3264 'http_user' => $encrypted_http_user,
3265 'http_pass' => $encrypted_http_pass,
3266 'wpe' => $wpe,
3267 'is_staging' => $isStaging,
3268 );
3269
3270 if ( null !== $force_use_ipv4 ) {
3271 $values['force_use_ipv4'] = $force_use_ipv4;
3272 }
3273
3274 $syncValues = array(
3275 'dtsSync' => 0,
3276 'dtsSyncStart' => 0,
3277 'dtsAutomaticSync' => 0,
3278 'dtsAutomaticSyncStart' => 0,
3279 'totalsize' => 0,
3280 'extauth' => '',
3281 'sync_errors' => '',
3282 );
3283 if ( $this->wpdb->insert( $this->table_name( 'wp' ), $values ) ) {
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 );
3287 $syncValues['wpid'] = $websiteid;
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() ) );
3290 $this->wpdb->insert(
3291 $this->table_name( 'wp_settings_backup' ),
3292 array(
3293 'wpid' => $websiteid,
3294 'archiveFormat' => 'global',
3295 )
3296 );
3297
3298 foreach ( $groupnames as $groupname ) {
3299 if ( $this->wpdb->insert(
3300 $this->table_name( 'group' ),
3301 array(
3302 'userid' => $userid,
3303 'name' => $this->escape( htmlspecialchars( $groupname ) ),
3304 )
3305 )
3306 ) {
3307 $groupids[] = $this->wpdb->insert_id;
3308 }
3309 }
3310 // add groupids.
3311 foreach ( $groupids as $groupid ) {
3312 $this->wpdb->insert(
3313 $this->table_name( 'wp_group' ),
3314 array(
3315 'wpid' => $websiteid,
3316 'groupid' => $groupid,
3317 )
3318 );
3319 }
3320 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3321 return $websiteid;
3322 }
3323 }
3324
3325 return false;
3326 }
3327
3328 /**
3329 * Remove child site from the MainWP Dashboard.
3330 *
3331 * @param int $websiteid Child site ID.
3332 *
3333 * @return int|boolean Return child site ID that was removed or false on failure.
3334 *
3335 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3336 */
3337 public function remove_website( $websiteid ) {
3338 if ( MainWP_Utility::ctype_digit( $websiteid ) ) {
3339 $nr = $this->wpdb->delete( $this->table_name( 'wp' ), array( 'id' => $websiteid ) );
3340 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3341 $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) );
3342 $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) );
3343 MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid );
3344 MainWP_DB_Uptime_Monitoring::instance()->delete_monitor( array( 'wpid' => $websiteid ) );
3345 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3346 return $nr;
3347 }
3348
3349 return false;
3350 }
3351
3352 /**
3353 * Update child site db values.
3354 *
3355 * @param int $websiteid Child site ID.
3356 * @param array $fields Database fields to update.
3357 *
3358 * @return int|boolean The number of rows updated, or false on error.
3359 */
3360 public function update_website_values( $websiteid, $fields ) {
3361 if ( ! empty( $fields ) ) {
3362 // MWP-1548: encrypt http_user / http_pass at rest if either
3363 // is present in $fields. Generic-fields updaters (clone
3364 // flow in extensions-handler, callers that pass arbitrary
3365 // column subsets) must go through the same fail-closed
3366 // contract as add_website / update_website.
3367 if ( array_key_exists( 'http_user', $fields ) ) {
3368 $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_user'], 'http_user' );
3369 if ( false === $encrypted ) {
3370 return false;
3371 }
3372 $fields['http_user'] = $encrypted;
3373 }
3374 if ( array_key_exists( 'http_pass', $fields ) ) {
3375 $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_pass'], 'http_pass' );
3376 if ( false === $encrypted ) {
3377 return false;
3378 }
3379 $fields['http_pass'] = $encrypted;
3380 }
3381 // Lock the data stream to prevent other processes from updating at the same time.
3382 $table_name = esc_sql( $this->table_name( 'wp' ) );
3383 $sql = $this->wpdb->prepare(
3384 "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3385 $websiteid
3386 );
3387 $this->wpdb->get_row( $sql );
3388
3389 return $this->wpdb->update( $this->table_name( 'wp' ), $fields, array( 'id' => $websiteid ) );
3390 }
3391
3392 return false;
3393 }
3394
3395 /**
3396 * Update child site sync values.
3397 *
3398 * @param int $websiteid Child site ID.
3399 * @param array $fields Database fields to update.
3400 *
3401 * @return int|boolean The number of rows updated, or false on error.
3402 */
3403 public function update_website_sync_values( $websiteid, $fields ) {
3404 if ( ! empty( $fields ) ) {
3405 return $this->wpdb->update( $this->table_name( 'wp_sync' ), $fields, array( 'wpid' => $websiteid ) );
3406 }
3407
3408 return false;
3409 }
3410
3411 /**
3412 * Update child site.
3413 *
3414 * @param int $websiteid Website ID.
3415 * @param string $url Child site URL.
3416 * @param int $userid Current user ID.
3417 * @param string $name Child site name.
3418 * @param string $siteadmin Child site administrator username.
3419 * @param array $groupids Group IDs.
3420 * @param array $groupnames Group Names.
3421 * @param string $pluginDir Plugin directory.
3422 * @param mixed $maximumFileDescriptorsOverride Overwrite the Maximum File Descriptors option.
3423 * @param mixed $maximumFileDescriptorsAuto Auto set the Maximum File Descriptors option.
3424 * @param mixed $maximumFileDescriptors Set the Maximum File Descriptors option.
3425 * @param int $verifyCertificate Whether or not to verify SSL Certificate.
3426 * @param mixed $archiveFormat Backup archive formate.
3427 * @param string $uniqueId Unique security ID.
3428 * @param string $http_user HTTP Basic Authentication username.
3429 * @param string $http_pass HTTP Basic Authentication password.
3430 * @param int $sslVersion SSL Version.
3431 * @param bool $disableHealthChecking Disable Site health threshold.
3432 * @param int $healthThreshold Site health threshold.
3433 * @param string $backup_method Primary backup method.
3434 *
3435 * @return boolean ture on success or false on failure.
3436 *
3437 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
3438 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3439 */
3440 public function update_website( // phpcs:ignore -- NOSONAR - complex.
3441 $websiteid,
3442 $url,
3443 $userid,
3444 $name,
3445 $siteadmin,
3446 $groupids,
3447 $groupnames,
3448 $pluginDir,
3449 $maximumFileDescriptorsOverride,
3450 $maximumFileDescriptorsAuto,
3451 $maximumFileDescriptors,
3452 $verifyCertificate = 1,
3453 $archiveFormat = 'global',
3454 $uniqueId = '',
3455 $http_user = null,
3456 $http_pass = null,
3457 $sslVersion = 0,
3458 $disableHealthChecking = 1,
3459 $healthThreshold = 0,
3460 $backup_method = 'global'
3461 ) {
3462
3463 $wpe = 0; // going to update when sync.
3464
3465 if ( MainWP_Utility::ctype_digit( $websiteid ) && MainWP_Utility::ctype_digit( $userid ) ) {
3466 $website = $this->get_website_by_id( $websiteid );
3467 if ( MainWP_System_Utility::can_edit_website( $website ) ) {
3468 // MWP-1548: encrypt http_user / http_pass at rest. Same
3469 // fail-closed contract as add_website -- a non-empty
3470 // value that fails to encrypt aborts the update so we
3471 // never overwrite an existing encrypted row with
3472 // plaintext when the encryption layer is unhealthy.
3473 $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' );
3474 $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' );
3475 if ( false === $encrypted_http_user || false === $encrypted_http_pass ) {
3476 return false;
3477 }
3478 // update admin.
3479 $this->wpdb->update(
3480 $this->table_name( 'wp' ),
3481 array(
3482 'url' => $url,
3483 'name' => wp_strip_all_tags( $name ),
3484 'adminname' => $siteadmin,
3485 'pluginDir' => $pluginDir,
3486 'verify_certificate' => intval( $verifyCertificate ),
3487 'ssl_version' => intval( $sslVersion ),
3488 'wpe' => intval( $wpe ),
3489 'uniqueId' => $uniqueId,
3490 'http_user' => $encrypted_http_user,
3491 'http_pass' => $encrypted_http_pass,
3492 'disable_health_check' => $disableHealthChecking,
3493 'health_threshold' => $healthThreshold,
3494 'primary_backup_method' => $backup_method,
3495 ),
3496 array( 'id' => $websiteid )
3497 );
3498 $this->wpdb->update(
3499 $this->table_name( 'wp_settings_backup' ),
3500 array( 'archiveFormat' => $archiveFormat ),
3501 array( 'wpid' => $websiteid )
3502 );
3503
3504 if ( get_option( 'mainwp_enableLegacyBackupFeature' ) ) {
3505 $this->wpdb->update(
3506 $this->table_name( 'wp' ),
3507 array(
3508 'maximumFileDescriptorsOverride' => (int) $maximumFileDescriptorsOverride,
3509 'maximumFileDescriptorsAuto' => (int) $maximumFileDescriptorsAuto,
3510 'maximumFileDescriptors' => (int) $maximumFileDescriptors,
3511 ),
3512 array( 'id' => $websiteid )
3513 );
3514 }
3515
3516 // remove groups.
3517 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3518 // Remove GA stats.
3519 $showErrors = $this->wpdb->hide_errors();
3520
3521 /**
3522 * Action: mainwp_ga_delete_site
3523 *
3524 * Fires upon site removal process in order to delete Google Analytics data.
3525 *
3526 * @param int $websiteid Child site ID.
3527 *
3528 * @since Unknown
3529 */
3530 do_action( 'mainwp_ga_delete_site', $websiteid );
3531
3532 if ( $showErrors ) {
3533 $this->wpdb->show_errors();
3534 }
3535 // add groups with groupnames.
3536 foreach ( $groupnames as $groupname ) {
3537 if ( $this->wpdb->insert(
3538 $this->table_name( 'group' ),
3539 array(
3540 'userid' => $userid,
3541 'name' => $this->escape( $groupname ),
3542 )
3543 )
3544 ) {
3545 $groupids[] = $this->wpdb->insert_id;
3546 }
3547 }
3548 // add groupids.
3549 foreach ( $groupids as $groupid ) {
3550 $this->wpdb->insert(
3551 $this->table_name( 'wp_group' ),
3552 array(
3553 'wpid' => $websiteid,
3554 'groupid' => $groupid,
3555 )
3556 );
3557 }
3558
3559 return true;
3560 }
3561 }
3562
3563 return false;
3564 }
3565
3566
3567 /**
3568 * Get website update stats via SQL.
3569 *
3570 * @return object|null Database query result of null on failure.
3571 */
3572 public function get_websites_stats_update_sql() {
3573 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3574 return 'SELECT wp.*,wp_sync.sync_errors FROM ' . $this->table_name( 'wp' ) . ' wp JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid WHERE (wp.statsUpdate = 0 OR ' . time() . ' - wp.statsUpdate >= ' . ( 60 * 60 * 24 ) . ')' . $where . ' ORDER BY wp.statsUpdate ASC';
3575 }
3576
3577 /**
3578 * Update child site statistics.
3579 *
3580 * Update whether or not a child site has been updated.
3581 *
3582 * @param mixed $websiteid Child site ID.
3583 * @param mixed $statsUpdated Child site Update status.
3584 *
3585 * @return (int|boolean) Number of rows effected in update or false on failure.
3586 */
3587 public function update_website_stats( $websiteid, $statsUpdated ) {
3588 return $this->wpdb->update(
3589 $this->table_name( 'wp' ),
3590 array( 'statsUpdate' => $statsUpdated ),
3591 array( 'id' => $websiteid )
3592 );
3593 }
3594
3595 /**
3596 * Get child site by url.
3597 *
3598 * @param string $url Child site URL.
3599 *
3600 * @return object|null Database query result or null on failure.
3601 */
3602 public function get_websites_by_url( $url ) {
3603 if ( '/' !== substr( $url, - 1 ) ) {
3604 $url .= '/';
3605 }
3606 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3607 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3608 $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3609 if ( $results ) {
3610 return $results;
3611 }
3612
3613 if ( stristr( $url, '/www.' ) ) {
3614 // remove www if it's there!
3615 $url = str_replace( '/www.', '/', $url );
3616 } else {
3617 // add www if it's not there!
3618 $url = str_replace( 'https://', 'https://www.', $url );
3619 $url = str_replace( 'http://', 'http://www.', $url );
3620 }
3621
3622 $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3623 if ( $results ) {
3624 return $results;
3625 }
3626
3627 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
3628
3629 return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3630 }
3631
3632 /**
3633 * Get recently-synced child sites whose stored url differs from the
3634 * child-reported siteurl.
3635 *
3636 * The trailing slash must be normalized in SQL: stored urls always end
3637 * with a slash while child-reported siteurl values never do, so a plain
3638 * inequality would match nearly every site.
3639 *
3640 * @since 6.2
3641 *
3642 * @param int $days Freshness gate: only sites synced within this many days.
3643 *
3644 * @return array|object|null Rows with id, url, siteurl or null on failure.
3645 */
3646 public function get_websites_with_url_siteurl_mismatch( $days = 30 ) {
3647 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3648 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3649 $minimum_sync = time() - intval( $days ) * DAY_IN_SECONDS;
3650
3651 return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT wp.id, wp.url, wp.siteurl FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.siteurl <> '' AND TRIM(TRAILING '/' FROM wp.url) <> TRIM(TRAILING '/' FROM wp.siteurl) AND wp_sync.dtsSync >= %d", $minimum_sync ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3652 }
3653
3654 /**
3655 * Get IDs of child sites that have a non-empty value for a website option.
3656 *
3657 * @since 6.2
3658 *
3659 * @param string $option_name Website option name.
3660 * @param int $limit Maximum number of IDs to return.
3661 *
3662 * @return array Site IDs.
3663 */
3664 public function get_website_ids_with_nonempty_option( $option_name, $limit = 3 ) {
3665 $options_table = esc_sql( $this->table_name( 'wp_options' ) );
3666
3667 return $this->wpdb->get_col( $this->wpdb->prepare( "SELECT wpid FROM {$options_table} WHERE name = %s AND value <> '' ORDER BY wpid ASC LIMIT %d", $option_name, max( 1, intval( $limit ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3668 }
3669
3670 /**
3671 * Get one child site whose stored url exactly matches the given URL,
3672 * excluding a site ID. Used as the duplicate guard before a URL correction.
3673 *
3674 * @since 6.2
3675 *
3676 * @param string $url URL to match ( trailing slash enforced ).
3677 * @param int $exclude_id Site ID to exclude.
3678 *
3679 * @return object|null Matching row ( id, url ) or null.
3680 */
3681 public function get_website_by_exact_url_excluding_id( $url, $exclude_id ) {
3682 $url = rtrim( (string) $url, '/' );
3683 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3684
3685 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT id, url FROM {$wp_table} WHERE TRIM(TRAILING '/' FROM url) = %s AND id <> %d", $url, intval( $exclude_id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3686 }
3687
3688 /**
3689 * Method get_websites_to_notice_health_threshold()
3690 *
3691 * Get websites to notice site health.
3692 *
3693 * @param int $globalThreshold Global site health threshold.
3694 */
3695 public function get_websites_to_notice_health_threshold( $globalThreshold ) {
3696
3697 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3698 $extra_view = array( 'monitoring_notification_emails', 'settings_notification_emails' );
3699
3700 if ( 80 >= $globalThreshold ) { // actual is 80.
3701 // should-be-improved site health.
3702 $where_global_threshold = '( wp.health_threshold = 0 AND wp_sync.health_value < 80 )';
3703 } else {
3704 // good site health.
3705 $where_global_threshold = '( wp.health_threshold = 0 AND wp_sync.health_value >= 80 )';
3706 }
3707
3708 $where_site_threshold = ' ( wp.health_threshold = 80 AND wp_sync.health_value < 80 ) '; // should-be-improved site health.
3709 $where_site_threshold .= ' OR ( wp.health_threshold = 100 AND wp_sync.health_value >= 80 ) '; // good site health.
3710
3711 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3712 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3713
3714 $view_selects = '';
3715 $view_joins = '';
3716
3717 $opts_view = $this->get_wp_options_join( $extra_view );
3718
3719 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3720 $view_selects = ',' . $opts_view['selects'];
3721 $view_joins = $opts_view['joins'];
3722 }
3723 return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery
3724 "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid {$view_joins} WHERE wp.disable_health_check <> 1 AND wp.offline_check_result = 1 AND ( {$where_global_threshold} OR{$where_site_threshold} ) AND wp_sync.health_site_noticed = 0 " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd, the view fragments come from get_wp_options_join(), which allowlists option names, and both threshold fragments are literals picked above.
3725 $where . ' GROUP BY wp.id ',
3726 OBJECT
3727 );
3728 }
3729
3730 /**
3731 * Get websites offline status.
3732 *
3733 * @return array Sites with offline status.
3734 */
3735 public function get_websites_http_check_status() {
3736 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3737 $extra_view = array( 'settings_notification_emails' );
3738 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3739
3740 $view_selects = '';
3741 $view_joins = '';
3742
3743 $opts_view = $this->get_wp_options_join( $extra_view );
3744
3745 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3746 $view_selects = ',' . $opts_view['selects'];
3747 $view_joins = $opts_view['joins'];
3748 }
3749
3750 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery.
3751 return $this->wpdb->get_results(
3752 "SELECT wp.*{$view_selects} FROM {$wp_table} wp {$view_joins}" . ' WHERE wp.suspended = 0 AND wp.http_code_noticed = 0 AND wp.offline_check_result = -1 ' . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the view fragments come from get_wp_options_join(), which allowlists option names.
3753 $where . ' GROUP BY wp.id ',
3754 OBJECT
3755 );
3756 }
3757
3758 /**
3759 * Method set_website_noticed_http_check().
3760 *
3761 * @param array $site_id The site id .
3762 *
3763 * @return void
3764 */
3765 public function set_website_noticed_http_check( $site_id = array() ) {
3766 if ( empty( $site_id ) ) {
3767 return;
3768 }
3769 $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . esc_sql( $this->table_name( 'wp' ) ) . ' SET http_code_noticed = 1 WHERE id = %d', $site_id ) ); // phpcs:ignore PluginCheck.Security.DirectDB.UnpreparedSQL -- $site_id is an integer that is validated by ctype_digit() before being passed to this method.
3770 }
3771
3772 /**
3773 * Get DB Sites.
3774 *
3775 * @since 4.6
3776 *
3777 * @param mixed $params params.
3778 *
3779 * @return array $dbwebsites.
3780 */
3781 public function get_db_sites( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
3782
3783 $dbwebsites = array();
3784
3785 $data_fields = MainWP_System_Utility::get_default_map_site_fields();
3786 $data_fields[] = 'verify_certificate';
3787 $data_fields[] = 'client_id';
3788
3789 $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array();
3790 $sites = isset( $params['sites'] ) && is_array( $params['sites'] ) ? $params['sites'] : array();
3791 $groups = isset( $params['groups'] ) && is_array( $params['groups'] ) ? $params['groups'] : array();
3792 $clients = isset( $params['clients'] ) && is_array( $params['clients'] ) ? $params['clients'] : array();
3793 $schema_fields = isset( $params['schema_fields'] ) && is_array( $params['schema_fields'] ) ? $params['schema_fields'] : array(); // since 5.2.
3794 $selectgroups = isset( $params['selectgroups'] ) && ! empty( $params['selectgroups'] ) ? true : false; // since 5.2.
3795
3796 if ( ! empty( $schema_fields ) ) { // since 5.2.
3797 foreach ( $schema_fields as $field_name ) {
3798 if ( ! in_array( $field_name, $data_fields ) ) {
3799 $data_fields[] = $field_name;
3800 }
3801 }
3802 } elseif ( is_array( $fields ) ) {
3803 foreach ( $fields as $field_indx => $field_name ) {
3804
3805 $get_field = $field_name;
3806 if ( is_numeric( $get_field ) || is_bool( $get_field ) ) { // to compatible fix.
3807 $get_field = $field_indx;
3808 }
3809
3810 if ( in_array( $get_field, static::$possible_options ) && ! in_array( $get_field, $data_fields ) ) {
3811 $data_fields[] = $get_field;
3812 }
3813 }
3814 }
3815
3816 if ( ! empty( $sites ) ) {
3817 foreach ( $sites as $v ) {
3818 if ( MainWP_Utility::ctype_digit( $v ) ) {
3819 $website = static::instance()->get_website_by_id( $v, $selectgroups );
3820 if ( empty( $website ) ) {
3821 continue;
3822 }
3823 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3824 }
3825 }
3826 }
3827
3828 if ( ! empty( $groups ) ) {
3829 foreach ( $groups as $v ) {
3830 if ( MainWP_Utility::ctype_digit( $v ) ) {
3831 $websites = static::instance()->query( static::instance()->get_sql_websites_by_group_id( $v, $selectgroups ) );
3832 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
3833 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3834 }
3835 static::free_result( $websites );
3836 }
3837 }
3838 }
3839
3840 $params = array(
3841 'full_data' => true,
3842 'selectgroups' => $selectgroups,
3843 );
3844 $client_sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $clients, $params );
3845 if ( $client_sites ) {
3846 foreach ( $client_sites as $website ) {
3847 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3848 }
3849 }
3850 return $dbwebsites;
3851 }
3852
3853 /**
3854 * Get Sites.
3855 *
3856 * @param int $websiteid The id of the child site you wish to retrieve.
3857 * @param bool $for_manager Check Team Control.
3858 * @param array $others Array of others.
3859 *
3860 * @return array $output Array of content to output.
3861 *
3862 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
3863 * @uses \MainWP\Dashboard\MainWP_Utility::get_nice_url()
3864 */
3865 public function get_sites( $websiteid = null, $for_manager = false, $others = array() ) { // phpcs:ignore -- NOSONAR - not quite complex function.
3866
3867 if ( ! is_array( $others ) ) {
3868 $others = array();
3869 }
3870
3871 $search_site = null;
3872 $orderBy = 'wp.url';
3873 $offset = false;
3874 $rowcount = false;
3875 $extraWhere = null;
3876
3877 if ( isset( $websiteid ) && ( null !== $websiteid ) ) {
3878 $website = static::instance()->get_website_by_id( $websiteid );
3879
3880 if ( ! MainWP_System_Utility::can_edit_website( $website ) ) {
3881 return false;
3882 }
3883
3884 if ( ! \mainwp_current_user_can( 'site', $websiteid ) ) {
3885 return false;
3886 }
3887
3888 return array(
3889 array(
3890 'id' => $websiteid,
3891 'url' => MainWP_Utility::get_nice_url( $website->url, true ),
3892 'name' => $website->name,
3893 'totalsize' => $website->totalsize,
3894 'sync_errors' => $website->sync_errors,
3895 ),
3896 );
3897 } else {
3898 if ( isset( $others['orderby'] ) ) {
3899 if ( 'site' === $others['orderby'] ) {
3900 $orderBy = 'wp.name ' . ( 'asc' === $others['order'] ? 'asc' : 'desc' );
3901 } elseif ( 'url' === $others['orderby'] ) {
3902 $orderBy = 'wp.url ' . ( 'asc' === $others['order'] ? 'asc' : 'desc' );
3903 }
3904 }
3905 if ( isset( $others['search'] ) ) {
3906 $search_site = trim( $others['search'] );
3907 }
3908
3909 if ( is_array( $others ) && isset( $others['plugins_slug'] ) ) {
3910 $slugs = explode( ',', $others['plugins_slug'] );
3911 $extraWhere = '';
3912 foreach ( $slugs as $slug ) {
3913 $slug = wp_json_encode( $slug );
3914 $slug = trim( $slug, '"' );
3915 $slug = str_replace( '\\', '.', $slug );
3916 $extraWhere .= ' wp.plugins REGEXP "' . $slug . '" OR';
3917 }
3918 $extraWhere = trim( rtrim( $extraWhere, 'OR' ) );
3919
3920 if ( '' === $extraWhere ) {
3921 $extraWhere = null;
3922 } else {
3923 $extraWhere = '(' . $extraWhere . ')';
3924 }
3925 }
3926 }
3927
3928 $totalRecords = '';
3929
3930 if ( isset( $others['per_page'] ) && ! empty( $others['per_page'] ) ) {
3931 $sql = static::instance()->get_sql_websites_for_current_user( false, $search_site, $orderBy, false, false, $extraWhere, $for_manager );
3932 $websites_total = static::instance()->query( $sql );
3933 $totalRecords = ( $websites_total ? static::num_rows( $websites_total ) : 0 );
3934
3935 if ( $websites_total ) {
3936 static::free_result( $websites_total );
3937 }
3938
3939 $rowcount = absint( $others['per_page'] );
3940 $pagenum = isset( $others['paged'] ) ? absint( $others['paged'] ) : 0;
3941 if ( $pagenum > $totalRecords ) {
3942 $pagenum = $totalRecords;
3943 }
3944 $pagenum = max( 1, $pagenum );
3945 $offset = ( $pagenum - 1 ) * $rowcount;
3946
3947 }
3948
3949 $sql = static::instance()->get_sql_websites_for_current_user( false, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager );
3950 $websites = static::instance()->query( $sql );
3951
3952 $output = array();
3953 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
3954 $re = array(
3955 'id' => $website->id,
3956 'url' => MainWP_Utility::get_nice_url( $website->url, true ),
3957 'name' => $website->name,
3958 'totalsize' => $website->totalsize,
3959 'sync_errors' => $website->sync_errors,
3960 'client_id' => $website->client_id,
3961 );
3962
3963 if ( 0 < $totalRecords ) {
3964 $re['totalRecords'] = $totalRecords;
3965 $totalRecords = 0;
3966 }
3967
3968 $output[] = $re;
3969 }
3970 static::free_result( $websites );
3971
3972 return $output;
3973 }
3974
3975 /**
3976 * Method get_lookup_items().
3977 *
3978 * Get bulk lookup items to reduce number of db queries.
3979 *
3980 * @param string $item_name lookup item name.
3981 * @param int $item_id lookup item id.
3982 * @param string $obj_name loockup object name.
3983 *
3984 * @return mixed Result
3985 */
3986 public function get_lookup_items( $item_name, $item_id, $obj_name ) {
3987 return $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'lookup_item_objects' ) . ' WHERE item_name=%s AND item_id = %d AND object_name = %s', $item_name, $item_id, $obj_name ) ); //phpcs:ignore -- ok.
3988 }
3989
3990 /**
3991 * Method insert_lookup_item().
3992 *
3993 * Insert lookup item, need checks existed before to prevent double values.
3994 *
3995 * @param string $item_name item name.
3996 * @param int $item_id item id.
3997 * @param string $obj_name object name.
3998 * @param int $obj_id object id.
3999 *
4000 * @return mixed Result
4001 */
4002 public function insert_lookup_item( $item_name, $item_id, $obj_name, $obj_id ) {
4003 // get_lookup_items() and delete_lookup_items() match item_name as given, so a
4004 // name that sanitizing would alter is refused instead of being stored under a
4005 // value the caller can never read back or delete.
4006 if ( ! is_string( $item_name ) || sanitize_text_field( $item_name ) !== $item_name ) {
4007 return false;
4008 }
4009 if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) {
4010 return false;
4011 }
4012 $data = array(
4013 'item_name' => $item_name,
4014 'item_id' => $item_id,
4015 'object_name' => $obj_name,
4016 'object_id' => $obj_id,
4017 );
4018 // wpdb refuses a value past the column length before any SQL runs, and
4019 // insert_id would still hold the previous insert's id.
4020 if ( false === $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data ) ) {
4021 return false;
4022 }
4023 return $this->wpdb->insert_id; // must return lookup id.
4024 }
4025
4026 /**
4027 * Method delete_lookup_items().
4028 *
4029 * Delete bulk lookup items by lookup ids or object names with item id and item name, to reduce number of db queries.
4030 *
4031 * @param string $by Delete by.
4032 * @param array $params params.
4033 *
4034 * @return mixed Result
4035 */
4036 public function delete_lookup_items( $by = 'lookup_id', $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
4037 if ( ! is_array( $params ) ) {
4038 return false;
4039 }
4040
4041 $lookup_ids = isset( $params['lookup_ids'] ) ? $params['lookup_ids'] : null;
4042 $item_id = isset( $params['item_id'] ) ? $params['item_id'] : null;
4043 $object_id = isset( $params['object_id'] ) ? $params['object_id'] : null;
4044 $item_name = isset( $params['item_name'] ) ? $params['item_name'] : null;
4045 $obj_names = isset( $params['object_names'] ) ? $params['object_names'] : null;
4046
4047 if ( 'object_name' === $by ) {
4048 if ( empty( $item_id ) || empty( $item_name ) ) {
4049 return false;
4050 }
4051
4052 $obj_names = $this->escape_array( $obj_names );
4053 if ( ! empty( $obj_names ) ) {
4054 $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'lookup_item_objects' ) . ' WHERE item_name = %s AND item_id = %d AND object_name IN ("' . implode( '","', $obj_names ) . '") ', $item_name, $item_id ) ); //phpcs:ignore -- ok.
4055 return true;
4056 }
4057 } elseif ( 'object_id' === $by ) {
4058 if ( empty( $object_id ) || empty( $item_name ) || empty( $obj_names ) ) {
4059 return false;
4060 }
4061
4062 $obj_names = $this->escape_array( $obj_names );
4063 if ( ! empty( $obj_names ) ) {
4064 $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'lookup_item_objects' ) . ' WHERE item_name = %s AND object_id = %d AND object_name IN ("' . implode( '","', $obj_names ) . '") ', $item_name, $object_id ) ); //phpcs:ignore -- ok.
4065 return true;
4066 }
4067 } elseif ( 'lookup_id' === $by ) {
4068 if ( empty( $lookup_ids ) ) {
4069 return false;
4070 }
4071 if ( is_numeric( $lookup_ids ) ) {
4072 $lookup_ids = array( $lookup_ids );
4073 } elseif ( is_array( $lookup_ids ) ) {
4074 $lookup_ids = MainWP_Utility::array_numeric_filter( $lookup_ids );
4075 } else {
4076 return false;
4077 }
4078 $this->wpdb->query( 'DELETE FROM ' . $this->table_name( 'lookup_item_objects' ) . ' WHERE lookup_id IN (' . implode( ',', $lookup_ids ) . ') ' ); //phpcs:ignore -- ok.
4079 return true;
4080 }
4081 return false;
4082 }
4083
4084
4085 /**
4086 * Insert a new REST API key row and return the credential payload.
4087 *
4088 * Returns an array with key_id, user_id, plaintext consumer_key,
4089 * plaintext consumer_secret, and key_permissions on success. Returns
4090 * false when the wpdb->insert() call fails (no current user, missing
4091 * table, schema mismatch, etc.). Callers must check the return type
4092 * before treating it as an array.
4093 *
4094 * @param string $consumer_key Consumer key.
4095 * @param string $consumer_secret Secret key.
4096 * @param string $scope scope.
4097 * @param string $description description.
4098 * @param int $enabled 1 or 0.
4099 * @param array $others others.
4100 *
4101 * @return array|false Credential payload on success, false on failure.
4102 */
4103 public function insert_rest_api_key( $consumer_key, $consumer_secret, $scope, $description, $enabled, $others = array() ) {
4104 global $current_user;
4105
4106 if ( $current_user ) {
4107 $user_id = $current_user->ID;
4108 }
4109
4110 if ( empty( $user_id ) ) {
4111 return false;
4112 }
4113
4114 unset( $others ); // Parameter retained for signature compatibility; key_pass/key_type fields are vestigial after MWP-1544 cleanup.
4115
4116 // Hash the consumer_secret with WordPress's password hasher so the
4117 // value at rest is no longer reversible by a DB-read primitive.
4118 // The plaintext is returned to the caller below so it can be shown
4119 // to the admin once at creation time. See MWP-1540.
4120 $hashed_secret = wp_hash_password( $consumer_secret );
4121
4122 // Created API keys.
4123 $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4124 $inserted = $this->wpdb->insert(
4125 $this->table_name( 'api_keys' ),
4126 array(
4127 'user_id' => $user_id,
4128 'description' => $description,
4129 'permissions' => $permissions,
4130 'consumer_key' => mainwp_api_hash( $consumer_key ),
4131 'consumer_secret' => $hashed_secret,
4132 'truncated_key' => substr( $consumer_key, -7 ),
4133 'enabled' => $enabled,
4134 ),
4135 array(
4136 '%d',
4137 '%s',
4138 '%s',
4139 '%s',
4140 '%s',
4141 '%s',
4142 '%d',
4143 ),
4144 );
4145
4146 // wpdb->insert() returns false on failure. Without this guard the
4147 // function would still hand back the plaintext consumer_secret plus
4148 // $wpdb->insert_id, but that insert_id is the LAST successful insert
4149 // on the connection (typically from earlier in the same request),
4150 // not this row. The caller's empty-key_id check would pass and the
4151 // operator would receive a credential that was never persisted.
4152 // See MWP-1540 PR review feedback.
4153 if ( false === $inserted ) {
4154 return false;
4155 }
4156
4157 return array(
4158 'key_id' => $this->wpdb->insert_id,
4159 'user_id' => $user_id,
4160 'consumer_key' => $consumer_key,
4161 'consumer_secret' => $consumer_secret,
4162 'key_permissions' => $permissions,
4163 );
4164 }
4165
4166 /**
4167 * Update rest api key.
4168 *
4169 * @param int $key_id Consumer key.
4170 * @param string $scope scope.
4171 * @param string $description description.
4172 * @param int $enabled Enabled.
4173 *
4174 * @return array
4175 */
4176 public function update_rest_api_key( $key_id, $scope, $description, $enabled = 1 ) {
4177 $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4178 return $this->wpdb->update(
4179 $this->table_name( 'api_keys' ),
4180 array(
4181 'description' => $description,
4182 'permissions' => $permissions,
4183 'enabled' => $enabled ? 1 : 0,
4184 ),
4185 array(
4186 'key_id' => $key_id,
4187 )
4188 );
4189 }
4190
4191
4192 /**
4193 * Method is_existed_enabled_rest_key().
4194 *
4195 * @return bool result.
4196 */
4197 public function is_existed_enabled_rest_key() {
4198 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4199 $enabled = $this->wpdb->get_row( "SELECT * FROM {$table_name} WHERE enabled = 1 LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4200 return $enabled ? true : false;
4201 }
4202
4203 /**
4204 * Method get_rest_api_key_by().
4205 *
4206 * @param int $id To get key.
4207 *
4208 * @return array
4209 */
4210 public function get_rest_api_key_by( $id ) {
4211 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4212 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table_name} WHERE key_id = %d", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4213 }
4214
4215 /**
4216 * Method remove_rest_api_key().
4217 *
4218 * @param string $id to delete.
4219 *
4220 * @return array
4221 */
4222 public function remove_rest_api_key( $id ) {
4223 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4224 return $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE key_id = %s", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4225 }
4226
4227 /**
4228 * Method get_rest_api_keys().
4229 *
4230 * @return array
4231 */
4232 public function get_rest_api_keys() {
4233 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4234 return $this->wpdb->get_results( "SELECT * FROM {$table_name} ORDER BY key_id DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4235 }
4236
4237
4238 /**
4239 * Update regular process.
4240 *
4241 * @param array $data process data.
4242 * @return mixed
4243 */
4244 public function update_regular_process( $data ) {
4245 if ( isset( $data['process_id'] ) ) {
4246 $process_id = $data['process_id'];
4247 unset( $data['process_id'] );
4248 return $this->wpdb->update( $this->table_name( 'schedule_processes' ), $data, array( 'process_id' => $process_id ) );
4249 } elseif ( is_array( $data ) && isset( $data['type'] ) && isset( $data['process_slug'] ) ) {
4250 return $this->wpdb->insert( $this->table_name( 'schedule_processes' ), $data );
4251 }
4252 return false;
4253 }
4254
4255 /**
4256 * Delete regular process.
4257 *
4258 * @param int $process_id Process id.
4259 * @param int $item_id Item id.
4260 * @param string $pro_type Process type.
4261 * @param string $pro_slug Process slug.
4262 *
4263 * @return mixed
4264 */
4265 public function delete_regular_process( $process_id = false, $item_id = false, $pro_type = false, $pro_slug = false ) {
4266
4267 if ( is_numeric( $process_id ) && ! empty( $process_id ) ) {
4268 return $this->wpdb->delete(
4269 $this->table_name( 'schedule_processes' ),
4270 array(
4271 'process_id' => $process_id,
4272 )
4273 );
4274 } elseif ( ! empty( $pro_type ) || ! empty( $pro_slug ) ) {
4275
4276 $data = array();
4277
4278 if ( ! empty( $pro_type ) ) {
4279 $data['type'] = $pro_type;
4280 }
4281
4282 if ( ! empty( $pro_slug ) ) {
4283 $data['process_slug'] = $pro_slug;
4284 }
4285
4286 if ( ! empty( $item_id ) ) {
4287 $data['item_id'] = $item_id;
4288 }
4289 // Bulk delete.
4290 return $this->wpdb->delete( $this->table_name( 'schedule_processes' ), $data );
4291 }
4292 return false;
4293 }
4294
4295 /**
4296 * Method get_regular_process_by_item_id_type_slug
4297 *
4298 * @param integer $item_id item id.
4299 * @param string $type type.
4300 * @param string $process_slug process slug.
4301 *
4302 * @return mixed result
4303 */
4304 public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) {
4305 $table_name = esc_sql( $this->table_name( 'schedule_processes' ) );
4306 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT pr.* FROM {$table_name} pr WHERE pr.item_id = %d AND pr.type = %s AND pr.process_slug = %s", $item_id, $type, $process_slug ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4307 }
4308
4309 /**
4310 * Log SQL queries for debugging via hook.
4311 *
4312 * This method provides a structured way to log SQL queries for development/debugging.
4313 * It does NOT use error_log() directly - instead, it fires the `mainwp_log_system_query`
4314 * action hook, allowing external listeners (logging plugins, debug tools) to handle
4315 * the log output appropriately.
4316 *
4317 * To enable query logging:
4318 * 1. Set `$params['dev_log_query'] = 1` when calling database methods
4319 * 2. Add a listener to the `mainwp_log_system_query` action hook
4320 *
4321 * Example listener:
4322 * ```php
4323 * add_action( 'mainwp_log_system_query', function( $params, $sql, $caller ) {
4324 * error_log( 'MainWP Query: ' . $sql );
4325 * }, 10, 3 );
4326 * ```
4327 *
4328 * @param array $params Query parameters. Set 'dev_log_query' to enable logging.
4329 * @param string $sql The SQL query string.
4330 * @param mixed $caller Instance of caller class (optional).
4331 * @return void
4332 */
4333 public function log_system_query( $params, $sql, $caller = false ) {
4334 $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller );
4335 if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) {
4336 do_action( 'mainwp_log_system_query', $params, $sql, $caller );
4337 }
4338 }
4339
4340 /**
4341 * Method get_core_tables().
4342 *
4343 * Returns the list of core tables used by MainWP for validation data integrity and other purposes.
4344 *
4345 * @return array Core tables info
4346 */
4347 public function get_core_tables() {
4348
4349 $core_tables = array(
4350 $this->table_name( 'wp_clients' ),
4351 $this->table_name( 'wp_clients_fields' ),
4352 $this->table_name( 'wp_clients_field_values' ),
4353 $this->table_name( 'wp_clients_contacts' ),
4354 $this->table_name( 'monitors' ),
4355 $this->table_name( 'monitor_heartbeat' ),
4356 $this->table_name( 'monitor_stat_hourly' ),
4357 $this->table_name( 'wp' ),
4358 $this->table_name( 'wp_sync' ),
4359 $this->table_name( 'wp_options' ),
4360 $this->table_name( 'wp_settings_backup' ),
4361 $this->table_name( 'users' ),
4362 $this->table_name( 'wp_status' ),
4363 $this->table_name( 'group' ),
4364 $this->table_name( 'wp_group' ),
4365 $this->table_name( 'lookup_item_objects' ),
4366 $this->table_name( 'wp_backup_progress' ),
4367 $this->table_name( 'wp_backup' ),
4368 $this->table_name( 'api_keys' ),
4369 $this->table_name( 'action_log' ),
4370 $this->table_name( 'request_log' ),
4371 $this->table_name( 'schedule_processes' ),
4372 $this->table_name( 'cost_tracker' ),
4373 );
4374
4375 if ( ! defined( 'MAINWP_MODULE_LOG_ENABLED' ) || MAINWP_MODULE_LOG_ENABLED ) {
4376 $core_tables[] = $this->table_name( 'wp_logs' );
4377 $core_tables[] = $this->table_name( 'wp_logs_meta' );
4378 $core_tables[] = $this->table_name( 'wp_logs_meta_archive' );
4379 }
4380
4381 return apply_filters( 'mainwp_database_core_tables', $core_tables );
4382 }
4383 }
4384