PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / class / class-mainwp-db.php

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

4,402 lines 179.3 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
3321 // Add primary monitor, use global settings.
3322 $success = MainWP_DB_Uptime_Monitoring::instance()->update_wp_monitor(
3323 array(
3324 'wpid' => $websiteid,
3325 'suburl' => '',
3326 )
3327 );
3328
3329 $opt_name = 'mainwp_primary_monitor_repair_attempts_for_wp_' . $websiteid;
3330
3331 if ( ! $success ) {
3332 update_option( $opt_name, 1 );
3333 MainWP_Utility::unhide_mainwp_message( 'failed-attempt-create-primary-monitor-' . $websiteid );
3334 } elseif ( get_option( $opt_name ) ) {
3335 delete_option( $opt_name );
3336 }
3337
3338 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3339 return $websiteid;
3340 }
3341 }
3342
3343 return false;
3344 }
3345
3346 /**
3347 * Remove child site from the MainWP Dashboard.
3348 *
3349 * @param int $websiteid Child site ID.
3350 *
3351 * @return int|boolean Return child site ID that was removed or false on failure.
3352 *
3353 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3354 */
3355 public function remove_website( $websiteid ) {
3356 if ( MainWP_Utility::ctype_digit( $websiteid ) ) {
3357 $nr = $this->wpdb->delete( $this->table_name( 'wp' ), array( 'id' => $websiteid ) );
3358 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3359 $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) );
3360 $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) );
3361 MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid );
3362 MainWP_DB_Uptime_Monitoring::instance()->delete_site_monitors( $websiteid );
3363 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3364 return $nr;
3365 }
3366
3367 return false;
3368 }
3369
3370 /**
3371 * Update child site db values.
3372 *
3373 * @param int $websiteid Child site ID.
3374 * @param array $fields Database fields to update.
3375 *
3376 * @return int|boolean The number of rows updated, or false on error.
3377 */
3378 public function update_website_values( $websiteid, $fields ) {
3379 if ( ! empty( $fields ) ) {
3380 // MWP-1548: encrypt http_user / http_pass at rest if either
3381 // is present in $fields. Generic-fields updaters (clone
3382 // flow in extensions-handler, callers that pass arbitrary
3383 // column subsets) must go through the same fail-closed
3384 // contract as add_website / update_website.
3385 if ( array_key_exists( 'http_user', $fields ) ) {
3386 $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_user'], 'http_user' );
3387 if ( false === $encrypted ) {
3388 return false;
3389 }
3390 $fields['http_user'] = $encrypted;
3391 }
3392 if ( array_key_exists( 'http_pass', $fields ) ) {
3393 $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_pass'], 'http_pass' );
3394 if ( false === $encrypted ) {
3395 return false;
3396 }
3397 $fields['http_pass'] = $encrypted;
3398 }
3399 // Lock the data stream to prevent other processes from updating at the same time.
3400 $table_name = esc_sql( $this->table_name( 'wp' ) );
3401 $sql = $this->wpdb->prepare(
3402 "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3403 $websiteid
3404 );
3405 $this->wpdb->get_row( $sql );
3406
3407 return $this->wpdb->update( $this->table_name( 'wp' ), $fields, array( 'id' => $websiteid ) );
3408 }
3409
3410 return false;
3411 }
3412
3413 /**
3414 * Update child site sync values.
3415 *
3416 * @param int $websiteid Child site ID.
3417 * @param array $fields Database fields to update.
3418 *
3419 * @return int|boolean The number of rows updated, or false on error.
3420 */
3421 public function update_website_sync_values( $websiteid, $fields ) {
3422 if ( ! empty( $fields ) ) {
3423 return $this->wpdb->update( $this->table_name( 'wp_sync' ), $fields, array( 'wpid' => $websiteid ) );
3424 }
3425
3426 return false;
3427 }
3428
3429 /**
3430 * Update child site.
3431 *
3432 * @param int $websiteid Website ID.
3433 * @param string $url Child site URL.
3434 * @param int $userid Current user ID.
3435 * @param string $name Child site name.
3436 * @param string $siteadmin Child site administrator username.
3437 * @param array $groupids Group IDs.
3438 * @param array $groupnames Group Names.
3439 * @param string $pluginDir Plugin directory.
3440 * @param mixed $maximumFileDescriptorsOverride Overwrite the Maximum File Descriptors option.
3441 * @param mixed $maximumFileDescriptorsAuto Auto set the Maximum File Descriptors option.
3442 * @param mixed $maximumFileDescriptors Set the Maximum File Descriptors option.
3443 * @param int $verifyCertificate Whether or not to verify SSL Certificate.
3444 * @param mixed $archiveFormat Backup archive formate.
3445 * @param string $uniqueId Unique security ID.
3446 * @param string $http_user HTTP Basic Authentication username.
3447 * @param string $http_pass HTTP Basic Authentication password.
3448 * @param int $sslVersion SSL Version.
3449 * @param bool $disableHealthChecking Disable Site health threshold.
3450 * @param int $healthThreshold Site health threshold.
3451 * @param string $backup_method Primary backup method.
3452 *
3453 * @return boolean ture on success or false on failure.
3454 *
3455 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
3456 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3457 */
3458 public function update_website( // phpcs:ignore -- NOSONAR - complex.
3459 $websiteid,
3460 $url,
3461 $userid,
3462 $name,
3463 $siteadmin,
3464 $groupids,
3465 $groupnames,
3466 $pluginDir,
3467 $maximumFileDescriptorsOverride,
3468 $maximumFileDescriptorsAuto,
3469 $maximumFileDescriptors,
3470 $verifyCertificate = 1,
3471 $archiveFormat = 'global',
3472 $uniqueId = '',
3473 $http_user = null,
3474 $http_pass = null,
3475 $sslVersion = 0,
3476 $disableHealthChecking = 1,
3477 $healthThreshold = 0,
3478 $backup_method = 'global'
3479 ) {
3480
3481 $wpe = 0; // going to update when sync.
3482
3483 if ( MainWP_Utility::ctype_digit( $websiteid ) && MainWP_Utility::ctype_digit( $userid ) ) {
3484 $website = $this->get_website_by_id( $websiteid );
3485 if ( MainWP_System_Utility::can_edit_website( $website ) ) {
3486 // MWP-1548: encrypt http_user / http_pass at rest. Same
3487 // fail-closed contract as add_website -- a non-empty
3488 // value that fails to encrypt aborts the update so we
3489 // never overwrite an existing encrypted row with
3490 // plaintext when the encryption layer is unhealthy.
3491 $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' );
3492 $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' );
3493 if ( false === $encrypted_http_user || false === $encrypted_http_pass ) {
3494 return false;
3495 }
3496 // update admin.
3497 $this->wpdb->update(
3498 $this->table_name( 'wp' ),
3499 array(
3500 'url' => $url,
3501 'name' => wp_strip_all_tags( $name ),
3502 'adminname' => $siteadmin,
3503 'pluginDir' => $pluginDir,
3504 'verify_certificate' => intval( $verifyCertificate ),
3505 'ssl_version' => intval( $sslVersion ),
3506 'wpe' => intval( $wpe ),
3507 'uniqueId' => $uniqueId,
3508 'http_user' => $encrypted_http_user,
3509 'http_pass' => $encrypted_http_pass,
3510 'disable_health_check' => $disableHealthChecking,
3511 'health_threshold' => $healthThreshold,
3512 'primary_backup_method' => $backup_method,
3513 ),
3514 array( 'id' => $websiteid )
3515 );
3516 $this->wpdb->update(
3517 $this->table_name( 'wp_settings_backup' ),
3518 array( 'archiveFormat' => $archiveFormat ),
3519 array( 'wpid' => $websiteid )
3520 );
3521
3522 if ( get_option( 'mainwp_enableLegacyBackupFeature' ) ) {
3523 $this->wpdb->update(
3524 $this->table_name( 'wp' ),
3525 array(
3526 'maximumFileDescriptorsOverride' => (int) $maximumFileDescriptorsOverride,
3527 'maximumFileDescriptorsAuto' => (int) $maximumFileDescriptorsAuto,
3528 'maximumFileDescriptors' => (int) $maximumFileDescriptors,
3529 ),
3530 array( 'id' => $websiteid )
3531 );
3532 }
3533
3534 // remove groups.
3535 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3536 // Remove GA stats.
3537 $showErrors = $this->wpdb->hide_errors();
3538
3539 /**
3540 * Action: mainwp_ga_delete_site
3541 *
3542 * Fires upon site removal process in order to delete Google Analytics data.
3543 *
3544 * @param int $websiteid Child site ID.
3545 *
3546 * @since Unknown
3547 */
3548 do_action( 'mainwp_ga_delete_site', $websiteid );
3549
3550 if ( $showErrors ) {
3551 $this->wpdb->show_errors();
3552 }
3553 // add groups with groupnames.
3554 foreach ( $groupnames as $groupname ) {
3555 if ( $this->wpdb->insert(
3556 $this->table_name( 'group' ),
3557 array(
3558 'userid' => $userid,
3559 'name' => $this->escape( $groupname ),
3560 )
3561 )
3562 ) {
3563 $groupids[] = $this->wpdb->insert_id;
3564 }
3565 }
3566 // add groupids.
3567 foreach ( $groupids as $groupid ) {
3568 $this->wpdb->insert(
3569 $this->table_name( 'wp_group' ),
3570 array(
3571 'wpid' => $websiteid,
3572 'groupid' => $groupid,
3573 )
3574 );
3575 }
3576
3577 return true;
3578 }
3579 }
3580
3581 return false;
3582 }
3583
3584
3585 /**
3586 * Get website update stats via SQL.
3587 *
3588 * @return object|null Database query result of null on failure.
3589 */
3590 public function get_websites_stats_update_sql() {
3591 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3592 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';
3593 }
3594
3595 /**
3596 * Update child site statistics.
3597 *
3598 * Update whether or not a child site has been updated.
3599 *
3600 * @param mixed $websiteid Child site ID.
3601 * @param mixed $statsUpdated Child site Update status.
3602 *
3603 * @return (int|boolean) Number of rows effected in update or false on failure.
3604 */
3605 public function update_website_stats( $websiteid, $statsUpdated ) {
3606 return $this->wpdb->update(
3607 $this->table_name( 'wp' ),
3608 array( 'statsUpdate' => $statsUpdated ),
3609 array( 'id' => $websiteid )
3610 );
3611 }
3612
3613 /**
3614 * Get child site by url.
3615 *
3616 * @param string $url Child site URL.
3617 *
3618 * @return object|null Database query result or null on failure.
3619 */
3620 public function get_websites_by_url( $url ) {
3621 if ( '/' !== substr( $url, - 1 ) ) {
3622 $url .= '/';
3623 }
3624 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3625 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3626 $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3627 if ( $results ) {
3628 return $results;
3629 }
3630
3631 if ( stristr( $url, '/www.' ) ) {
3632 // remove www if it's there!
3633 $url = str_replace( '/www.', '/', $url );
3634 } else {
3635 // add www if it's not there!
3636 $url = str_replace( 'https://', 'https://www.', $url );
3637 $url = str_replace( 'http://', 'http://www.', $url );
3638 }
3639
3640 $results = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.url = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3641 if ( $results ) {
3642 return $results;
3643 }
3644
3645 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
3646
3647 return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') = %s", $url ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3648 }
3649
3650 /**
3651 * Get recently-synced child sites whose stored url differs from the
3652 * child-reported siteurl.
3653 *
3654 * The trailing slash must be normalized in SQL: stored urls always end
3655 * with a slash while child-reported siteurl values never do, so a plain
3656 * inequality would match nearly every site.
3657 *
3658 * @since 6.2
3659 *
3660 * @param int $days Freshness gate: only sites synced within this many days.
3661 *
3662 * @return array|object|null Rows with id, url, siteurl or null on failure.
3663 */
3664 public function get_websites_with_url_siteurl_mismatch( $days = 30 ) {
3665 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3666 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3667 $minimum_sync = time() - intval( $days ) * DAY_IN_SECONDS;
3668
3669 return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT wp.id, wp.url, wp.siteurl FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid WHERE wp.siteurl <> '' AND TRIM(TRAILING '/' FROM wp.url) <> TRIM(TRAILING '/' FROM wp.siteurl) AND wp_sync.dtsSync >= %d", $minimum_sync ), OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd.
3670 }
3671
3672 /**
3673 * Get IDs of child sites that have a non-empty value for a website option.
3674 *
3675 * @since 6.2
3676 *
3677 * @param string $option_name Website option name.
3678 * @param int $limit Maximum number of IDs to return.
3679 *
3680 * @return array Site IDs.
3681 */
3682 public function get_website_ids_with_nonempty_option( $option_name, $limit = 3 ) {
3683 $options_table = esc_sql( $this->table_name( 'wp_options' ) );
3684
3685 return $this->wpdb->get_col( $this->wpdb->prepare( "SELECT wpid FROM {$options_table} WHERE name = %s AND value <> '' ORDER BY wpid ASC LIMIT %d", $option_name, max( 1, intval( $limit ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3686 }
3687
3688 /**
3689 * Get one child site whose stored url exactly matches the given URL,
3690 * excluding a site ID. Used as the duplicate guard before a URL correction.
3691 *
3692 * @since 6.2
3693 *
3694 * @param string $url URL to match ( trailing slash enforced ).
3695 * @param int $exclude_id Site ID to exclude.
3696 *
3697 * @return object|null Matching row ( id, url ) or null.
3698 */
3699 public function get_website_by_exact_url_excluding_id( $url, $exclude_id ) {
3700 $url = rtrim( (string) $url, '/' );
3701 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3702
3703 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT id, url FROM {$wp_table} WHERE TRIM(TRAILING '/' FROM url) = %s AND id <> %d", $url, intval( $exclude_id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
3704 }
3705
3706 /**
3707 * Method get_websites_to_notice_health_threshold()
3708 *
3709 * Get websites to notice site health.
3710 *
3711 * @param int $globalThreshold Global site health threshold.
3712 */
3713 public function get_websites_to_notice_health_threshold( $globalThreshold ) {
3714
3715 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3716 $extra_view = array( 'monitoring_notification_emails', 'settings_notification_emails' );
3717
3718 if ( 80 >= $globalThreshold ) { // actual is 80.
3719 // should-be-improved site health.
3720 $where_global_threshold = '( wp.health_threshold = 0 AND wp_sync.health_value < 80 )';
3721 } else {
3722 // good site health.
3723 $where_global_threshold = '( wp.health_threshold = 0 AND wp_sync.health_value >= 80 )';
3724 }
3725
3726 $where_site_threshold = ' ( wp.health_threshold = 80 AND wp_sync.health_value < 80 ) '; // should-be-improved site health.
3727 $where_site_threshold .= ' OR ( wp.health_threshold = 100 AND wp_sync.health_value >= 80 ) '; // good site health.
3728
3729 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3730 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3731
3732 $view_selects = '';
3733 $view_joins = '';
3734
3735 $opts_view = $this->get_wp_options_join( $extra_view );
3736
3737 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3738 $view_selects = ',' . $opts_view['selects'];
3739 $view_joins = $opts_view['joins'];
3740 }
3741 return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery
3742 "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid {$view_joins} WHERE wp.disable_health_check <> 1 AND wp.offline_check_result = 1 AND ( {$where_global_threshold} OR{$where_site_threshold} ) AND wp_sync.health_site_noticed = 0 " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names are esc_sql'd, the view fragments come from get_wp_options_join(), which allowlists option names, and both threshold fragments are literals picked above.
3743 $where . ' GROUP BY wp.id ',
3744 OBJECT
3745 );
3746 }
3747
3748 /**
3749 * Get websites offline status.
3750 *
3751 * @return array Sites with offline status.
3752 */
3753 public function get_websites_http_check_status() {
3754 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3755 $extra_view = array( 'settings_notification_emails' );
3756 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3757
3758 $view_selects = '';
3759 $view_joins = '';
3760
3761 $opts_view = $this->get_wp_options_join( $extra_view );
3762
3763 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3764 $view_selects = ',' . $opts_view['selects'];
3765 $view_joins = $opts_view['joins'];
3766 }
3767
3768 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery.
3769 return $this->wpdb->get_results(
3770 "SELECT wp.*{$view_selects} FROM {$wp_table} wp {$view_joins}" . ' WHERE wp.suspended = 0 AND wp.http_code_noticed = 0 AND wp.offline_check_result = -1 ' . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd; the view fragments come from get_wp_options_join(), which allowlists option names.
3771 $where . ' GROUP BY wp.id ',
3772 OBJECT
3773 );
3774 }
3775
3776 /**
3777 * Method set_website_noticed_http_check().
3778 *
3779 * @param array $site_id The site id .
3780 *
3781 * @return void
3782 */
3783 public function set_website_noticed_http_check( $site_id = array() ) {
3784 if ( empty( $site_id ) ) {
3785 return;
3786 }
3787 $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . esc_sql( $this->table_name( 'wp' ) ) . ' SET http_code_noticed = 1 WHERE id = %d', $site_id ) ); // phpcs:ignore PluginCheck.Security.DirectDB.UnpreparedSQL -- $site_id is an integer that is validated by ctype_digit() before being passed to this method.
3788 }
3789
3790 /**
3791 * Get DB Sites.
3792 *
3793 * @since 4.6
3794 *
3795 * @param mixed $params params.
3796 *
3797 * @return array $dbwebsites.
3798 */
3799 public function get_db_sites( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
3800
3801 $dbwebsites = array();
3802
3803 $data_fields = MainWP_System_Utility::get_default_map_site_fields();
3804 $data_fields[] = 'verify_certificate';
3805 $data_fields[] = 'client_id';
3806
3807 $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array();
3808 $sites = isset( $params['sites'] ) && is_array( $params['sites'] ) ? $params['sites'] : array();
3809 $groups = isset( $params['groups'] ) && is_array( $params['groups'] ) ? $params['groups'] : array();
3810 $clients = isset( $params['clients'] ) && is_array( $params['clients'] ) ? $params['clients'] : array();
3811 $schema_fields = isset( $params['schema_fields'] ) && is_array( $params['schema_fields'] ) ? $params['schema_fields'] : array(); // since 5.2.
3812 $selectgroups = isset( $params['selectgroups'] ) && ! empty( $params['selectgroups'] ) ? true : false; // since 5.2.
3813
3814 if ( ! empty( $schema_fields ) ) { // since 5.2.
3815 foreach ( $schema_fields as $field_name ) {
3816 if ( ! in_array( $field_name, $data_fields ) ) {
3817 $data_fields[] = $field_name;
3818 }
3819 }
3820 } elseif ( is_array( $fields ) ) {
3821 foreach ( $fields as $field_indx => $field_name ) {
3822
3823 $get_field = $field_name;
3824 if ( is_numeric( $get_field ) || is_bool( $get_field ) ) { // to compatible fix.
3825 $get_field = $field_indx;
3826 }
3827
3828 if ( in_array( $get_field, static::$possible_options ) && ! in_array( $get_field, $data_fields ) ) {
3829 $data_fields[] = $get_field;
3830 }
3831 }
3832 }
3833
3834 if ( ! empty( $sites ) ) {
3835 foreach ( $sites as $v ) {
3836 if ( MainWP_Utility::ctype_digit( $v ) ) {
3837 $website = static::instance()->get_website_by_id( $v, $selectgroups );
3838 if ( empty( $website ) ) {
3839 continue;
3840 }
3841 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3842 }
3843 }
3844 }
3845
3846 if ( ! empty( $groups ) ) {
3847 foreach ( $groups as $v ) {
3848 if ( MainWP_Utility::ctype_digit( $v ) ) {
3849 $websites = static::instance()->query( static::instance()->get_sql_websites_by_group_id( $v, $selectgroups ) );
3850 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
3851 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3852 }
3853 static::free_result( $websites );
3854 }
3855 }
3856 }
3857
3858 $params = array(
3859 'full_data' => true,
3860 'selectgroups' => $selectgroups,
3861 );
3862 $client_sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $clients, $params );
3863 if ( $client_sites ) {
3864 foreach ( $client_sites as $website ) {
3865 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3866 }
3867 }
3868 return $dbwebsites;
3869 }
3870
3871 /**
3872 * Get Sites.
3873 *
3874 * @param int $websiteid The id of the child site you wish to retrieve.
3875 * @param bool $for_manager Check Team Control.
3876 * @param array $others Array of others.
3877 *
3878 * @return array $output Array of content to output.
3879 *
3880 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
3881 * @uses \MainWP\Dashboard\MainWP_Utility::get_nice_url()
3882 */
3883 public function get_sites( $websiteid = null, $for_manager = false, $others = array() ) { // phpcs:ignore -- NOSONAR - not quite complex function.
3884
3885 if ( ! is_array( $others ) ) {
3886 $others = array();
3887 }
3888
3889 $search_site = null;
3890 $orderBy = 'wp.url';
3891 $offset = false;
3892 $rowcount = false;
3893 $extraWhere = null;
3894
3895 if ( isset( $websiteid ) && ( null !== $websiteid ) ) {
3896 $website = static::instance()->get_website_by_id( $websiteid );
3897
3898 if ( ! MainWP_System_Utility::can_edit_website( $website ) ) {
3899 return false;
3900 }
3901
3902 if ( ! \mainwp_current_user_can( 'site', $websiteid ) ) {
3903 return false;
3904 }
3905
3906 return array(
3907 array(
3908 'id' => $websiteid,
3909 'url' => MainWP_Utility::get_nice_url( $website->url, true ),
3910 'name' => $website->name,
3911 'totalsize' => $website->totalsize,
3912 'sync_errors' => $website->sync_errors,
3913 ),
3914 );
3915 } else {
3916 if ( isset( $others['orderby'] ) ) {
3917 if ( 'site' === $others['orderby'] ) {
3918 $orderBy = 'wp.name ' . ( 'asc' === $others['order'] ? 'asc' : 'desc' );
3919 } elseif ( 'url' === $others['orderby'] ) {
3920 $orderBy = 'wp.url ' . ( 'asc' === $others['order'] ? 'asc' : 'desc' );
3921 }
3922 }
3923 if ( isset( $others['search'] ) ) {
3924 $search_site = trim( $others['search'] );
3925 }
3926
3927 if ( is_array( $others ) && isset( $others['plugins_slug'] ) ) {
3928 $slugs = explode( ',', $others['plugins_slug'] );
3929 $extraWhere = '';
3930 foreach ( $slugs as $slug ) {
3931 $slug = wp_json_encode( $slug );
3932 $slug = trim( $slug, '"' );
3933 $slug = str_replace( '\\', '.', $slug );
3934 $extraWhere .= ' wp.plugins REGEXP "' . $slug . '" OR';
3935 }
3936 $extraWhere = trim( rtrim( $extraWhere, 'OR' ) );
3937
3938 if ( '' === $extraWhere ) {
3939 $extraWhere = null;
3940 } else {
3941 $extraWhere = '(' . $extraWhere . ')';
3942 }
3943 }
3944 }
3945
3946 $totalRecords = '';
3947
3948 if ( isset( $others['per_page'] ) && ! empty( $others['per_page'] ) ) {
3949 $sql = static::instance()->get_sql_websites_for_current_user( false, $search_site, $orderBy, false, false, $extraWhere, $for_manager );
3950 $websites_total = static::instance()->query( $sql );
3951 $totalRecords = ( $websites_total ? static::num_rows( $websites_total ) : 0 );
3952
3953 if ( $websites_total ) {
3954 static::free_result( $websites_total );
3955 }
3956
3957 $rowcount = absint( $others['per_page'] );
3958 $pagenum = isset( $others['paged'] ) ? absint( $others['paged'] ) : 0;
3959 if ( $pagenum > $totalRecords ) {
3960 $pagenum = $totalRecords;
3961 }
3962 $pagenum = max( 1, $pagenum );
3963 $offset = ( $pagenum - 1 ) * $rowcount;
3964
3965 }
3966
3967 $sql = static::instance()->get_sql_websites_for_current_user( false, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager );
3968 $websites = static::instance()->query( $sql );
3969
3970 $output = array();
3971 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
3972 $re = array(
3973 'id' => $website->id,
3974 'url' => MainWP_Utility::get_nice_url( $website->url, true ),
3975 'name' => $website->name,
3976 'totalsize' => $website->totalsize,
3977 'sync_errors' => $website->sync_errors,
3978 'client_id' => $website->client_id,
3979 );
3980
3981 if ( 0 < $totalRecords ) {
3982 $re['totalRecords'] = $totalRecords;
3983 $totalRecords = 0;
3984 }
3985
3986 $output[] = $re;
3987 }
3988 static::free_result( $websites );
3989
3990 return $output;
3991 }
3992
3993 /**
3994 * Method get_lookup_items().
3995 *
3996 * Get bulk lookup items to reduce number of db queries.
3997 *
3998 * @param string $item_name lookup item name.
3999 * @param int $item_id lookup item id.
4000 * @param string $obj_name loockup object name.
4001 *
4002 * @return mixed Result
4003 */
4004 public function get_lookup_items( $item_name, $item_id, $obj_name ) {
4005 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.
4006 }
4007
4008 /**
4009 * Method insert_lookup_item().
4010 *
4011 * Insert lookup item, need checks existed before to prevent double values.
4012 *
4013 * @param string $item_name item name.
4014 * @param int $item_id item id.
4015 * @param string $obj_name object name.
4016 * @param int $obj_id object id.
4017 *
4018 * @return mixed Result
4019 */
4020 public function insert_lookup_item( $item_name, $item_id, $obj_name, $obj_id ) {
4021 // get_lookup_items() and delete_lookup_items() match item_name as given, so a
4022 // name that sanitizing would alter is refused instead of being stored under a
4023 // value the caller can never read back or delete.
4024 if ( ! is_string( $item_name ) || sanitize_text_field( $item_name ) !== $item_name ) {
4025 return false;
4026 }
4027 if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) {
4028 return false;
4029 }
4030 $data = array(
4031 'item_name' => $item_name,
4032 'item_id' => $item_id,
4033 'object_name' => $obj_name,
4034 'object_id' => $obj_id,
4035 );
4036 // wpdb refuses a value past the column length before any SQL runs, and
4037 // insert_id would still hold the previous insert's id.
4038 if ( false === $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data ) ) {
4039 return false;
4040 }
4041 return $this->wpdb->insert_id; // must return lookup id.
4042 }
4043
4044 /**
4045 * Method delete_lookup_items().
4046 *
4047 * Delete bulk lookup items by lookup ids or object names with item id and item name, to reduce number of db queries.
4048 *
4049 * @param string $by Delete by.
4050 * @param array $params params.
4051 *
4052 * @return mixed Result
4053 */
4054 public function delete_lookup_items( $by = 'lookup_id', $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
4055 if ( ! is_array( $params ) ) {
4056 return false;
4057 }
4058
4059 $lookup_ids = isset( $params['lookup_ids'] ) ? $params['lookup_ids'] : null;
4060 $item_id = isset( $params['item_id'] ) ? $params['item_id'] : null;
4061 $object_id = isset( $params['object_id'] ) ? $params['object_id'] : null;
4062 $item_name = isset( $params['item_name'] ) ? $params['item_name'] : null;
4063 $obj_names = isset( $params['object_names'] ) ? $params['object_names'] : null;
4064
4065 if ( 'object_name' === $by ) {
4066 if ( empty( $item_id ) || empty( $item_name ) ) {
4067 return false;
4068 }
4069
4070 $obj_names = $this->escape_array( $obj_names );
4071 if ( ! empty( $obj_names ) ) {
4072 $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.
4073 return true;
4074 }
4075 } elseif ( 'object_id' === $by ) {
4076 if ( empty( $object_id ) || empty( $item_name ) || empty( $obj_names ) ) {
4077 return false;
4078 }
4079
4080 $obj_names = $this->escape_array( $obj_names );
4081 if ( ! empty( $obj_names ) ) {
4082 $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.
4083 return true;
4084 }
4085 } elseif ( 'lookup_id' === $by ) {
4086 if ( empty( $lookup_ids ) ) {
4087 return false;
4088 }
4089 if ( is_numeric( $lookup_ids ) ) {
4090 $lookup_ids = array( $lookup_ids );
4091 } elseif ( is_array( $lookup_ids ) ) {
4092 $lookup_ids = MainWP_Utility::array_numeric_filter( $lookup_ids );
4093 } else {
4094 return false;
4095 }
4096 $this->wpdb->query( 'DELETE FROM ' . $this->table_name( 'lookup_item_objects' ) . ' WHERE lookup_id IN (' . implode( ',', $lookup_ids ) . ') ' ); //phpcs:ignore -- ok.
4097 return true;
4098 }
4099 return false;
4100 }
4101
4102
4103 /**
4104 * Insert a new REST API key row and return the credential payload.
4105 *
4106 * Returns an array with key_id, user_id, plaintext consumer_key,
4107 * plaintext consumer_secret, and key_permissions on success. Returns
4108 * false when the wpdb->insert() call fails (no current user, missing
4109 * table, schema mismatch, etc.). Callers must check the return type
4110 * before treating it as an array.
4111 *
4112 * @param string $consumer_key Consumer key.
4113 * @param string $consumer_secret Secret key.
4114 * @param string $scope scope.
4115 * @param string $description description.
4116 * @param int $enabled 1 or 0.
4117 * @param array $others others.
4118 *
4119 * @return array|false Credential payload on success, false on failure.
4120 */
4121 public function insert_rest_api_key( $consumer_key, $consumer_secret, $scope, $description, $enabled, $others = array() ) {
4122 global $current_user;
4123
4124 if ( $current_user ) {
4125 $user_id = $current_user->ID;
4126 }
4127
4128 if ( empty( $user_id ) ) {
4129 return false;
4130 }
4131
4132 unset( $others ); // Parameter retained for signature compatibility; key_pass/key_type fields are vestigial after MWP-1544 cleanup.
4133
4134 // Hash the consumer_secret with WordPress's password hasher so the
4135 // value at rest is no longer reversible by a DB-read primitive.
4136 // The plaintext is returned to the caller below so it can be shown
4137 // to the admin once at creation time. See MWP-1540.
4138 $hashed_secret = wp_hash_password( $consumer_secret );
4139
4140 // Created API keys.
4141 $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4142 $inserted = $this->wpdb->insert(
4143 $this->table_name( 'api_keys' ),
4144 array(
4145 'user_id' => $user_id,
4146 'description' => $description,
4147 'permissions' => $permissions,
4148 'consumer_key' => mainwp_api_hash( $consumer_key ),
4149 'consumer_secret' => $hashed_secret,
4150 'truncated_key' => substr( $consumer_key, -7 ),
4151 'enabled' => $enabled,
4152 ),
4153 array(
4154 '%d',
4155 '%s',
4156 '%s',
4157 '%s',
4158 '%s',
4159 '%s',
4160 '%d',
4161 ),
4162 );
4163
4164 // wpdb->insert() returns false on failure. Without this guard the
4165 // function would still hand back the plaintext consumer_secret plus
4166 // $wpdb->insert_id, but that insert_id is the LAST successful insert
4167 // on the connection (typically from earlier in the same request),
4168 // not this row. The caller's empty-key_id check would pass and the
4169 // operator would receive a credential that was never persisted.
4170 // See MWP-1540 PR review feedback.
4171 if ( false === $inserted ) {
4172 return false;
4173 }
4174
4175 return array(
4176 'key_id' => $this->wpdb->insert_id,
4177 'user_id' => $user_id,
4178 'consumer_key' => $consumer_key,
4179 'consumer_secret' => $consumer_secret,
4180 'key_permissions' => $permissions,
4181 );
4182 }
4183
4184 /**
4185 * Update rest api key.
4186 *
4187 * @param int $key_id Consumer key.
4188 * @param string $scope scope.
4189 * @param string $description description.
4190 * @param int $enabled Enabled.
4191 *
4192 * @return array
4193 */
4194 public function update_rest_api_key( $key_id, $scope, $description, $enabled = 1 ) {
4195 $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4196 return $this->wpdb->update(
4197 $this->table_name( 'api_keys' ),
4198 array(
4199 'description' => $description,
4200 'permissions' => $permissions,
4201 'enabled' => $enabled ? 1 : 0,
4202 ),
4203 array(
4204 'key_id' => $key_id,
4205 )
4206 );
4207 }
4208
4209
4210 /**
4211 * Method is_existed_enabled_rest_key().
4212 *
4213 * @return bool result.
4214 */
4215 public function is_existed_enabled_rest_key() {
4216 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4217 $enabled = $this->wpdb->get_row( "SELECT * FROM {$table_name} WHERE enabled = 1 LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4218 return $enabled ? true : false;
4219 }
4220
4221 /**
4222 * Method get_rest_api_key_by().
4223 *
4224 * @param int $id To get key.
4225 *
4226 * @return array
4227 */
4228 public function get_rest_api_key_by( $id ) {
4229 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4230 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table_name} WHERE key_id = %d", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4231 }
4232
4233 /**
4234 * Method remove_rest_api_key().
4235 *
4236 * @param string $id to delete.
4237 *
4238 * @return array
4239 */
4240 public function remove_rest_api_key( $id ) {
4241 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4242 return $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE key_id = %s", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4243 }
4244
4245 /**
4246 * Method get_rest_api_keys().
4247 *
4248 * @return array
4249 */
4250 public function get_rest_api_keys() {
4251 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4252 return $this->wpdb->get_results( "SELECT * FROM {$table_name} ORDER BY key_id DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4253 }
4254
4255
4256 /**
4257 * Update regular process.
4258 *
4259 * @param array $data process data.
4260 * @return mixed
4261 */
4262 public function update_regular_process( $data ) {
4263 if ( isset( $data['process_id'] ) ) {
4264 $process_id = $data['process_id'];
4265 unset( $data['process_id'] );
4266 return $this->wpdb->update( $this->table_name( 'schedule_processes' ), $data, array( 'process_id' => $process_id ) );
4267 } elseif ( is_array( $data ) && isset( $data['type'] ) && isset( $data['process_slug'] ) ) {
4268 return $this->wpdb->insert( $this->table_name( 'schedule_processes' ), $data );
4269 }
4270 return false;
4271 }
4272
4273 /**
4274 * Delete regular process.
4275 *
4276 * @param int $process_id Process id.
4277 * @param int $item_id Item id.
4278 * @param string $pro_type Process type.
4279 * @param string $pro_slug Process slug.
4280 *
4281 * @return mixed
4282 */
4283 public function delete_regular_process( $process_id = false, $item_id = false, $pro_type = false, $pro_slug = false ) {
4284
4285 if ( is_numeric( $process_id ) && ! empty( $process_id ) ) {
4286 return $this->wpdb->delete(
4287 $this->table_name( 'schedule_processes' ),
4288 array(
4289 'process_id' => $process_id,
4290 )
4291 );
4292 } elseif ( ! empty( $pro_type ) || ! empty( $pro_slug ) ) {
4293
4294 $data = array();
4295
4296 if ( ! empty( $pro_type ) ) {
4297 $data['type'] = $pro_type;
4298 }
4299
4300 if ( ! empty( $pro_slug ) ) {
4301 $data['process_slug'] = $pro_slug;
4302 }
4303
4304 if ( ! empty( $item_id ) ) {
4305 $data['item_id'] = $item_id;
4306 }
4307 // Bulk delete.
4308 return $this->wpdb->delete( $this->table_name( 'schedule_processes' ), $data );
4309 }
4310 return false;
4311 }
4312
4313 /**
4314 * Method get_regular_process_by_item_id_type_slug
4315 *
4316 * @param integer $item_id item id.
4317 * @param string $type type.
4318 * @param string $process_slug process slug.
4319 *
4320 * @return mixed result
4321 */
4322 public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) {
4323 $table_name = esc_sql( $this->table_name( 'schedule_processes' ) );
4324 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT pr.* FROM {$table_name} pr WHERE pr.item_id = %d AND pr.type = %s AND pr.process_slug = %s", $item_id, $type, $process_slug ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is esc_sql'd.
4325 }
4326
4327 /**
4328 * Log SQL queries for debugging via hook.
4329 *
4330 * This method provides a structured way to log SQL queries for development/debugging.
4331 * It does NOT use error_log() directly - instead, it fires the `mainwp_log_system_query`
4332 * action hook, allowing external listeners (logging plugins, debug tools) to handle
4333 * the log output appropriately.
4334 *
4335 * To enable query logging:
4336 * 1. Set `$params['dev_log_query'] = 1` when calling database methods
4337 * 2. Add a listener to the `mainwp_log_system_query` action hook
4338 *
4339 * Example listener:
4340 * ```php
4341 * add_action( 'mainwp_log_system_query', function( $params, $sql, $caller ) {
4342 * error_log( 'MainWP Query: ' . $sql );
4343 * }, 10, 3 );
4344 * ```
4345 *
4346 * @param array $params Query parameters. Set 'dev_log_query' to enable logging.
4347 * @param string $sql The SQL query string.
4348 * @param mixed $caller Instance of caller class (optional).
4349 * @return void
4350 */
4351 public function log_system_query( $params, $sql, $caller = false ) {
4352 $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller );
4353 if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) {
4354 do_action( 'mainwp_log_system_query', $params, $sql, $caller );
4355 }
4356 }
4357
4358 /**
4359 * Method get_core_tables().
4360 *
4361 * Returns the list of core tables used by MainWP for validation data integrity and other purposes.
4362 *
4363 * @return array Core tables info
4364 */
4365 public function get_core_tables() {
4366
4367 $core_tables = array(
4368 $this->table_name( 'wp_clients' ),
4369 $this->table_name( 'wp_clients_fields' ),
4370 $this->table_name( 'wp_clients_field_values' ),
4371 $this->table_name( 'wp_clients_contacts' ),
4372 $this->table_name( 'monitors' ),
4373 $this->table_name( 'monitor_heartbeat' ),
4374 $this->table_name( 'monitor_stat_hourly' ),
4375 $this->table_name( 'wp' ),
4376 $this->table_name( 'wp_sync' ),
4377 $this->table_name( 'wp_options' ),
4378 $this->table_name( 'wp_settings_backup' ),
4379 $this->table_name( 'users' ),
4380 $this->table_name( 'wp_status' ),
4381 $this->table_name( 'group' ),
4382 $this->table_name( 'wp_group' ),
4383 $this->table_name( 'lookup_item_objects' ),
4384 $this->table_name( 'wp_backup_progress' ),
4385 $this->table_name( 'wp_backup' ),
4386 $this->table_name( 'api_keys' ),
4387 $this->table_name( 'action_log' ),
4388 $this->table_name( 'request_log' ),
4389 $this->table_name( 'schedule_processes' ),
4390 $this->table_name( 'cost_tracker' ),
4391 );
4392
4393 if ( ! defined( 'MAINWP_MODULE_LOG_ENABLED' ) || MAINWP_MODULE_LOG_ENABLED ) {
4394 $core_tables[] = $this->table_name( 'wp_logs' );
4395 $core_tables[] = $this->table_name( 'wp_logs_meta' );
4396 $core_tables[] = $this->table_name( 'wp_logs_meta_archive' );
4397 }
4398
4399 return apply_filters( 'mainwp_database_core_tables', $core_tables );
4400 }
4401 }
4402