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

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

4,193 lines 167.4 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 * Get wp_options database table view.
88 *
89 * @compatible function.
90 *
91 * @param array $fields Extra option fields.
92 * @param string $view_query view query.
93 *
94 * @return array wp_options view.
95 */
96 public function get_option_view( $fields = array(), $view_query = 'default' ) {
97
98 if ( ! is_array( $fields ) ) {
99 $fields = array();
100 }
101
102 $view = '(SELECT intwp.id AS wpid ';
103
104 $included_opts = array();
105
106 if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) {
107 $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,
108 (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,
109 (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,
110 (SELECT phpversion.value FROM ' . $this->table_name( 'wp_options' ) . ' phpversion WHERE phpversion.wpid = intwp.id AND phpversion.name = "phpversion" LIMIT 1) AS phpversion,
111 (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,
112 (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 ';
113 $included_opts = array( 'recent_comments', 'recent_posts', 'recent_pages', 'phpversion', 'added_timestamp', 'wp_upgrades' );
114 }
115
116 if ( ! in_array( 'signature_algo', $fields ) ) {
117 $fields[] = 'signature_algo';
118 }
119
120 if ( ! in_array( 'verify_method', $fields ) ) {
121 $fields[] = 'verify_method';
122 }
123
124 if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) {
125 $fields[] = 'cust_site_icon_info';
126 }
127
128 if ( is_array( $fields ) ) {
129 foreach ( $fields as $field ) {
130 if ( empty( $field ) ) {
131 continue;
132 }
133 if ( in_array( $field, $included_opts ) ) {
134 continue;
135 }
136 $view .= ', ';
137 $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 );
138 }
139 }
140
141 $view .= ' FROM ' . $this->table_name( 'wp' ) . ' intwp)';
142
143 return $view;
144 }
145
146 /**
147 * Method get_wp_options_join().
148 *
149 * @param array $fields Extra option fields.
150 * @param string $view_query view query.
151 * @param array $params Additional parameters.
152
153 *
154 * 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.
155 * 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.
156 *
157 * @since 6.0.8
158 *
159 * @return array wp_options view.
160 */
161 public function get_wp_options_join( $fields = array(), $view_query = 'default', $params = array() ) {
162
163 if ( ! is_array( $fields ) ) {
164 $fields = array();
165 }
166
167 if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) {
168 $fields[] = 'recent_comments';
169 $fields[] = 'recent_posts';
170 $fields[] = 'recent_pages';
171 $fields[] = 'phpversion';
172 $fields[] = 'added_timestamp';
173 $fields[] = 'wp_upgrades';
174 }
175
176 if ( ! in_array( 'signature_algo', $fields ) ) {
177 $fields[] = 'signature_algo';
178 }
179
180 if ( ! in_array( 'verify_method', $fields ) ) {
181 $fields[] = 'verify_method';
182 }
183
184 if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) {
185 $fields[] = 'cust_site_icon_info';
186 }
187
188 $fields = array_values( array_unique( array_filter( $fields ) ) );
189
190 $tbl_wp_options = $this->table_name( 'wp_options' );
191
192 $selects = array();
193 $joins = array();
194
195 foreach ( $fields as $name ) {
196 $alias = 'owp_' . preg_replace( '/[^a-z0-9_]/i', '_', $name );
197
198 // SELECT.
199 $selects[] = "{$alias}.value AS `" . $this->escape( $name ) . '`';
200
201 // JOIN.
202 $joins[] = "LEFT JOIN {$tbl_wp_options} {$alias}
203 ON {$alias}.wpid = wp.id
204 AND {$alias}.name = '" . $this->escape( $name ) . "'";
205 }
206
207 return array(
208 'selects' => implode( ', ', $selects ),
209 'joins' => implode( "\n", $joins ),
210 );
211 }
212
213 /**
214 * Method get_wp_options_view().
215 *
216 * @param array $fields Extra option fields.
217 * @param string $view_query view query.
218 * @param int $siteid Site id.
219 *
220 * @return string SQL subquery for wp_options view.
221 */
222 public function get_wp_options_view( $fields = array(), $view_query = 'default', $siteid = 0 ) {
223
224 if ( ! is_array( $fields ) ) {
225 $fields = array();
226 }
227
228 $where_site = '';
229
230 if ( ! empty( $siteid ) && is_numeric( $siteid ) ) {
231 $where_site = ' AND wpid = ' . intval( $siteid ) . ' ';
232 }
233
234 $view = '(SELECT wpid ';
235
236 $included_opts = array();
237
238 if ( empty( $fields ) || 'default' === $view_query || 'manage_site' === $view_query ) {
239 $view .= ',
240 MAX(CASE WHEN name = "recent_comments" THEN value END) AS recent_comments,
241 MAX(CASE WHEN name = "recent_posts" THEN value END) AS recent_posts,
242 MAX(CASE WHEN name = "recent_pages" THEN value END) AS recent_pages,
243 MAX(CASE WHEN name = "phpversion" THEN value END) AS phpversion,
244 MAX(CASE WHEN name = "added_timestamp" THEN value END) AS added_timestamp,
245 MAX(CASE WHEN name = "wp_upgrades" THEN value END) AS wp_upgrades ';
246 $included_opts = array( 'recent_comments', 'recent_posts', 'recent_pages', 'phpversion', 'added_timestamp', 'wp_upgrades' );
247 }
248
249 if ( ! in_array( 'signature_algo', $fields ) ) {
250 $fields[] = 'signature_algo';
251 }
252
253 if ( ! in_array( 'verify_method', $fields ) ) {
254 $fields[] = 'verify_method';
255 }
256
257 if ( ! in_array( 'cust_site_icon_info', $fields, true ) ) {
258 $fields[] = 'cust_site_icon_info';
259 }
260
261 if ( is_array( $fields ) ) {
262 foreach ( $fields as $field ) {
263 if ( empty( $field ) ) {
264 continue;
265 }
266 if ( in_array( $field, $included_opts ) ) {
267 continue;
268 }
269 $view .= ', ';
270 $view .= 'MAX(CASE WHEN name = "' . $this->escape( $field ) . '" THEN value END) AS ' . $this->escape( $field );
271
272 $included_opts[] = $this->escape( $field );
273 }
274 }
275
276 $view .= ' FROM ' . $this->table_name( 'wp_options' ) .
277 " WHERE 1 {$where_site} AND name IN ('" . implode( "','", $included_opts ) . "')
278 GROUP BY wpid ) ";
279
280 return $view;
281 }
282
283
284
285 /**
286 * Get SQL to get child sites for current user.
287 *
288 * @since 5.2.
289 * @param array $params other params.
290 *
291 * @return object|null Database query results or null on failure.
292 */
293 public function get_sql_websites_for_current_user_by_params( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
294
295 if ( ! is_array( $params ) ) {
296 $params = array();
297 }
298
299 /**
300 * The hook mainwp_get_sql_websites_by_params
301 *
302 * @since 5.5
303 */
304 $params = apply_filters( 'mainwp_get_sql_websites_by_params', $params );
305
306 $view = isset( $params['view'] ) ? $params['view'] : 'default';
307 $with_clients = isset( $params['with_clients'] ) && $params['with_clients'] ? true : false;
308
309 // legacy support.
310 $selectgroups = isset( $params['with_tags'] ) && $params['with_tags'] ? true : false;
311 $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url';
312 $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false;
313 $rowcount = isset( $params['rowcount'] ) && $params['rowcount'] ? true : false;
314 $extraWhere = isset( $params['where'] ) ? $params['where'] : null; // NOTE: without 'AND' at begining and ending of 'where'.
315 $for_manager = isset( $params['for_manager'] ) && $params['for_manager'] ? true : false;
316 $others_fields = isset( $params['others_fields'] ) && is_array( $params['others_fields'] ) ? $params['others_fields'] : array( 'favi_icon' );
317 $is_staging = isset( $params['is_staging'] ) && in_array( $params['is_staging'], array( 'yes', 'no' ) ) ? $params['is_staging'] : 'no';
318 $limit = isset( $params['limit'] ) ? intval( $params['limit'] ) : '';
319
320 $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
321
322 $s = isset( $params['s'] ) ? $params['s'] : '';
323 $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
324 $include = isset( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array();
325 $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array();
326 $page = isset( $params['page'] ) ? intval( $params['page'] ) : false;
327 $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false;
328
329 // This parameter is used to enable caching in certain cases.
330 $_included_cache_ids = isset( $params['_included_cache_ids'] ) ? wp_parse_id_list( $params['_included_cache_ids'] ) : array();
331
332 $select_wpfields = isset( $params['select_wp_fields'] ) ? wp_parse_list( $params['select_wp_fields'] ) : '';
333 $select_syncfields = isset( $params['select_sync_fields'] ) ? wp_parse_list( $params['select_sync_fields'] ) : '';
334
335 $where = '';
336
337 if ( ! empty( $extraWhere ) ) {
338 $where .= ' AND ' . $extraWhere;
339 }
340
341 if ( ! $for_manager ) {
342 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
343 }
344
345 $connected_sql = '';
346
347 if ( is_array( $params ) && isset( $params['connected'] ) && 'yes' === $params['connected'] ) {
348 $connected_sql = ' AND wp_sync.sync_errors = "" ';
349 } elseif ( is_array( $params ) && isset( $params['connected'] ) && 'no' === $params['connected'] ) {
350 $connected_sql = ' AND wp_sync.sync_errors <> "" ';
351 }
352
353 if ( ! empty( $s ) ) {
354 $s = trim( $s );
355 // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety.
356 $like_pattern = '%' . $this->wpdb->esc_like( $s ) . '%';
357 $where .= $this->wpdb->prepare(
358 ' AND ( wp.id LIKE %s OR wp.name LIKE %s OR wp.url LIKE %s ) ',
359 $like_pattern,
360 $like_pattern,
361 $like_pattern
362 );
363 }
364
365 if ( ! empty( $exclude ) ) {
366 $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') ';
367 }
368
369 if ( ! empty( $include ) ) {
370 $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') ';
371 }
372
373 if ( ! empty( $_included_cache_ids ) ) {
374 $where .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') ';
375 }
376
377 $specific_wp_fields = '';
378 if ( ! empty( $select_wpfields ) ) {
379 foreach ( $select_wpfields as $_field ) {
380 $specific_wp_fields .= 'wp.' . $this->escape( $_field ) . ',';
381 }
382 $specific_wp_fields = rtrim( $specific_wp_fields, ',' );
383 }
384
385 $specific_sync_fields = '';
386 if ( ! empty( $select_syncfields ) ) {
387 foreach ( $select_syncfields as $_field ) {
388 $specific_sync_fields .= 'wp_sync.' . $this->escape( $_field ) . ',';
389 }
390 $specific_sync_fields = rtrim( $specific_sync_fields, ',' );
391 }
392
393 // any, connected, disconnected, suspended, available_update.
394 if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) {
395 $status_conds = array();
396 if ( in_array( 'available_update', $status ) ) {
397 $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 <> '[]' ) ";
398 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
399 $results = $this->wpdb->get_results( "SELECT wpid FROM {$table_name} WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]'" );
400 if ( $results ) {
401 $wp_ids = array();
402 foreach ( $results as $item ) {
403 if ( ! empty( $item->wpid ) ) {
404 $wp_ids[] = $item->wpid;
405 }
406 }
407 $wp_ids = ! empty( $wp_ids ) ? array_unique( $wp_ids ) : array();
408 if ( ! empty( $wp_ids ) ) {
409 $available_sql .= ' OR wp.id IN ( ' . implode( ',', $wp_ids ) . ' )';
410 }
411 }
412 $status_conds[] = ' ( ' . $available_sql . ') ';
413 }
414
415 if ( in_array( 'connected', $status ) ) {
416 $status_conds[] = ' ( wp_sync.sync_errors = "" ) ';
417 }
418 if ( in_array( 'disconnected', $status ) ) {
419 $status_conds[] = " wp_sync.sync_errors <> '' ";
420 }
421
422 if ( in_array( 'suspended', $status ) ) {
423 $status_conds[] = ' wp.suspended = 1 ';
424 }
425
426 if ( ! empty( $status_conds ) ) {
427 $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) ';
428 }
429 if ( in_array( 'unsuspended', $status ) && ! in_array( 'suspended', $status ) ) { // to sure not conflict the suspended status.
430 $where .= ' AND wp.suspended = 0 ';
431 }
432 }
433
434 if ( ! empty( $page ) && ! empty( $per_page ) ) {
435 $limit = ( $page - 1 ) * $per_page . ',' . $per_page;
436 }
437
438 if ( 'wp.url' === $orderBy ) {
439 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
440 }
441
442 $select_clients = '';
443 $join_clients = '';
444
445 if ( $with_clients ) {
446 $select_clients = ', wpclient.name as client_name ';
447 $clients_table = esc_sql( $this->table_name( 'wp_clients' ) );
448 $join_clients = " LEFT JOIN {$clients_table} wpclient ON wp.client_id = wpclient.client_id ";
449 }
450
451 $base_fields = array(
452 'wp.id',
453 'wp.url',
454 'wp.name',
455 'wp.client_id',
456 'wp.verify_certificate',
457 'wp.http_user',
458 'wp.http_pass',
459 'wp.ssl_version',
460 'wp.adminname',
461 'wp.privkey',
462 'wp.pubkey',
463 'wp.wpe',
464 'wp.is_staging',
465 'wp.force_use_ipv4',
466 'wp.siteurl',
467 'wp.suspended',
468 'wp.mainwpdir',
469 'wp.is_ignoreCoreUpdates',
470 'wp.is_ignorePluginUpdates',
471 'wp.is_ignoreThemeUpdates',
472 'wp_sync.sync_errors',
473 'wp.backup_before_upgrade',
474 'wp.userid',
475 'wp.plugins',
476 'wp.themes',
477 'wp.offline_check_result', // 1 - online, -1 offline.
478 'wp.automatic_update',
479 );
480
481 $select = ' wp.*,wp_sync.* ';
482 if ( 'base_view' === $view ) {
483 $select = implode( ',', $base_fields );
484 } elseif ( 'updates_view' === $view ) {
485 $updates_fields = array(
486 'wp.plugin_upgrades',
487 'wp.theme_upgrades',
488 'wp.translation_upgrades',
489 'wp.premium_upgrades',
490 'wp.ignored_themes',
491 'wp.ignored_plugins',
492 );
493 $select = implode( ',', array_merge( $updates_fields, $base_fields ) );
494 }
495
496 $view_selects = '';
497 $view_joins = '';
498
499 if ( $use_comp_subquery ) {
500 $view_selects = ',wp_optionview.* ';
501 $view_joins = ' JOIN ' . $this->get_option_view_by( $view, $others_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid ';
502 } else {
503 $opts_view = $this->get_option_view_by_join( $view, $others_fields );
504 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
505 $view_selects = ',' . $opts_view['selects'];
506 $view_joins = $opts_view['joins'];
507 }
508 }
509
510 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
511 if ( $selectgroups ) {
512 $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 ' .
513 $select_clients . '
514
515 FROM ' . $this->table_name( 'wp' ) . ' wp
516 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
517 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
518 ' . $join_clients . '
519 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
520 ' . $view_joins . '
521 WHERE 1 ' . $where . $connected_sql . '
522 GROUP BY wp.id, wp_sync.sync_id
523 ORDER BY ' . $orderBy;
524 } elseif ( ! empty( $specific_wp_fields ) ) { // Optimize select sites data.
525 $select = $specific_wp_fields;
526 $join_sync = '';
527 $group_by = 'wp.id';
528
529 if ( ! empty( $specific_sync_fields ) ) {
530 $select .= ',' . $specific_sync_fields;
531 $join_sync = ' JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid';
532 $group_by .= ', wp_sync.sync_id';
533 }
534 $qry = 'SELECT ' . $select . $view_selects . '
535 FROM ' . $this->table_name( 'wp' ) . ' wp
536 ' . $join_sync . ' ' . $view_joins . '
537 WHERE 1 ' . $where . $connected_sql . '
538 GROUP BY ' . $group_by . '
539 ORDER BY ' . $orderBy;
540 } else {
541 $qry = 'SELECT ' . $select . $view_selects . $select_clients . '
542 FROM ' . $this->table_name( 'wp' ) . ' wp
543 ' . $join_clients . '
544 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
545 ' . $view_joins . '
546 WHERE 1 ' . $where . $connected_sql . '
547 GROUP BY wp.id, wp_sync.sync_id
548 ORDER BY ' . $orderBy;
549 }
550
551 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
552 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
553 } elseif ( false !== $rowcount ) {
554 $qry .= ' LIMIT ' . $rowcount;
555 } elseif ( ! empty( $limit ) ) {
556 $qry .= ' LIMIT ' . $limit;
557 } else {
558 // load all sites so check to support limit sites loading.
559 $limit_sites = ! empty( $params['limit_sites'] ) ? intval( $params['limit_sites'] ) : 0;
560 if ( ! empty( $limit_sites ) ) {
561 $current_page = (int) get_option( 'mainwp_manage_updates_limit_current_page', 0 );
562 $current_page = $current_page > 0 ? $current_page - 1 : 0;
563 $start = $current_page * $limit_sites;
564 $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites );
565 }
566 }
567
568 if ( ! empty( $_included_cache_ids ) ) {
569 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql websites by params=%s]', $qry ) );
570 }
571 MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql websites by params=%s]', $qry ) );
572 return $qry;
573 }
574
575 /**
576 * Improve get wp_options database table view.
577 *
578 * @param array $view Option view.
579 * @param array $other_fields Extra option fields.
580 *
581 * @return array wp_options view.
582 */
583 public function get_option_view_by_join( $view = '', $other_fields = array() ) {
584
585 $default = array(
586 'recent_comments',
587 'recent_posts',
588 'recent_pages',
589 'phpversion',
590 'added_timestamp',
591 'wp_upgrades',
592 );
593
594 $fields = array();
595
596 if ( 'updates_view' === $view ) {
597 $fields = array(
598 'wp_upgrades',
599 'ignored_wp_upgrades',
600 'ignored_trans_updates',
601 );
602 } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) {
603 $fields = array();
604 if ( 'monitor_view' === $view ) {
605 $fields[] = 'health_site_status';
606 }
607 } elseif ( 'custom_view' !== $view ) {
608 $fields = $default;
609 }
610
611 if ( is_array( $other_fields ) && ! empty( $other_fields ) ) {
612 $fields = array_unique( array_merge( $fields, $other_fields ) );
613 }
614
615 if ( 'custom_view' !== $view ) {
616 if ( ! in_array( 'signature_algo', $fields ) ) {
617 $fields[] = 'signature_algo';
618 }
619
620 if ( ! in_array( 'verify_method', $fields ) ) {
621 $fields[] = 'verify_method';
622 }
623 }
624
625 $fields = array_values( array_filter( $fields ) );
626
627 $tbl_wp_options = $this->table_name( 'wp_options' );
628
629 $selects = array();
630 $joins = array();
631
632 foreach ( $fields as $name ) {
633 $alias = 'owp_' . preg_replace( '/[^a-z0-9_]/i', '_', $name );
634
635 // SELECT.
636 $selects[] = "{$alias}.value AS `" . $this->escape( $name ) . '`';
637
638 // JOIN.
639 $joins[] = "LEFT JOIN {$tbl_wp_options} {$alias}
640 ON {$alias}.wpid = wp.id
641 AND {$alias}.name = '" . $this->escape( $name ) . "'";
642 }
643
644 return array(
645 'selects' => implode( ', ', $selects ),
646 'joins' => implode( "\n", $joins ),
647 );
648 }
649
650 /**
651 * Get wp_options database table view.
652 *
653 * 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.
654 *
655 * @param array $view Option view.
656 * @param array $other_fields Extra option fields.
657 *
658 * @return array wp_options view.
659 */
660 public function get_option_view_by( $view = '', $other_fields = array() ) {
661
662 $default = array(
663 'recent_comments',
664 'recent_posts',
665 'recent_pages',
666 'phpversion',
667 'added_timestamp',
668 'wp_upgrades',
669 );
670
671 $fields = array();
672
673 if ( 'updates_view' === $view ) {
674 $fields = array(
675 'wp_upgrades',
676 'ignored_wp_upgrades',
677 'ignored_trans_updates',
678 );
679 } elseif ( in_array( $view, array( 'simple_view', 'base_view', 'monitor_view', 'ping_view', 'uptime_notification' ) ) ) {
680 $fields = array();
681 if ( 'monitor_view' === $view ) {
682 $fields[] = 'health_site_status';
683 }
684 } elseif ( 'custom_view' !== $view ) {
685 $fields = $default;
686 }
687
688 if ( is_array( $other_fields ) && ! empty( $other_fields ) ) {
689 $fields = array_unique( array_merge( $fields, $other_fields ) );
690 }
691
692 $view_query = '(SELECT wpid ';
693
694 if ( 'custom_view' !== $view ) {
695 if ( ! in_array( 'signature_algo', $fields ) ) {
696 $fields[] = 'signature_algo';
697 }
698
699 if ( ! in_array( 'verify_method', $fields ) ) {
700 $fields[] = 'verify_method';
701 }
702 }
703
704 foreach ( $fields as $field ) {
705
706 if ( empty( $field ) ) {
707 continue;
708 }
709
710 $view_query .= ', ';
711 $view_query .= 'MAX(CASE WHEN name = "' . $this->escape( $field ) . '" THEN value END) AS ' . $this->escape( $field );
712 }
713
714 $view_query .= ' FROM ' . $this->table_name( 'wp_options' ) .
715 " WHERE name IN ('" . implode( "','", $fields ) . "')
716 GROUP BY wpid ) ";
717
718 return $view_query;
719 }
720
721 /**
722 * Method get_select_groups_belong().
723 *
724 * @return string sql.
725 */
726 public function get_select_groups_belong() {
727 return ', ( SELECT GROUP_CONCAT(grbl.name ORDER BY grbl.name SEPARATOR ",")
728 FROM ' . $this->table_name( 'wp_group' ) . ' wpgrbl
729 JOIN ' . $this->table_name( 'group' ) . ' grbl ON grbl.id = wpgrbl.groupid WHERE wpgrbl.wpid = wp.id ) as wpgroups_belong,
730 ( SELECT GROUP_CONCAT(grbl.id ORDER BY grbl.name SEPARATOR ",") FROM ' . $this->table_name( 'wp_group' ) . ' wpgrbl
731 JOIN ' . $this->table_name( 'group' ) . ' grbl ON grbl.id = wpgrbl.groupid WHERE wpgrbl.wpid = wp.id ) as wpgroupids_belong,
732 ( SELECT GROUP_CONCAT(grbl.color ORDER BY grbl.name SEPARATOR ",") FROM ' . $this->table_name( 'wp_group' ) . ' wpgrbl
733 JOIN ' . $this->table_name( 'group' ) . ' grbl ON grbl.id = wpgrbl.groupid WHERE wpgrbl.wpid = wp.id ) as wpgroupcolors_belong ';
734 }
735
736 /**
737 * Get connected child sites.
738 *
739 * @param array $sites_ids Websites ids - option field.
740 *
741 * @return array $connected_sites Array of connected sites.
742 */
743 public function get_connected_websites( $sites_ids = false ) {
744 $where = $this->get_sql_where_allow_access_sites( 'wp' );
745 $wp_table = esc_sql( $this->table_name( 'wp' ) );
746 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
747
748 $sql = "SELECT wp.*,wp_sync.*
749 FROM {$wp_table} wp
750 JOIN {$wp_sync_table} wp_sync
751 ON wp.id = wp_sync.wpid
752 WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors = \"\") " .
753 $where;
754
755 $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()
756 $connected_sites = array();
757 if ( $websites ) {
758 foreach ( $websites as $website ) {
759
760 if ( ! empty( $sites_ids ) && ! in_array( $website->id, $sites_ids ) ) {
761 continue;
762 }
763
764 $connected_sites[] = array(
765 'id' => $website->id,
766 'name' => $website->name,
767 'url' => $website->url,
768 );
769 }
770 }
771 return $connected_sites;
772 }
773
774 /**
775 * Get disconnected child sites.
776 *
777 * @param array $sites_ids Websites ids - option field.
778 *
779 * @return array $disc_sites Array of disonnected sites.
780 */
781 public function get_disconnected_websites( $sites_ids = false ) {
782 $where = $this->get_sql_where_allow_access_sites( 'wp' );
783 $wp_table = esc_sql( $this->table_name( 'wp' ) );
784 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
785
786 $sql = "SELECT wp.*,wp_sync.*
787 FROM {$wp_table} wp
788 JOIN {$wp_sync_table} wp_sync
789 ON wp.id = wp_sync.wpid
790 WHERE (wp_sync.sync_errors IS NOT NULL) AND (wp_sync.sync_errors <> \"\") " .
791 $where;
792
793 $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()
794 $disc_sites = array();
795 if ( $websites ) {
796 foreach ( $websites as $website ) {
797
798 if ( ! empty( $sites_ids ) && ! in_array( $website->id, $sites_ids ) ) {
799 continue;
800 }
801
802 $disc_sites[] = array(
803 'id' => $website->id,
804 'name' => $website->name,
805 'url' => $website->url,
806 );
807 }
808 }
809 return $disc_sites;
810 }
811
812 /**
813 * Get child site count.
814 *
815 * @param null $userId Current user ID.
816 * @param bool $all_access Check if user has access to all sites.
817 *
818 * @return int Child site count.
819 *
820 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
821 *
822 * @see get_websites_count_for_current_user() For Abilities API with status/tags/client filters.
823 * This method is intentionally simple for UI display purposes (total sites count).
824 * The two methods serve different use cases and should not be consolidated.
825 */
826 public function get_websites_count( $userId = null, $all_access = false ) {
827 static $total_sites;
828 if ( null !== $total_sites ) { // NOSONAR -- static value.
829 return $total_sites;
830 }
831 if ( ( null === $userId ) && MainWP_System::instance()->is_multi_user() ) {
832
833 /**
834 * Current user global.
835 *
836 * @global string
837 */
838 global $current_user;
839
840 $userId = $current_user->ID;
841 }
842 $where = ( null === $userId ? '' : ' wp.userid = ' . intval( $userId ) );
843 if ( ! $all_access ) {
844 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
845 }
846 $table_name = esc_sql( $this->table_name( 'wp' ) );
847 $qry = "SELECT COUNT(wp.id) FROM {$table_name} wp WHERE 1 {$where}";
848
849 $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()
850 $total_sites = $total;// NOSONAR -- static value.
851 return $total;
852 }
853
854
855 /**
856 * Get child sites stats count.
857 *
858 * @param array $params Params.
859 */
860 public function get_websites_stats_count( $params = array() ) {
861 if ( ! is_array( $params ) ) {
862 $params = array();
863 }
864
865 if ( isset( $params['all_access'] ) ) {
866 $all_access = ! empty( $params['all_access'] ) ? true : false;
867 } else {
868 $all_access = true;
869 }
870
871 $where = '';
872 if ( ! $all_access ) {
873 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
874 }
875
876 $select_stats = ' ( SELECT COUNT(wp.id) as count_all ';
877 if ( ! empty( $params['count_disconnected'] ) ) {
878 $select_stats .= ',( SELECT COUNT(wp_disconnected.id) FROM ' . $this->table_name( 'wp' ) . ' wp_disconnected LEFT JOIN ' . $this->table_name( 'wp_sync' ) . ' as wp_sync ';
879 $select_stats .= ' ON wp_disconnected.id = wp_sync.wpid WHERE wp_sync.sync_errors <> "" ) as count_disconnected ';
880 }
881 if ( ! empty( $params['count_suspended'] ) ) {
882 $select_stats .= ',( SELECT COUNT(wp_suspended.id) FROM ' . $this->table_name( 'wp' ) . ' wp_suspended WHERE wp_suspended.suspended = 1 ) as count_suspended ';
883 }
884 $qry = 'SELECT * FROM ' . $select_stats;
885 $qry .= ' FROM ' . $this->table_name( 'wp' ) . ' wp ' . $where . ' ) as wp_stats ';
886
887 return $this->wpdb->get_row( $qry, ARRAY_A ); //phpcs:ignore -- ok.
888 }
889
890 /**
891 * Get Child site wp_options database table.
892 *
893 * @param array $website Child Site array.
894 * @param mixed $option Child Site wp_options table name.
895 * @param mixed $default_value default value.
896 * @param mixed $json_format Is json format value.
897 *
898 * @return string|null Database query result (as string), or null on failure.
899 */
900 public function get_website_option( $website, $option, $default_value = null, $json_format = false ) { //phpcs:ignore -- NOSONAR - complex.
901
902 if ( is_array( $website ) ) {
903 if ( isset( $website[ $option ] ) ) {
904 $value = $website[ $option ];
905 if ( true === $json_format ) {
906 $value = ! empty( $value ) ? json_decode( $value, true ) : array();
907 return is_array( $value ) ? $value : array();
908 } else {
909 return $value;
910 }
911 }
912 $site_id = $website['id'];
913 } elseif ( is_object( $website ) ) {
914 if ( property_exists( $website, $option ) ) {
915 $value = $website->{$option};
916 if ( true === $json_format ) {
917 $value = ! empty( $value ) ? json_decode( $value, true ) : array();
918 return is_array( $value ) ? $value : array();
919 } else {
920 return $value;
921 }
922 }
923 $site_id = $website->id;
924 } elseif ( is_numeric( $website ) ) { // to support $site_id = 0, for global options.
925 $site_id = $website;
926 } else {
927 return false;
928 }
929
930 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
931 $value = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT value FROM {$table_name} WHERE wpid = %d AND name = %s", $site_id, $option ) );
932
933 if ( null === $value && null !== $default_value ) {
934 return $default_value;
935 }
936
937 if ( true === $json_format ) {
938 $value = ! empty( $value ) ? json_decode( $value, true ) : array();
939 return is_array( $value ) ? $value : array();
940 } else {
941 return $value;
942 }
943 }
944
945 /**
946 * Get Child site wp_options json value.
947 *
948 * @since 5.1.1
949 *
950 * @param array $website Child Site array.
951 * @param mixed $option Child Site wp_options table name.
952 * @param mixed $default_value default value.
953 *
954 * @return string|null Database query result (as string), or null on failure.
955 */
956 public function get_json_website_option( $website, $option, $default_value = null ) {
957 return $this->get_website_option( $website, $option, $default_value, true );
958 }
959
960 /**
961 * Get child site options.
962 *
963 * @param array $website Child site.
964 * @param mixed $options Child site options name.
965 *
966 * @return string|null Database query result (as string), or null on failure.
967 */
968 public function get_website_options_array( &$website, $options ) { // phpcs:ignore -- NOSONAR - complex.
969
970 if ( ! is_array( $options ) || empty( $options ) ) {
971 return array();
972 }
973
974 if ( is_array( $website ) ) {
975 $site_id = $website['id'];
976 } elseif ( is_object( $website ) ) {
977 $site_id = $website->id;
978 } elseif ( is_numeric( $website ) ) { // to support $site_id = 0 for global options.
979 $site_id = $website;
980 } else {
981 return array();
982 }
983
984 $arr_options = array();
985 $get_options = array();
986
987 foreach ( $options as $option ) {
988 if ( is_array( $website ) ) {
989 if ( isset( $website[ $option ] ) ) {
990 $arr_options[ $option ] = $website[ $option ];
991 } else {
992 $get_options[] = $option;
993 }
994 } elseif ( is_object( $website ) ) {
995 if ( property_exists( $website, $option ) ) {
996 $arr_options[ $option ] = $website->{$option};
997 } else {
998 $get_options[] = $option;
999 }
1000 } else {
1001 $get_options[] = $option;
1002 }
1003 }
1004
1005 if ( empty( $get_options ) ) {
1006 return $arr_options; // all options.
1007 }
1008
1009 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1010 $placeholders = implode( ',', array_fill( 0, count( $get_options ), '%s' ) );
1011 $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 ) ) );
1012
1013 $fill_options = array(
1014 'primary_lasttime_backup',
1015 );
1016
1017 foreach ( (array) $options_db as $o ) {
1018 $arr_options[ $o->name ] = $o->value;
1019 if ( in_array( $o->name, $fill_options ) ) {
1020 if ( is_array( $website ) ) {
1021 if ( ! isset( $website[ $o->name ] ) ) {
1022 $website[ $o->name ] = $o->value;
1023 }
1024 } elseif ( is_object( $website ) ) {
1025 if ( ! property_exists( $website, $o->name ) ) {
1026 $website->{$o->name} = $o->value;
1027 }
1028 }
1029 }
1030 }
1031 return $arr_options;
1032 }
1033
1034 /**
1035 * Update child site options.
1036 *
1037 * @param object $website Child site object.
1038 * @param mixed $option Option to update.
1039 * @param mixed $value Value to update with.
1040 */
1041 public function update_website_option( $website, $option, $value ) {
1042
1043 if ( is_numeric( $website ) ) {
1044 $site_id = intval( $website );
1045 } else {
1046 $site_id = $website->id;
1047 }
1048
1049 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1050 $rslt = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name FROM {$table_name} WHERE wpid = %d AND name = %s", $site_id, $option ) );
1051 if ( empty( $rslt ) ) {
1052 $this->wpdb->insert(
1053 $this->table_name( 'wp_options' ),
1054 array(
1055 'wpid' => $site_id,
1056 'name' => $option,
1057 'value' => $value,
1058 )
1059 );
1060 } else {
1061 $this->wpdb->update(
1062 $this->table_name( 'wp_options' ),
1063 array( 'value' => $value ),
1064 array(
1065 'wpid' => $site_id,
1066 'name' => $option,
1067 )
1068 );
1069 }
1070 }
1071
1072
1073 /**
1074 * Remove child site options.
1075 *
1076 * @param object $website Child site object.
1077 * @param mixed $options Option to update.
1078 */
1079 public function remove_website_option( $website, $options ) {
1080
1081 if ( empty( $options ) ) {
1082 return;
1083 }
1084
1085 if ( is_numeric( $website ) ) {
1086 $site_id = intval( $website );
1087 } else {
1088 $site_id = $website->id;
1089 }
1090
1091 if ( ! is_array( $options ) ) {
1092 $options = (array) $options;
1093 }
1094
1095 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1096 foreach ( $options as $opt ) {
1097 $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE wpid=%d AND name=%s", $site_id, $opt ) );
1098 }
1099 }
1100
1101
1102 /**
1103 * Get general Child site option.
1104 *
1105 * @param mixed $option Child Site option name.
1106 *
1107 * @return string|null Database query result (as string), or null on failure.
1108 */
1109 private function get_general_website_option( $option ) {
1110
1111 if ( null !== static::$general_options ) {
1112 if ( isset( static::$general_options[ $option ] ) ) {
1113 return static::$general_options[ $option ];
1114 }
1115 } else {
1116 static::$general_options[] = array();
1117 }
1118
1119 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1120 $val = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT value FROM {$table_name} WHERE wpid = %d AND name = %s", 0, $option ) );
1121
1122 static::$general_options[ $option ] = $val;
1123 return $val;
1124 }
1125
1126 /**
1127 * Get child site options.
1128 *
1129 * @param mixed $options Child site options name.
1130 *
1131 * @return string|null Database query result (as string), or null on failure.
1132 */
1133 public function get_general_options_array( $options ) {
1134
1135 if ( ! is_array( $options ) || empty( $options ) ) {
1136 return array();
1137 }
1138
1139 $return_options = array();
1140 if ( null !== static::$general_options ) {
1141 foreach ( static::$general_options as $opt => $val ) {
1142 if ( in_array( $opt, $options ) ) {
1143 $return_options[ $opt ] = $val;
1144 }
1145 }
1146 } else {
1147 static::$general_options[] = array();
1148 }
1149
1150 $diff_options = array();
1151 foreach ( $options as $opt ) {
1152 if ( ! isset( $return_options[ $opt ] ) ) {
1153 $diff_options[] = $opt;
1154 }
1155 }
1156
1157 if ( empty( $diff_options ) ) {
1158 return $return_options;
1159 }
1160
1161 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1162 $placeholders = implode( ',', array_fill( 0, count( $diff_options ), '%s' ) );
1163 $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 ) ) );
1164
1165 foreach ( (array) $options_db as $o ) {
1166 $return_options[ $o->name ] = $o->value;
1167 static::$general_options[ $o->name ] = $o->value;
1168 }
1169 return $return_options;
1170 }
1171
1172 /**
1173 * Update general site options.
1174 *
1175 * @param mixed $option Option to update.
1176 * @param mixed $value Value to update with.
1177 * @param string $type_value Type values: single|array.
1178 */
1179 public function update_general_option( $option, $value, $type_value = 'single' ) {
1180
1181 if ( 'array' === $type_value ) {
1182 if ( empty( $value ) ) {
1183 $value = array();
1184 } elseif ( ! is_array( $value ) ) {
1185 return false;
1186 }
1187 $value = wp_json_encode( $value );
1188 }
1189
1190 if ( null === static::$general_options ) {
1191 static::$general_options[] = array();
1192 }
1193 static::$general_options[ $option ] = $value;
1194
1195 $table_name = esc_sql( $this->table_name( 'wp_options' ) );
1196 $rslt = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT name FROM {$table_name} WHERE wpid = %d AND name = %s", 0, $option ) );
1197
1198 if ( empty( $rslt ) ) {
1199 $this->wpdb->insert(
1200 $this->table_name( 'wp_options' ),
1201 array(
1202 'wpid' => 0,
1203 'name' => $option,
1204 'value' => $value,
1205 )
1206 );
1207 } else {
1208 $this->wpdb->update(
1209 $this->table_name( 'wp_options' ),
1210 array( 'value' => $value ),
1211 array(
1212 'wpid' => 0,
1213 'name' => $option,
1214 )
1215 );
1216 }
1217 return true;
1218 }
1219
1220 /**
1221 * Get general Child site option.
1222 *
1223 * @param mixed $opt Child Site option name.
1224 * @param string $type_value Type values: single|array.
1225 *
1226 * @return string|null Database query result (as string), or null on failure.
1227 */
1228 public function get_general_option( $opt, $type_value = 'single' ) {
1229 if ( 'single' === $type_value ) {
1230 return $this->get_general_website_option( $opt );
1231 } elseif ( 'array' === $type_value ) {
1232 $json_value = $this->get_general_website_option( $opt );
1233 if ( empty( $json_value ) ) {
1234 return array();
1235 }
1236 return json_decode( $json_value, true );
1237 }
1238 return false;
1239 }
1240
1241 /**
1242 * Get child sites by user ID.
1243 *
1244 * @param int $userid User ID.
1245 * @param bool $selectgroups Selected groups.
1246 * @param null $search_site Site search field value.
1247 * @param string $orderBy Order list by. Default: URL.
1248 *
1249 * @return array|object|null Database query results or null on failer.
1250 */
1251 public function get_websites_by_user_id( $userid, $selectgroups = false, $search_site = null, $orderBy = 'wp.url' ) {
1252 return $this->get_results_result( $this->get_sql_websites_by_user_id( $userid, $selectgroups, $search_site, $orderBy ) );
1253 }
1254
1255 /**
1256 * Get child sites.
1257 *
1258 * @return string SQL string.
1259 */
1260 public function get_sql_websites() {
1261 $where = $this->get_sql_where_allow_access_sites( 'wp' );
1262
1263 $view_selects = '';
1264 $view_joins = '';
1265
1266 $opts_view = $this->get_wp_options_join();
1267
1268 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
1269 $view_selects = ',' . $opts_view['selects'];
1270 $view_joins = $opts_view['joins'];
1271 }
1272
1273 return 'SELECT wp.*,wp_sync.*' . $view_selects . '
1274 FROM ' . $this->table_name( 'wp' ) . ' wp
1275 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1276 ' . $view_joins . '
1277 WHERE 1 ' . $where . ' ORDER BY wp.id';
1278 }
1279
1280 /**
1281 * Get child sites by user id via SQL.
1282 *
1283 * @param int $userid Given user ID.
1284 * @param bool $selectgroups Selected groups. Default: false.
1285 * @param null $search_site Site search field value. Default: null.
1286 * @param string $orderBy Order list by. Default: URL.
1287 * @param bool $offset Query offset. Default: false.
1288 * @param bool $rowcount Row count. Default: falese.
1289 *
1290 * @return object|null Return database query or null on failure.
1291 *
1292 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
1293 */
1294 public function get_sql_websites_by_user_id( $userid, $selectgroups = false, $search_site = null, $orderBy = 'wp.url', $offset = false, $rowcount = false ) {
1295 if ( MainWP_Utility::ctype_digit( $userid ) ) {
1296 $where = '';
1297 if ( null !== $search_site ) {
1298 $search_site = trim( $search_site );
1299 $where = ' AND (wp.name LIKE "%' . $search_site . '%" OR wp.url LIKE "%' . $search_site . '%") ';
1300 }
1301
1302 $where .= $this->get_sql_where_allow_access_sites( 'wp' );
1303
1304 $view_selects = '';
1305 $view_joins = '';
1306
1307 $opts_view = $this->get_wp_options_join();
1308
1309 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
1310 $view_selects = ',' . $opts_view['selects'];
1311 $view_joins = $opts_view['joins'];
1312 }
1313
1314 if ( $selectgroups ) {
1315 $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
1316 FROM ' . $this->table_name( 'wp' ) . ' wp
1317 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1318 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1319 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1320 ' . $view_joins . '
1321 WHERE wp.userid = ' . $userid . "
1322 $where
1323 GROUP BY wp.id, wp_sync.sync_id
1324 ORDER BY " . $orderBy;
1325 } else {
1326 $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . '
1327 FROM ' . $this->table_name( 'wp' ) . ' wp
1328 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1329 ' . $view_joins . '
1330 WHERE wp.userid = ' . $userid . "
1331 $where
1332 ORDER BY " . $orderBy;
1333 }
1334
1335 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1336 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
1337 } elseif ( false !== $rowcount ) {
1338 $qry .= ' LIMIT ' . $rowcount;
1339 }
1340
1341 return $qry;
1342 }
1343
1344 return null;
1345 }
1346
1347 /**
1348 * Get SQL to get child sites for current user.
1349 *
1350 * @param bool $selectgroups Selected groups. Default: false.
1351 * @param null $search_site Site search field value. Default: null.
1352 * @param string $orderBy Order list by. Default: URL.
1353 * @param bool $offset Query offset. Default: false.
1354 * @param bool $rowcount Row count. Default: false.
1355 * @param null $extraWhere Extra WHERE. Default: null.
1356 * @param bool $for_manager For role manager. Default: false.
1357 * @param mixed $extra_view Extra view. Default favi_icon.
1358 * @param string $is_staging yes|no Is child site a staging site.
1359 * @param array $params other params.
1360 *
1361 * @return object|null Database query results or null on failure.
1362 *
1363 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
1364 */
1365 public function get_sql_websites_for_current_user( // phpcs:ignore -- NOSONAR - complex.
1366 $selectgroups = false,
1367 $search_site = null,
1368 $orderBy = 'wp.url',
1369 $offset = false,
1370 $rowcount = false,
1371 $extraWhere = null,
1372 $for_manager = false,
1373 $extra_view = array( 'favi_icon' ),
1374 $is_staging = 'no',
1375 $params = array()
1376 ) {
1377
1378 /**
1379 * The hook mainwp_get_sql_websites
1380 *
1381 * @since 5.5
1382 */
1383 $params = apply_filters( 'mainwp_get_sql_websites', $params, $selectgroups, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager, $extra_view, $is_staging );
1384
1385 $where = '';
1386 if ( MainWP_System::instance()->is_multi_user() ) {
1387
1388 /**
1389 * Current user global.
1390 *
1391 * @global string
1392 */
1393 global $current_user;
1394
1395 $where .= ' AND wp.userid = ' . $current_user->ID . ' ';
1396 }
1397
1398 if ( null !== $search_site ) {
1399 $search_site = trim( $search_site );
1400 // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety.
1401 $like_pattern = '%' . $this->wpdb->esc_like( $search_site ) . '%';
1402 $where .= $this->wpdb->prepare(
1403 ' AND (wp.name LIKE %s OR wp.url LIKE %s) ',
1404 $like_pattern,
1405 $like_pattern
1406 );
1407 }
1408
1409 if ( ! empty( $extraWhere ) ) {
1410 $where .= ' AND ' . $extraWhere . ' ';
1411 }
1412
1413 if ( ! $for_manager ) {
1414 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
1415 }
1416
1417 $connected_sql = '';
1418
1419 if ( is_array( $params ) && isset( $params['connected'] ) && 'yes' === $params['connected'] ) {
1420 $connected_sql = ' AND wp_sync.sync_errors = "" ';
1421 } elseif ( is_array( $params ) && isset( $params['connected'] ) && 'no' === $params['connected'] ) {
1422 $connected_sql = ' AND wp_sync.sync_errors <> "" ';
1423 }
1424
1425 $use_comp_subquery = false;
1426
1427 $limit = '';
1428 if ( $params && is_array( $params ) ) {
1429 $s = isset( $params['s'] ) ? $params['s'] : '';
1430 $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
1431 $include = isset( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array();
1432 $status = isset( $params['status'] ) ? wp_parse_list( $params['status'] ) : array();
1433 $page = isset( $params['page'] ) ? intval( $params['page'] ) : false;
1434 $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false;
1435
1436 // This parameter is used to enable caching in certain cases.
1437 $_included_cache_ids = isset( $params['_included_cache_ids'] ) ? wp_parse_id_list( $params['_included_cache_ids'] ) : array();
1438
1439 if ( ! empty( $s ) ) {
1440 $s = trim( $s );
1441 // Note: This SQL is executed via m_query() which bypasses wpdb, so we can't
1442 // use wpdb->prepare() (its placeholders won't be resolved). Instead, escape
1443 // LIKE wildcards and the value manually. First escape LIKE special chars,
1444 // then SQL escape the result.
1445 $like_value = '%' . $this->escape( $this->wpdb->esc_like( $s ) ) . '%';
1446 $where .= " AND ( wp.id LIKE '{$like_value}' OR wp.name LIKE '{$like_value}' OR wp.url LIKE '{$like_value}' ) ";
1447 }
1448
1449 if ( ! empty( $exclude ) ) {
1450 $where .= ' AND wp.id NOT IN (' . implode( ',', $exclude ) . ') ';
1451 }
1452
1453 if ( ! empty( $include ) ) {
1454 $where .= ' AND wp.id IN (' . implode( ',', $include ) . ') ';
1455 }
1456
1457 if ( ! empty( $_included_cache_ids ) ) {
1458 $where .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') ';
1459 }
1460
1461 // any, connected, disconnected, suspended, available_update.
1462 if ( ! empty( $status ) && is_array( $status ) && ! in_array( 'any', $status ) ) {
1463 $status_conds = array();
1464 if ( in_array( 'available_update', $status ) ) {
1465 $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 <> '[]' ) ";
1466 $options_table = esc_sql( $this->table_name( 'wp_options' ) );
1467 $results = $this->wpdb->get_results( "SELECT wpid FROM {$options_table} WHERE name = 'wp_upgrades' AND value <> '' AND value <> '[]'" );
1468 if ( $results ) {
1469 $wp_ids = array();
1470 foreach ( $results as $item ) {
1471 if ( ! empty( $item->wpid ) ) {
1472 $wp_ids[] = $item->wpid;
1473 }
1474 }
1475 $wp_ids = ! empty( $wp_ids ) ? array_unique( $wp_ids ) : array();
1476 if ( ! empty( $wp_ids ) ) {
1477 $available_sql .= ' OR wp.id IN ( ' . implode( ',', $wp_ids ) . ' )';
1478 }
1479 }
1480 $status_conds[] = ' ( ' . $available_sql . ') ';
1481 }
1482
1483 if ( in_array( 'connected', $status ) ) {
1484 $status_conds[] = ' ( wp_sync.sync_errors = "" ) ';
1485 }
1486 if ( in_array( 'disconnected', $status ) ) {
1487 $status_conds[] = " wp_sync.sync_errors <> '' ";
1488 }
1489
1490 if ( in_array( 'suspended', $status ) ) {
1491 $status_conds[] = ' wp.suspended = 1 ';
1492 }
1493
1494 if ( ! empty( $status_conds ) ) {
1495 $where .= ' AND ( ' . implode( ' OR ', $status_conds ) . ' ) ';
1496 }
1497
1498 if ( in_array( 'unsuspended', $status ) && ! in_array( 'suspended', $status ) ) { // to sure not conflict the suspended status.
1499 $where .= ' AND wp.suspended = 0 ';
1500 }
1501 }
1502
1503 if ( ! empty( $page ) && ! empty( $per_page ) ) {
1504 $limit = ( $page - 1 ) * $per_page . ',' . $per_page;
1505 }
1506 $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
1507 }
1508
1509 if ( 'wp.url' === $orderBy ) {
1510 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
1511 }
1512
1513 $view_selects = '';
1514 $view_joins = '';
1515
1516 if ( $use_comp_subquery ) {
1517 $view_selects = ',wp_optionview.* ';
1518 $view_joins = ' JOIN ' . $this->get_wp_options_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid ';
1519 } else {
1520 $opts_view = $this->get_wp_options_join( $extra_view );
1521
1522 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
1523 $view_selects = ',' . $opts_view['selects'];
1524 $view_joins = $opts_view['joins'];
1525 }
1526 }
1527
1528 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1529 if ( $selectgroups ) {
1530 $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,
1531 wpclient.name as client_name
1532 FROM ' . $this->table_name( 'wp' ) . ' wp
1533 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1534 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1535 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1536 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1537 ' . $view_joins . '
1538 WHERE 1 ' . $where . $connected_sql . '
1539 GROUP BY wp.id, wp_sync.sync_id
1540 ORDER BY ' . $orderBy;
1541 } else {
1542 $qry = 'SELECT wp.*,wp_sync.*' . $view_selects . ', wpclient.name as client_name
1543 FROM ' . $this->table_name( 'wp' ) . ' wp
1544 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1545 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1546 ' . $view_joins . '
1547 WHERE 1 ' . $where . $connected_sql . '
1548 GROUP BY wp.id, wp_sync.sync_id
1549 ORDER BY ' . $orderBy;
1550 }
1551
1552 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1553 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
1554 } elseif ( false !== $rowcount ) {
1555 $qry .= ' LIMIT ' . $rowcount;
1556 } elseif ( ! empty( $limit ) ) {
1557 $qry .= ' LIMIT ' . $limit;
1558 } else {
1559 // load all sites so check to support limit sites loading.
1560 $limit_sites = ! empty( $params['limit_sites'] ) ? intval( $params['limit_sites'] ) : 0;
1561 if ( ! empty( $limit_sites ) ) {
1562 $current_page = (int) get_option( 'mainwp_manage_updates_limit_current_page', 0 );
1563 $current_page = $current_page > 0 ? $current_page - 1 : 0;
1564 $start = $current_page * $limit_sites;
1565 $qry .= ' LIMIT ' . intval( $start ) . ', ' . intval( $limit_sites );
1566 }
1567 }
1568
1569 if ( ! empty( $_included_cache_ids ) ) {
1570 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql websites=%s]', $qry ) );
1571 }
1572 MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql websites=%s]', $qry ) );
1573
1574 return $qry;
1575 }
1576
1577
1578 /**
1579 * Get SQL to get wp child sites for current user.
1580 *
1581 * @since 4.3
1582 *
1583 * @param array $params params .
1584 *
1585 * @return object|null Database query results or null on failure.
1586 */
1587 public function get_sql_wp_for_current_user( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
1588 if ( ! is_array( $params ) ) {
1589 $params = array();
1590 }
1591
1592 $selectgroups = ! empty( $params['select_groups'] ) ? true : false;
1593 $search_site = isset( $params['search_site'] ) && ! empty( $params['search_site'] ) ? $params['search_site'] : null;
1594 $orderBy = isset( $params['order_by'] ) && ! empty( $params['order_by'] ) ? $params['order_by'] : 'wp.url';
1595 $offset = isset( $params['offset'] ) ? $params['offset'] : false;
1596 $rowcount = isset( $params['row_count'] ) ? $params['row_count'] : false;
1597 $for_manager = isset( $params['for_manager'] ) ? $params['for_manager'] : false;
1598 $extraWhere = isset( $params['extra_where'] ) && ! empty( $params['extra_where'] ) ? $params['extra_where'] : null;
1599 $extra_view = isset( $params['extra_view'] ) && is_array( $params['extra_view'] ) && ! empty( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
1600 $extra_join = isset( $params['extra_join'] ) ? $params['extra_join'] : '';
1601 $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
1602
1603 $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();
1604 $extra_select_sql_fields = isset( $params['extra_select_sql_fields'] ) && ! empty( $params['extra_select_sql_fields'] ) ? $params['extra_select_sql_fields'] : '';
1605
1606 $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no';
1607 $count_only = isset( $params['count_only'] ) && $params['count_only'] ? true : false;
1608
1609 $where = '';
1610
1611 if ( null !== $search_site ) {
1612 $search_site = trim( $search_site );
1613 // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety.
1614 $like_pattern = '%' . $this->wpdb->esc_like( $search_site ) . '%';
1615 $where .= $this->wpdb->prepare(
1616 ' AND (wp.name LIKE %s OR wp.url LIKE %s) ',
1617 $like_pattern,
1618 $like_pattern
1619 );
1620 }
1621
1622 if ( null !== $extraWhere ) {
1623 $where .= ' AND ' . $extraWhere;
1624 }
1625
1626 if ( ! $for_manager ) {
1627 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
1628 }
1629
1630 if ( 'wp.url' === $orderBy ) {
1631 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
1632 }
1633
1634 $select_wp_fields = $this->get_sql_select_wp_valid_fields( $extra_select_wp_fields );
1635
1636 if ( ! empty( $extra_select_sql_fields ) ) {
1637 $extra_select_sql_fields = ',' . $extra_select_sql_fields;
1638 }
1639
1640 $view_selects = '';
1641 $view_joins = '';
1642
1643 if ( $use_comp_subquery ) {
1644 $view_selects = ',wp_optionview.* ';
1645 $view_joins = ' JOIN ' . $this->get_wp_options_view( $extra_view ) . ' wp_optionview ON wp.id = wp_optionview.wpid ';
1646 } else {
1647 $opts_view = $this->get_wp_options_join( $extra_view );
1648 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
1649 $view_selects = ',' . $opts_view['selects'];
1650 $view_joins = $opts_view['joins'];
1651 }
1652 }
1653
1654 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
1655 if ( $selectgroups ) {
1656 if ( $count_only ) {
1657 $select = ' COUNT(DISTINCT(wp.id)) ';
1658 } else {
1659 $select = $select_wp_fields . '
1660 ' . $extra_select_sql_fields . '
1661 ,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 ';
1662 }
1663 $qry = 'SELECT ' . $select . '
1664 FROM ' . $this->table_name( 'wp' ) . ' wp
1665 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
1666 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
1667 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1668 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1669 ' . $view_joins . '
1670 ' . $extra_join . '
1671 WHERE 1 ' . $where;
1672 if ( ! $count_only ) {
1673 $qry .= ' GROUP BY wp.id, wp_sync.sync_id';
1674 }
1675 $qry .= ' ORDER BY ' . $orderBy;
1676 } else {
1677 if ( $count_only ) {
1678 $select = ' COUNT(DISTINCT(wp.id)) ';
1679 } else {
1680 $select = $select_wp_fields . '
1681 ' . $extra_select_sql_fields . '
1682 ,wp_sync.sync_errors' . $view_selects . ', wpclient.name as client_name ';
1683 }
1684 $qry = 'SELECT ' . $select . '
1685 FROM ' . $this->table_name( 'wp' ) . ' wp
1686 LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id
1687 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
1688 ' . $view_joins . '
1689 ' . $extra_join . '
1690 WHERE 1 ' . $where;
1691 if ( ! $count_only ) {
1692 $qry .= ' GROUP BY wp.id, wp_sync.sync_id';
1693 }
1694 $qry .= ' ORDER BY ' . $orderBy;
1695 }
1696
1697 if ( ! $count_only ) {
1698 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
1699 $qry .= ' LIMIT ' . intval( $offset ) . ', ' . intval( $rowcount );
1700 } elseif ( false !== $rowcount ) {
1701 $qry .= ' LIMIT ' . intval( $rowcount );
1702 }
1703 }
1704 return $qry;
1705 }
1706 /**
1707 * Get SQL select websites fields.
1708 *
1709 * @since 4.3
1710 *
1711 * @param array $other_fields extra select wp fields .
1712 *
1713 * @return string sql string.
1714 */
1715 public function get_sql_select_wp_valid_fields( $other_fields = array() ) {
1716
1717 $allow_other_fields = array(
1718 'offline_checks_last',
1719 'offline_check_result', // 1 - online, -1 offline.
1720 'http_response_code',
1721 'disable_health_check',
1722 'health_threshold',
1723 'note',
1724 'statsUpdate',
1725 'directories',
1726 'plugin_upgrades',
1727 'theme_upgrades',
1728 'translation_upgrades',
1729 'premium_upgrades',
1730 'securityIssues',
1731 'themes',
1732 'ignored_themes',
1733 'plugins',
1734 'ignored_plugins',
1735 'users',
1736 'categories',
1737 'pluginDir',
1738 'automatic_update',
1739 'backup_before_upgrade',
1740 'mainwpdir',
1741 'is_ignoreCoreUpdates',
1742 'is_ignorePluginUpdates',
1743 'is_ignoreThemeUpdates',
1744 'verify_certificate',
1745 'force_use_ipv4',
1746 'ssl_version',
1747 'http_user',
1748 'http_pass',
1749 'wpe',
1750 'is_staging',
1751 'client_id',
1752 );
1753
1754 $default_fields = array( 'id', 'url', 'name', 'adminname', 'verify_certificate', 'ssl_version', 'http_user', 'http_pass', 'suspended' );
1755
1756 $select = ' ';
1757
1758 foreach ( $default_fields as $field ) {
1759 $select .= 'wp.' . $this->escape( $field ) . ',';
1760 }
1761 foreach ( $other_fields as $field ) {
1762 if ( ! in_array( $field, $allow_other_fields ) ) {
1763 continue;
1764 }
1765 if ( in_array( $field, $default_fields ) ) {
1766 continue;
1767 }
1768 $select .= 'wp.' . $this->escape( $field ) . ',';
1769 }
1770 $select = rtrim( $select, ',' );
1771 return $select;
1772 }
1773
1774 /**
1775 * Get child sites for current user.
1776 *
1777 * @param array $params to get sites. Default: array().
1778 *
1779 * @return array Results or null on failure.
1780 *
1781 * @uses \MainWP\Dashboard\MainWP_Utility::map_site()
1782 */
1783 public function get_websites_for_current_user( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
1784 if ( ! is_array( $params ) ) {
1785 $params = array();
1786 }
1787
1788 $selectgroups = isset( $params['selectgroups'] ) ? $params['selectgroups'] : false;
1789 $search_site = isset( $params['search_site'] ) ? $params['search_site'] : null;
1790 $orderBy = isset( $params['order_by'] ) ? $params['order_by'] : 'wp.url';
1791 $offset = isset( $params['offset'] ) ? $params['offset'] : false;
1792 $rowcount = isset( $params['rowcount'] ) ? $params['rowcount'] : false;
1793 $extraWhere = isset( $params['where'] ) ? $params['where'] : null;
1794 $extra_view = isset( $params['extra_view'] ) && is_array( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
1795 $is_staging = isset( $params['is_staging'] ) ? $params['is_staging'] : 'no';
1796 $full_data = isset( $params['full_data'] ) && $params['full_data'] && ( 'no' !== $params['full_data'] ) ? true : false;
1797 $select_data = isset( $params['select_data'] ) && is_array( $params['select_data'] ) ? $params['select_data'] : false;
1798 $format = isset( $params['format'] ) ? $params['format'] : '';
1799 $clients = isset( $params['client'] ) ? $params['client'] : '';
1800 $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array();
1801
1802 $for_manager = isset( $params['no_perm_check'] ) && true === $params['no_perm_check'];
1803
1804 $urlsWhere = '';
1805
1806 if ( isset( $params['urls'] ) && ! empty( $params['urls'] ) ) {
1807 $urls = explode( ';', $params['urls'] );
1808 foreach ( $urls as $url ) {
1809 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
1810 if ( '/' !== substr( $url, - 1 ) ) {
1811 $url .= '/';
1812 }
1813 $urlsWhere .= '"' . $this->escape( $url ) . '", ';
1814 }
1815 $urlsWhere = rtrim( $urlsWhere, ', ' );
1816 }
1817
1818 if ( ! empty( $urlsWhere ) ) {
1819 $urlsWhere = " ( replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '') IN ( " . $urlsWhere . ') ) ';
1820
1821 if ( empty( $extraWhere ) ) {
1822 $extraWhere = $urlsWhere;
1823 } else {
1824 $extraWhere = $extraWhere . ' AND ' . $urlsWhere;
1825 }
1826 }
1827
1828 $clientWhere = '';
1829 if ( ! empty( $clients ) ) {
1830 $clients = explode( ';', $clients );
1831 foreach ( $clients as $client ) {
1832 if ( is_numeric( $client ) ) {
1833 $clientWhere .= intval( $client ) . ', ';
1834 }
1835 }
1836 $clientWhere = rtrim( $clientWhere, ', ' );
1837 }
1838
1839 if ( ! empty( $clientWhere ) ) {
1840 $clientWhere = ' ( wp.client_id IN ( ' . $clientWhere . ') ) ';
1841 if ( empty( $extraWhere ) ) {
1842 $extraWhere = $clientWhere;
1843 } else {
1844 $extraWhere = $extraWhere . ' AND ' . $clientWhere;
1845 }
1846 }
1847
1848 $args = array(
1849 's' => isset( $params['s'] ) ? $params['s'] : '',
1850 'exclude' => isset( $params['exclude'] ) && ! empty( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array(),
1851 'include' => isset( $params['include'] ) && ! empty( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array(),
1852 'status' => isset( $params['status'] ) && ! empty( $params['status'] ) ? wp_parse_list( $params['status'] ) : '',
1853 'page' => isset( $params['paged'] ) ? intval( $params['paged'] ) : false,
1854 'per_page' => isset( $params['items_per_page'] ) ? intval( $params['items_per_page'] ) : false,
1855 );
1856
1857 $data = array( 'id', 'url', 'name', 'client_id' );
1858
1859 if ( $full_data ) {
1860 $data = array(
1861 'id',
1862 'url',
1863 'name',
1864 'offline_checks_last',
1865 'offline_check_result', // 1 - online, -1 offline.
1866 'http_response_code',
1867 'disable_health_check',
1868 'health_threshold',
1869 'note',
1870 'dbsize',
1871 'plugin_upgrades',
1872 'theme_upgrades',
1873 'translation_upgrades',
1874 'securityIssues',
1875 'themes',
1876 'plugins',
1877 'automatic_update',
1878 'sync_errors',
1879 'dtsAutomaticSync',
1880 'dtsAutomaticSyncStart',
1881 'dtsSync',
1882 'dtsSyncStart',
1883 'last_post_gmt',
1884 'health_value',
1885 'phpversion',
1886 'wp_upgrades',
1887 'security_stats',
1888 'client_id',
1889 'adminname',
1890 'privkey',
1891 'http_user',
1892 'http_pass',
1893 'ssl_version',
1894 'signature_algo',
1895 'verify_method',
1896 'verify_certificate',
1897 'suspended',
1898 );
1899
1900 if ( ! in_array( 'security_stats', $extra_view ) ) {
1901 $extra_view[] = 'security_stats';
1902 }
1903 }
1904
1905 if ( ! empty( $select_data ) && is_array( $select_data ) ) {
1906 $data = $select_data;
1907 }
1908
1909 if ( $selectgroups ) {
1910 $data[] = 'wpgroups';
1911 $data[] = 'wpgroupids';
1912 }
1913
1914 if ( ! empty( $fields ) ) {
1915 $data = array_unique( array_merge( $fields, $data ) ); // to prevent difference fields name.
1916 }
1917
1918 $dbwebsites = array();
1919
1920 $sql = $this->get_sql_websites_for_current_user( $selectgroups, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager, $extra_view, $is_staging, $args );
1921 $websites = $this->query( $sql );
1922
1923 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
1924
1925 $obj_data = MainWP_Utility::map_site( $website, $data );
1926
1927 if ( $full_data ) {
1928 $sum_upgrades = 0;
1929 if ( '' !== $obj_data->plugin_upgrades ) {
1930 $plugin_upgrades = json_decode( $obj_data->plugin_upgrades, true );
1931 if ( is_array( $plugin_upgrades ) ) {
1932 $sum_upgrades += count( $plugin_upgrades );
1933 }
1934 }
1935
1936 if ( '' !== $obj_data->theme_upgrades ) {
1937 $theme_upgrades = json_decode( $obj_data->theme_upgrades, true );
1938 if ( is_array( $theme_upgrades ) ) {
1939 $sum_upgrades += count( $theme_upgrades );
1940 }
1941 }
1942
1943 if ( '' !== $obj_data->wp_upgrades ) {
1944 $wp_upgrades = json_decode( $obj_data->wp_upgrades, true );
1945 if ( is_array( $wp_upgrades ) ) {
1946 $sum_upgrades += count( $wp_upgrades );
1947 }
1948 }
1949 $obj_data->sum_of_upgrades = $sum_upgrades;
1950 }
1951
1952 if ( 'array' === $format ) {
1953 $dbwebsites[] = $obj_data;
1954 } else {
1955 $dbwebsites[ $website->id ] = $obj_data;
1956 }
1957 }
1958 static::free_result( $websites );
1959 return $dbwebsites;
1960 }
1961
1962 /**
1963 * Get count of child sites for the current user with filters.
1964 *
1965 * This method is optimized for the Abilities API to return site counts
1966 * with filtering support for status, tags (groups), and client_id.
1967 *
1968 * IMPORTANT: This method is intended ONLY for Abilities API consumers.
1969 * For legacy UI code paths (e.g., admin dashboard widgets, site count displays),
1970 * use get_websites_count() instead. The two methods serve different purposes:
1971 * - get_websites_count(): Simple total count for UI display (cached, no filters)
1972 * - get_websites_count_for_current_user(): Filtered count for API pagination
1973 *
1974 * @since 5.3
1975 *
1976 * @param array $params Filter parameters:
1977 * - status (string): 'connected', 'disconnected', 'suspended'
1978 * - tags (array): Array of tag/group IDs to filter by
1979 * - client_id (int): Client ID to filter by.
1980 *
1981 * @return int Count of sites matching the filters.
1982 */
1983 public function get_websites_count_for_current_user( $params = array() ) { //phpcs:ignore -- NOSONAR - complex.
1984
1985 if ( ! is_array( $params ) ) {
1986 $params = array();
1987 }
1988
1989 $status = isset( $params['status'] ) ? $params['status'] : '';
1990 $tags = isset( $params['tags'] ) && is_array( $params['tags'] ) ? $params['tags'] : array();
1991 $client_id = isset( $params['client_id'] ) ? intval( $params['client_id'] ) : 0;
1992 $s = isset( $params['s'] ) ? $params['s'] : '';
1993
1994 // Validate status value (defense-in-depth for direct callers outside Abilities API).
1995 $valid_statuses = array( 'connected', 'disconnected', 'suspended', '' );
1996 if ( ! in_array( $status, $valid_statuses, true ) ) {
1997 $status = '';
1998 }
1999
2000 $where = '';
2001 $sql_params = array();
2002
2003 // Multi-user support: filter by current user.
2004 if ( MainWP_System::instance()->is_multi_user() ) {
2005 global $current_user;
2006 $where .= ' AND wp.userid = %d ';
2007 $sql_params[] = (int) $current_user->ID;
2008 }
2009
2010 // Access control for sites.
2011 $where .= $this->get_sql_where_allow_access_sites( 'wp', 'no' );
2012
2013 // Status filtering: connected, disconnected, suspended.
2014 if ( ! empty( $status ) ) {
2015 switch ( $status ) {
2016 case 'connected':
2017 $where .= ' AND wp_sync.sync_errors = "" AND wp.suspended = 0 ';
2018 break;
2019 case 'disconnected':
2020 $where .= ' AND wp_sync.sync_errors <> "" ';
2021 break;
2022 case 'suspended':
2023 $where .= ' AND wp.suspended = 1 ';
2024 break;
2025 default:
2026 // No additional filtering.
2027 break;
2028 }
2029 }
2030
2031 // Client ID filtering.
2032 if ( ! empty( $client_id ) ) {
2033 $where .= ' AND wp.client_id = %d ';
2034 $sql_params[] = (int) $client_id;
2035 }
2036
2037 // Search filtering.
2038 if ( ! empty( $s ) ) {
2039 $s = trim( $s );
2040 $like_pattern = '%' . $this->wpdb->esc_like( $s ) . '%';
2041 $where .= $this->wpdb->prepare(
2042 ' AND ( wp.id LIKE %s OR wp.name LIKE %s OR wp.url LIKE %s ) ',
2043 $like_pattern,
2044 $like_pattern,
2045 $like_pattern
2046 );
2047 }
2048
2049 // Tags (groups) filtering.
2050 $join_group = '';
2051 if ( ! empty( $tags ) ) {
2052 $tags = array_map( 'intval', $tags );
2053 $tags = array_filter(
2054 $tags,
2055 function ( $id ) {
2056 return $id > 0;
2057 }
2058 );
2059 if ( ! empty( $tags ) ) {
2060 $join_group = ' JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2061 $placeholders = implode( ', ', array_fill( 0, count( $tags ), '%d' ) );
2062 $where .= " AND wpgroup.groupid IN ( $placeholders ) ";
2063 $sql_params = array_merge( $sql_params, $tags );
2064 }
2065 }
2066
2067 $qry = 'SELECT COUNT(DISTINCT wp.id) FROM ' . $this->table_name( 'wp' ) . ' wp ' .
2068 'JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid ' .
2069 $join_group .
2070 'WHERE 1 ' . $where;
2071
2072 // Only call prepare() when we have placeholders.
2073 $result = $sql_params
2074 ? $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.
2075 : $this->wpdb->get_var( $qry ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- We have already prepared the query with the parameters.
2076
2077 return (int) $result;
2078 }
2079
2080 /**
2081 * Get the child sites the current user has searched for.
2082 *
2083 * @param array $params Query parameters.
2084 *
2085 * @return boolean|null $qry Database query results or null on failure.
2086 *
2087 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
2088 */
2089 public function get_sql_search_websites_for_current_user( $params ) { // phpcs:ignore -- NOSONAR - complex.
2090
2091 if ( ! is_array( $params ) ) {
2092 $params = array();
2093 }
2094
2095 $view = isset( $params['view'] ) ? $params['view'] : 'default'; // must be default.
2096 $selectgroups = isset( $params['selectgroups'] ) && $params['selectgroups'] ? true : false;
2097 $search_site = isset( $params['search'] ) ? trim( $params['search'] ) : null;
2098 $orderBy = isset( $params['orderby'] ) ? $params['orderby'] : 'wp.url';
2099 $offset = isset( $params['offset'] ) ? intval( $params['offset'] ) : false;
2100 $rowcount = isset( $params['rowcount'] ) ? intval( $params['rowcount'] ) : false;
2101 $extraWhere = isset( $params['extra_where'] ) ? $params['extra_where'] : null; // without AND prefix.
2102 $for_manager = isset( $params['for_manager'] ) && $params['for_manager'] ? true : false;
2103 $extra_view = isset( $params['extra_view'] ) ? $params['extra_view'] : array( 'favi_icon' );
2104 $is_staging = isset( $params['is_staging'] ) && 'yes' === $params['is_staging'] ? 'yes' : 'no';
2105 $is_count = isset( $params['count_only'] ) && $params['count_only'] ? true : false;
2106 $group_ids = isset( $params['group_id'] ) && ! empty( $params['group_id'] ) ? $params['group_id'] : array();
2107 $client_ids = isset( $params['client_id'] ) && ! empty( $params['client_id'] ) ? $params['client_id'] : array();
2108 $group_logic = ( isset( $params['group_logic'] ) && 'and' === $params['group_logic'] ) ? 'and' : 'or';
2109 $is_not = isset( $params['isnot'] ) && ! empty( $params['isnot'] ) ? true : false;
2110 $selected_sites = isset( $params['selected_sites'] ) ? $params['selected_sites'] : array();
2111
2112 // This parameter is used to enable caching in certain cases.
2113 $_included_cache_ids = isset( $params['_included_cache_ids'] ) ? wp_parse_id_list( $params['_included_cache_ids'] ) : array();
2114 $where_cache_ids = '';
2115
2116 if ( ! is_array( $group_ids ) ) {
2117 $group_ids = array();
2118 }
2119
2120 // valid group ids.
2121 $group_ids = array_filter(
2122 $group_ids,
2123 function ( $e ) {
2124 if ( 'nogroups' === $e ) {
2125 return true;
2126 }
2127 return ( is_numeric( $e ) && 0 < $e ) ? true : false;
2128 }
2129 );
2130
2131 if ( ! is_array( $client_ids ) ) {
2132 $client_ids = array();
2133 }
2134
2135 // valid group ids.
2136 $client_ids = array_filter(
2137 $client_ids,
2138 function ( $e ) {
2139 if ( 'noclients' === $e ) {
2140 return true;
2141 }
2142 return is_numeric( $e ) && ! empty( $e ) ? true : false; // to valid client ids.
2143 }
2144 );
2145
2146 if ( $selectgroups ) {
2147 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
2148 if ( $staging_group && in_array( $staging_group, $group_ids ) ) {
2149 if ( empty( $group_ids ) ) {
2150 $is_staging = 'yes';
2151 } else {
2152 $is_staging = 'nocheckstaging';
2153 }
2154 }
2155 }
2156
2157 if ( ! is_array( $selected_sites ) ) {
2158 $selected_sites = array();
2159 }
2160 $selected_sites = MainWP_Utility::array_numeric_filter( $selected_sites );
2161
2162 $where = '';
2163 if ( MainWP_System::instance()->is_multi_user() ) {
2164
2165 /**
2166 * Current user global.
2167 *
2168 * @global string
2169 */
2170 global $current_user;
2171
2172 $where .= ' AND wp.userid = ' . $current_user->ID . ' ';
2173 }
2174
2175 if ( ! empty( $_included_cache_ids ) ) {
2176 $where_cache_ids .= ' AND wp.id IN (' . implode( ',', $_included_cache_ids ) . ') ';
2177 } else {
2178 if ( ! empty( $selected_sites ) ) {
2179 $where .= ' AND wp.id IN (' . implode( ',', $selected_sites ) . ') ';
2180 }
2181
2182 if ( ! empty( $extraWhere ) ) {
2183 $where .= ' AND ' . $extraWhere;
2184 }
2185 }
2186
2187 // Search filtering.
2188 if ( null !== $search_site && '' !== $search_site ) {
2189 // Escape LIKE wildcards first (%, _, \).
2190 // Note: Cannot use WordPress escaping functions (esc_like, esc_sql, prepare) because
2191 // WordPress 6.2+ creates placeholder tokens that are only substituted during query execution.
2192 // Since this method returns a SQL string for later execution, tokens would never be substituted.
2193 // We use direct mysqli_real_escape_string() which is the same underlying function WordPress uses.
2194 $search_escaped = str_replace( array( '\\', '%', '_' ), array( '\\\\', '\\%', '\\_' ), $search_site );
2195 $like_pattern = '%' . $search_escaped . '%';
2196
2197 // SQL escape using direct mysqli function to bypass WordPress placeholder tokens.
2198 if ( $this->wpdb->dbh instanceof \mysqli ) {
2199 $like_pattern_escaped = mysqli_real_escape_string( $this->wpdb->dbh, $like_pattern );
2200 } else {
2201 // Fallback: reject if no mysqli connection available.
2202 $like_pattern_escaped = '';
2203 }
2204
2205 if ( '' !== $like_pattern_escaped ) {
2206 $where .= " AND (wp.name LIKE '" . $like_pattern_escaped . "' OR wp.url LIKE '" . $like_pattern_escaped . "') ";
2207 }
2208 }
2209
2210 $staging_enabled = is_plugin_active( 'mainwp-staging-extension/mainwp-staging-extension.php' ) || is_plugin_active( 'mainwp-timecapsule-extension/mainwp-timecapsule-extension.php' );
2211 if ( ! $staging_enabled ) {
2212 $is_staging = 'no';
2213 }
2214
2215 if ( ! $for_manager ) {
2216 $where .= $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
2217 }
2218
2219 if ( $is_count ) {
2220 $orderBy = '';
2221 } elseif ( 'wp.url' === $orderBy ) {
2222 $orderBy = "replace(replace(replace(replace(replace(wp.url, 'https://www.',''), 'http://www.',''), 'https://', ''), 'http://', ''), 'www.', '')";
2223 }
2224
2225 if ( ! empty( $orderBy ) ) {
2226 $orderBy = ' ORDER BY ' . $orderBy;
2227 }
2228
2229 $join_group = '';
2230 $where_group = '';
2231 $having_group = '';
2232
2233 if ( in_array( 'nogroups', $group_ids ) ) {
2234 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2235 $group_ids = array_filter(
2236 $group_ids,
2237 function ( $e ) {
2238 return 'nogroups' !== $e;
2239 }
2240 );
2241 if ( ! empty( $group_ids ) ) {
2242 $groups = implode( ',', $group_ids );
2243 $groups_count = count( $group_ids );
2244 if ( $is_not ) {
2245 $where_group = ' AND wpgroup.groupid IS NOT NULL ';
2246 if ( 'and' === $group_logic ) {
2247 $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 . ' ';
2248 $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_match_all . ' ) ';
2249 } else {
2250 $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 . ') ';
2251 $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_is_not . ' ) ';
2252 }
2253 } elseif ( 'and' === $group_logic ) {
2254 $where_group = ' AND 1 = 0 ';
2255 } else {
2256 $where_group = ' AND ( wpgroup.groupid IS NULL OR wpgroup.groupid IN (' . $groups . ') ) ';
2257 }
2258 } elseif ( $is_not ) {
2259 $where_group = ' AND wpgroup.groupid IS NOT NULL ';
2260 } else {
2261 $where_group = ' AND wpgroup.groupid IS NULL ';
2262 }
2263 } elseif ( $group_ids ) {
2264 $groups = implode( ',', $group_ids );
2265 $groups_count = count( $group_ids );
2266 if ( $is_not ) {
2267 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2268 $where_group = '';
2269 if ( 'and' === $group_logic ) {
2270 $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 . ' ';
2271 $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_match_all . ' ) ';
2272 } else {
2273 $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 . ') ';
2274 $where_group .= ' AND wp.id NOT IN ( ' . $sub_select_is_not . ' ) ';
2275 }
2276 } else {
2277 $join_group = ' JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2278 $where_group = ' AND wpgroup.groupid IN (' . $groups . ') ';
2279 if ( 'and' === $group_logic ) {
2280 $having_group = 'COUNT(DISTINCT wpgroup.groupid) = ' . $groups_count;
2281 }
2282 }
2283 }
2284
2285 $select_groups_belong = '';
2286
2287 if ( ! $is_count && $group_ids ) {
2288 $select_groups_belong = $this->get_select_groups_belong();
2289 }
2290
2291 $join_client = '';
2292 $where_client = '';
2293 $group_by = ' GROUP BY wp.id, wp_sync.sync_id';
2294 if ( ! empty( $having_group ) ) {
2295 $group_by .= ' HAVING ' . $having_group;
2296 }
2297 $group_by = ' GROUP BY wp.id, wp_sync.sync_id';
2298 if ( ! empty( $having_group ) ) {
2299 $group_by .= ' HAVING ' . $having_group;
2300 }
2301 if ( in_array( 'noclients', $client_ids ) ) {
2302 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2303 $client_ids = array_filter(
2304 $client_ids,
2305 function ( $e ) {
2306 return 'noclients' !== $e;
2307 }
2308 );
2309 if ( ! empty( $client_ids ) ) {
2310 $clients = implode( ',', $client_ids );
2311 if ( $is_not ) {
2312 $where_client = ' AND wpclient.client_id IS NOT NULL AND wp.client_id NOT IN (' . $clients . ') ';
2313 } else {
2314 $where_client = ' AND wpclient.client_id IN (' . $clients . ') ';
2315 }
2316 } elseif ( $is_not ) {
2317 $where_client = ' AND wpclient.client_id IS NOT NULL ';
2318 } else {
2319 $where_client = ' AND wpclient.client_id IS NULL ';
2320 }
2321 } elseif ( $client_ids && ! empty( $client_ids ) ) {
2322 $clients = implode( ',', $client_ids );
2323 if ( $is_not ) {
2324 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2325 $where_client = ' AND ( wpclient.client_id NOT IN (' . $clients . ') OR wpclient.client_id IS NULL ) ';
2326 } else {
2327 $join_client = ' JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2328 $where_client = ' AND wpclient.client_id IN (' . $clients . ') ';
2329 }
2330 }
2331
2332 if ( '' === $join_client ) {
2333 $join_client = ' LEFT JOIN ' . $this->table_name( 'wp_clients' ) . ' wpclient ON wp.client_id = wpclient.client_id ';
2334 }
2335
2336 $light_fields = array(
2337 'wp.id',
2338 'wp.url',
2339 'wp.name',
2340 'wp.client_id',
2341 'wp.verify_certificate',
2342 'wp.http_user',
2343 'wp.http_pass',
2344 'wp.ssl_version',
2345 'wp.adminname',
2346 'wp.privkey',
2347 'wp.pubkey',
2348 'wp.wpe',
2349 'wp.is_staging',
2350 'wp.force_use_ipv4',
2351 'wp.siteurl',
2352 'wp.suspended',
2353 'wp.mainwpdir',
2354 'wp.is_ignoreCoreUpdates',
2355 'wp.is_ignorePluginUpdates',
2356 'wp.is_ignoreThemeUpdates',
2357 'wp.backup_before_upgrade',
2358 'wp.userid',
2359 'wp_sync.sync_errors',
2360 );
2361
2362 $legacy_status_fields = array(
2363 'wp.offline_check_result', // 1 - online, -1 offline.
2364 'wp.http_response_code',
2365 'wp.offline_checks_last',
2366 );
2367
2368 $light_fields = array_merge( $light_fields, $legacy_status_fields );
2369
2370 $join_monitors = '';
2371
2372 $select_fields = array(
2373 'wp.*',
2374 'wp_sync.*',
2375 );
2376
2377 if ( 'light_view' === $view ) {
2378 $select_fields = $light_fields;
2379 } elseif ( 'monitor_view' === $view ) {
2380 $select_fields = $light_fields;
2381 $select_fields[] = 'mo.*';
2382 $join_monitors = 'LEFT JOIN (
2383 SELECT m1.*
2384 FROM ' . $this->table_name( 'monitors' ) . ' m1
2385 JOIN (
2386 SELECT wpid, MAX(monitor_id) AS max_id
2387 FROM ' . $this->table_name( 'monitors' ) . '
2388 WHERE issub = 0
2389 GROUP BY wpid
2390 ) mm ON mm.wpid = m1.wpid AND m1.monitor_id = mm.max_id
2391 ) mo ON mo.wpid = wp.id ';
2392 } elseif ( 'manage_site' === $view ) {
2393 $select_fields[] = 'mo.monitor_id';
2394 $join_monitors = ' LEFT JOIN (
2395 SELECT wpid, MAX(monitor_id) AS monitor_id
2396 FROM ' . $this->table_name( 'monitors' ) . '
2397 WHERE issub = 0
2398 GROUP BY wpid
2399 ) AS mo
2400 ON mo.wpid = wp.id ';
2401
2402 }
2403
2404 $select = implode( ',', $select_fields );
2405
2406 $view_selects = '';
2407 $view_joins = '';
2408
2409 $opts_view = $this->get_wp_options_join( $extra_view, $view );
2410
2411 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
2412 $view_selects = ',' . $opts_view['selects'];
2413 $view_joins = $opts_view['joins'];
2414 }
2415
2416 // wpgroups to fix issue for mysql 8.0, as groups will generate error syntax.
2417 if ( $selectgroups ) {
2418
2419 if ( empty( $join_group ) ) {
2420 $join_group = ' LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid ';
2421 }
2422
2423 $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 ' .
2424 $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' .
2425 $join_client . ' ' .
2426 $join_group .
2427 $join_monitors . '
2428 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgroup.groupid = gr.id
2429
2430 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2431 ' . $view_joins . '
2432 WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by .
2433 $orderBy;
2434 } else {
2435 $qry = 'SELECT ' . $select . $view_selects . ', wpclient.name as client_name ' .
2436 $select_groups_belong . ' FROM ' . $this->table_name( 'wp' ) . ' wp ' .
2437 $join_group . ' ' .
2438 $join_client .
2439 $join_monitors . '
2440 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2441 ' . $view_joins . '
2442 WHERE 1 ' . $where_cache_ids . $where . $where_group . $where_client . $group_by .
2443 $orderBy;
2444 }
2445
2446 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
2447 // When cache IDs are provided, they already represent the page-specific subset,
2448 // so the effective offset within that subset is always 0.
2449 $effective_offset = ! empty( $_included_cache_ids ) ? 0 : $offset;
2450 $qry .= ' LIMIT ' . $effective_offset . ', ' . $rowcount;
2451 } elseif ( false !== $rowcount ) {
2452 $qry .= ' LIMIT ' . $rowcount;
2453 }
2454
2455 if ( ! empty( $_included_cache_ids ) ) {
2456 MainWP_Logger::instance()->log_events( 'cache-metrics', sprintf( '[sql search websites=%s]', $qry ) );
2457 }
2458 MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[sql search websites=%s]', $qry ) );
2459
2460 return $qry;
2461 }
2462
2463 /**
2464 * Get child sites where allowed access via SQL.
2465 *
2466 * @param string $site_table_alias Child site table alias.
2467 * @param string $is_staging yes|no Is child site a staging site.
2468 *
2469 * @return boolean|null $_where Database query results or null on failure.
2470 */
2471 public function get_sql_where_allow_access_sites( $site_table_alias = '', $is_staging = 'no' ) { // phpcs:ignore -- NOSONAR - complex.
2472
2473 if ( empty( $site_table_alias ) ) {
2474 $site_table_alias = $this->table_name( 'wp' );
2475 }
2476
2477 // check to filter the staging sites.
2478 $where_staging = ' AND ' . $site_table_alias . '.is_staging = 0 ';
2479 if ( 'no' === $is_staging ) {
2480 $where_staging = ' AND ' . $site_table_alias . '.is_staging = 0 ';
2481 } elseif ( 'yes' === $is_staging ) {
2482 $where_staging = ' AND ' . $site_table_alias . '.is_staging = 1 ';
2483 } elseif ( 'nocheckstaging' === $is_staging ) {
2484 $where_staging = '';
2485 }
2486 // end staging filter.
2487
2488 $_where = $where_staging;
2489 // To fix bug run from cron job.
2490 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
2491 return $_where;
2492 }
2493
2494 // To fix bug run from wp cli.
2495 if ( defined( 'WP_CLI' ) && WP_CLI ) {
2496 return $_where;
2497 }
2498
2499 // Run from Rest Api.
2500 if ( defined( 'MAINWP_REST_API_DOING' ) && MAINWP_REST_API_DOING ) {
2501 return $_where;
2502 }
2503
2504 /**
2505 * Filter: mainwp_currentuserallowedaccesssites
2506 *
2507 * Filters allowed sites for the current user.
2508 *
2509 * @since Unknown
2510 */
2511 $allowed_sites = apply_filters( 'mainwp_currentuserallowedaccesssites', 'all' );
2512
2513 if ( 'all' === $allowed_sites ) {
2514 return $_where;
2515 }
2516
2517 if ( is_array( $allowed_sites ) && ! empty( $allowed_sites ) ) {
2518 // valid group ids.
2519 $allowed_sites = array_filter(
2520 $allowed_sites,
2521 function ( $e ) {
2522 return is_numeric( $e ) ? true : false;
2523 }
2524 );
2525 $_where .= ' AND ' . $site_table_alias . '.id IN (' . implode( ',', $allowed_sites ) . ') ';
2526 } else {
2527 $_where .= ' AND 0 ';
2528 }
2529
2530 return $_where;
2531 }
2532
2533 /**
2534 * Get groupd where allowed access via SQL.
2535 *
2536 * @param string $group_table_alias Child site table alias.
2537 * @param string $with_staging yes|no Is child site a staging site.
2538 *
2539 * @return boolean|null $_where Database query results or null on failer.
2540 */
2541 public function get_sql_where_allow_groups( $group_table_alias = '', $with_staging = 'no' ) { // phpcs:ignore -- NOSONAR - complex.
2542
2543 if ( empty( $group_table_alias ) ) {
2544 $group_table_alias = $this->table_name( 'group' );
2545 }
2546
2547 // check to filter the staging group.
2548 $where_staging_group = '';
2549 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
2550 if ( $staging_group ) {
2551 $where_staging_group = ' AND ' . $group_table_alias . '.id <> ' . $staging_group . ' ';
2552 if ( 'yes' === $with_staging ) {
2553 $where_staging_group = '';
2554 }
2555 }
2556
2557 // end staging filter.
2558 $_where = $where_staging_group;
2559
2560 // To fix bug run from cron job.
2561 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
2562 return $_where;
2563 }
2564
2565 // Run from wp cli.
2566 if ( defined( 'WP_CLI' ) && WP_CLI ) {
2567 return $_where;
2568 }
2569
2570 // Run from Rest Api.
2571 if ( defined( 'MAINWP_REST_API_DOING' ) && MAINWP_REST_API_DOING ) {
2572 return $_where;
2573 }
2574
2575 /**
2576 * Filter: mainwp_currentuserallowedaccessgroups
2577 *
2578 * Filters allowed groups for the current user.
2579 *
2580 * @since Unknown
2581 */
2582 $allowed_groups = apply_filters( 'mainwp_currentuserallowedaccessgroups', 'all' );
2583
2584 if ( 'all' === $allowed_groups ) {
2585 return $_where;
2586 }
2587
2588 if ( is_array( $allowed_groups ) && ! empty( $allowed_groups ) ) {
2589
2590 // valid group ids.
2591 $allowed_groups = array_filter(
2592 $allowed_groups,
2593 function ( $e ) {
2594 return is_numeric( $e ) ? true : false;
2595 }
2596 );
2597
2598 return ' AND ' . $group_table_alias . '.id IN (' . implode( ',', $allowed_groups ) . ') ' . $_where;
2599 } else {
2600 return ' AND 0 ';
2601 }
2602 }
2603
2604
2605 /**
2606 * Get child site by id and params.
2607 *
2608 * @param int $id Child site ID.
2609 * @param array $params params.
2610 * @param string $obj OBJECT|ARRAY_A.
2611 *
2612 * @return object|null Database query results or null on failure.
2613 */
2614 public function get_website_by_id_params( $id, $params = array(), $obj = OBJECT ) {
2615 return $this->get_row_result( $this->get_sql_website_by_params( $id, $params ), $obj );
2616 }
2617
2618 /**
2619 * Get sql child site by id and params.
2620 *
2621 * @param int $id Child site ID.
2622 * @param array $params params.
2623 *
2624 * @return object|null Database query results or null on failure.
2625 */
2626 public function get_sql_website_by_params( $id, $params = array() ) {
2627
2628 if ( ! is_array( $params ) ) {
2629 $params = array();
2630 }
2631
2632 $select_groups = ! empty( $params['select_groups'] ) ? true : false;
2633
2634 $view = ! empty( $params['view'] ) ? $params['view'] : 'simple_view';
2635 $view_fields = isset( $params['view_fields'] ) ? $params['view_fields'] : array();
2636
2637 if ( is_string( $view_fields ) ) {
2638 $view_fields = (array) $view_fields;
2639 } elseif ( ! is_array( $view_fields ) ) {
2640 $view_fields = array();
2641 }
2642
2643 if ( MainWP_Utility::ctype_digit( $id ) ) {
2644
2645 $use_comp_subquery = ! empty( $params['use_compatible_subquery'] ) ? true : false;
2646
2647 $view_selects = '';
2648 $view_joins = '';
2649
2650 if ( $use_comp_subquery ) {
2651 $view_selects = ',wp_optionview.* ';
2652 $view_joins = ' JOIN ' . $this->get_option_view_by( $view, $view_fields ) . ' wp_optionview ON wp.id = wp_optionview.wpid ';
2653 } else {
2654 $opts_view = $this->get_option_view_by_join( $view, $view_fields );
2655 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
2656 $view_selects = ',' . $opts_view['selects'];
2657 $view_joins = $opts_view['joins'];
2658 }
2659 }
2660
2661 $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' );
2662 if ( $select_groups ) {
2663 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
2664 FROM ' . $this->table_name( 'wp' ) . ' wp
2665 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
2666 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
2667 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2668 ' . $view_joins . '
2669 WHERE wp.id = ' . $id . $where . '
2670 GROUP BY wp.id, wp_sync.sync_id';
2671 }
2672
2673 return 'SELECT wp.*,wp_sync.*' . $view_selects . '
2674 FROM ' . $this->table_name( 'wp' ) . ' wp
2675 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2676 ' . $view_joins . '
2677 WHERE id = ' . $id . $where;
2678 }
2679 return null;
2680 }
2681
2682 /**
2683 * Get child site by id.
2684 *
2685 * @param int $id Child site ID.
2686 * @param array $selectGroups Select groups.
2687 * @param array $extra_view Get extra option fields.
2688 * @param int $obj OBJECT|ARRAY_A.
2689 *
2690 * @return object|null Database query results or null on failure.
2691 */
2692 public function get_website_by_id( $id, $selectGroups = false, $extra_view = array(), $obj = OBJECT ) {
2693 return $this->get_row_result( $this->get_sql_website_by_id( $id, $selectGroups, $extra_view ), $obj );
2694 }
2695
2696 /**
2697 * Get child site by id via SQL.
2698 *
2699 * @param int $id Child site ID.
2700 * @param bool $selectGroups Selected groups.
2701 * @param mixed $extra_view Extra view value.
2702 *
2703 * @return object|null Database query result or null on failure.
2704 *
2705 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
2706 */
2707 public function get_sql_website_by_id( $id, $selectGroups = false, $extra_view = array() ) {
2708
2709 if ( ! is_array( $extra_view ) || empty( $extra_view ) ) {
2710 $extra_view = array( 'favi_icon', 'site_info' );
2711 }
2712
2713 if ( MainWP_Utility::ctype_digit( $id ) ) {
2714
2715 $view_selects = '';
2716 $view_joins = '';
2717
2718 $opts_view = $this->get_wp_options_join( $extra_view );
2719
2720 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
2721 $view_selects = ',' . $opts_view['selects'];
2722 $view_joins = $opts_view['joins'];
2723 }
2724
2725 $where = $this->get_sql_where_allow_access_sites( 'wp', 'nocheckstaging' );
2726 if ( $selectGroups ) {
2727 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
2728 FROM ' . $this->table_name( 'wp' ) . ' wp
2729 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
2730 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
2731 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2732 ' . $view_joins . '
2733 WHERE wp.id = ' . $id . $where . '
2734 GROUP BY wp.id, wp_sync.sync_id';
2735 }
2736
2737 return 'SELECT wp.*,wp_sync.*' . $view_selects . '
2738 FROM ' . $this->table_name( 'wp' ) . ' wp
2739 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2740 ' . $view_joins . '
2741 WHERE id = ' . $id . $where;
2742 }
2743
2744 return null;
2745 }
2746
2747 /**
2748 * Method get_websites_by_ids()
2749 *
2750 * Get child sites by child site IDs.
2751 *
2752 * @param array $ids Child site IDs.
2753 * @param int $userId User ID.
2754 *
2755 * @return object|null Database query result or null on failure.
2756 *
2757 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
2758 */
2759 public function get_websites_by_ids( $ids, $userId = null ) {
2760 if ( ( null === $userId ) && MainWP_System::instance()->is_multi_user() ) {
2761
2762 /**
2763 * Current user global.
2764 *
2765 * @global string
2766 */
2767 global $current_user;
2768
2769 $userId = $current_user->ID;
2770 }
2771
2772 // valid group ids.
2773 $ids = array_filter(
2774 $ids,
2775 function ( $e ) {
2776 return ( is_numeric( $e ) && 0 < $e ) ? true : false;
2777 }
2778 );
2779
2780 $where = $this->get_sql_where_allow_access_sites();
2781 $table_name = esc_sql( $this->table_name( 'wp' ) );
2782 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
2783 $sql = "SELECT * FROM {$table_name} WHERE id IN ({$placeholders})";
2784 $params = $ids;
2785
2786 if ( null !== $userId ) {
2787 $sql .= ' AND userid = %d';
2788 $params[] = intval( $userId );
2789 }
2790
2791 $sql .= ' ' . $where;
2792
2793 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
2794 }
2795
2796 /**
2797 * Get child sites by groups IDs.
2798 *
2799 * @param array $ids Groups IDs.
2800 * @param int $userId User ID.
2801 * @param array $fields array fields .
2802 *
2803 * @return object|null Database query result or null on failure.
2804 *
2805 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
2806 */
2807 public function get_websites_by_group_ids( $ids, $userId = null, $fields = array() ) {
2808 if ( empty( $ids ) ) {
2809 return array();
2810 }
2811 if ( ( null === $userId ) && MainWP_System::instance()->is_multi_user() ) {
2812
2813 /**
2814 * Current user global.
2815 *
2816 * @global string
2817 */
2818 global $current_user;
2819
2820 $userId = $current_user->ID;
2821 }
2822
2823 // valid group ids.
2824 $group_ids = array_filter(
2825 $ids,
2826 function ( $e ) {
2827 return is_numeric( $e ) ? true : false;
2828 }
2829 );
2830
2831 $select = '*';
2832 if ( ! empty( $fields ) && is_array( $fields ) ) {
2833 $fields = array_filter( array_map( 'trim', $fields ) );
2834 if ( $fields ) {
2835 $select = '';
2836 foreach ( $fields as $field ) {
2837 $select .= $this->escape( $field ) . ',';
2838 }
2839 $select = rtrim( $select, ',' );
2840 }
2841 }
2842 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 );
2843 }
2844
2845 /**
2846 * Get child sites by group ID.
2847 *
2848 * @param int $id Group ID.
2849 * @param bool $selectgroups Selected groups. Default: false.
2850 * @param string $orderBy Order list by. Default: URL.
2851 * @param bool $offset Query offset. Default: false.
2852 * @param bool $rowcount Row count. Default: falese.
2853 * @param null $where SQL WHERE value.
2854 * @param null $search_site Site search field value. Default: null.
2855 * @param array $others Others params.
2856 *
2857 * @return object|null Database query result or null on failure.
2858 */
2859 public function get_websites_by_group_id( //phpcs:ignore -- NOSONAR -ok.
2860 $id,
2861 $selectgroups = false,
2862 $orderBy = 'wp.url',
2863 $offset = false,
2864 $rowcount = false,
2865 $where = null,
2866 $search_site = null,
2867 $others = array()
2868 ) {
2869 return $this->get_results_result(
2870 $this->get_sql_websites_by_group_id(
2871 $id,
2872 $selectgroups,
2873 $orderBy,
2874 $offset,
2875 $rowcount,
2876 $where,
2877 $search_site,
2878 $others
2879 )
2880 );
2881 }
2882
2883 /**
2884 * Get count of child sites by group ID.
2885 *
2886 * Uses an efficient COUNT query instead of fetching all rows.
2887 *
2888 * @param int $id Group ID.
2889 *
2890 * @return int Number of sites in the group.
2891 *
2892 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
2893 */
2894 public function get_websites_count_by_group_id( $id ) {
2895 if ( ! MainWP_Utility::ctype_digit( $id ) ) {
2896 return 0;
2897 }
2898
2899 // Determine if this is the staging group.
2900 $is_staging = 'no';
2901 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
2902 if ( $staging_group && (int) $id === (int) $staging_group ) {
2903 $is_staging = 'yes';
2904 }
2905
2906 $where_allowed = $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
2907
2908 // Use prepare() for the groupid parameter.
2909 $qry = $this->wpdb->prepare(
2910 'SELECT COUNT(DISTINCT wp.id) FROM ' . $this->table_name( 'wp' ) . ' wp
2911 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
2912 WHERE wpgroup.groupid = %d',
2913 $id
2914 ) . $where_allowed;
2915
2916 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.
2917 }
2918
2919 /**
2920 * Get child sites by group id via SQL.
2921 *
2922 * @param int $id Group ID.
2923 * @param bool $selectgroups Selected groups. Default: false.
2924 * @param string $orderBy Order list by. Default: URL.
2925 * @param bool $offset Query offset. Default: false.
2926 * @param bool $rowcount Row count. Default: falese.
2927 * @param null $where SQL WHERE value.
2928 * @param null $search_site Site search field value. Default: null.
2929 * @param array $others Others params.
2930 *
2931 * @return object|null Return database query or null on failure.
2932 *
2933 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
2934 */
2935 public function get_sql_websites_by_group_id( // phpcs:ignore -- NOSONAR - complex.
2936 $id,
2937 $selectgroups = false,
2938 $orderBy = 'wp.url',
2939 $offset = false,
2940 $rowcount = false,
2941 $where = null,
2942 $search_site = null,
2943 $others = array()
2944 ) {
2945
2946 $is_staging = 'no';
2947 if ( $selectgroups ) {
2948 $staging_group = get_option( 'mainwp_stagingsites_group_id' );
2949 if ( $staging_group && $id === $staging_group ) {
2950 $is_staging = 'yes';
2951 }
2952 }
2953
2954 $where_search = '';
2955 if ( ! empty( $search_site ) ) {
2956 $search_site = trim( $search_site );
2957 // Use esc_like() to escape LIKE wildcards (%, _) then prepare() for SQL safety.
2958 $like_pattern = '%' . $this->wpdb->esc_like( $search_site ) . '%';
2959 $where_search .= $this->wpdb->prepare(
2960 ' AND (wp.name LIKE %s OR wp.url LIKE %s) ',
2961 $like_pattern,
2962 $like_pattern
2963 );
2964 }
2965
2966 $extra_view = is_array( $others ) && isset( $others['extra_view'] ) && is_array( $others['extra_view'] ) && ! empty( $others['extra_view'] ) ? $others['extra_view'] : array( 'site_info' );
2967
2968 $view_query = null;
2969 if ( is_array( $others ) && ! empty( $others['view_query'] ) ) {
2970 $view_query = $others['view_query'];
2971 }
2972
2973 if ( empty( $view_query ) ) {
2974 $view_query = $selectgroups ? 'default' : 'group'; // To compatible.
2975 }
2976
2977 $view_selects = '';
2978 $view_joins = '';
2979
2980 $opts_view = $this->get_wp_options_join( $extra_view, $view_query );
2981
2982 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
2983 $view_selects = ',' . $opts_view['selects'];
2984 $view_joins = $opts_view['joins'];
2985 }
2986
2987 if ( MainWP_Utility::ctype_digit( $id ) ) {
2988 $where_allowed = $this->get_sql_where_allow_access_sites( 'wp', $is_staging );
2989 if ( $selectgroups ) {
2990 $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
2991 FROM ' . $this->table_name( 'wp' ) . ' wp
2992 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
2993 LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON wp.id = wpgr.wpid
2994 LEFT JOIN ' . $this->table_name( 'group' ) . ' gr ON wpgr.groupid = gr.id
2995 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
2996 ' . $view_joins . '
2997 WHERE wpgroup.groupid = ' . $id . ' ' .
2998 ( empty( $where ) ? '' : ' AND ' . $where ) . $where_allowed . $where_search . '
2999 GROUP BY wp.id, wp_sync.sync_id
3000 ORDER BY ' . $orderBy;
3001 } else {
3002 $qry = 'SELECT wp.*' . $view_selects . ', wp_sync.* FROM ' . $this->table_name( 'wp' ) . ' wp
3003 JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
3004 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
3005 ' . $view_joins . '
3006 WHERE wpgroup.groupid = ' . $id . ' ' . $where_allowed . $where_search .
3007 ( empty( $where ) ? '' : ' AND ' . $where ) . ' ORDER BY ' . $orderBy;
3008 }
3009 if ( ( false !== $offset ) && ( false !== $rowcount ) ) {
3010 $qry .= ' LIMIT ' . $offset . ', ' . $rowcount;
3011 } elseif ( false !== $rowcount ) {
3012 $qry .= ' LIMIT ' . $rowcount;
3013 }
3014
3015 return $qry;
3016 }
3017
3018 return null;
3019 }
3020
3021 /**
3022 * Get child sites by group name.
3023 *
3024 * @param int $userid Current user ID.
3025 * @param string $groupname Group name.
3026 *
3027 * @return object|null Database query result or null on failure.
3028 */
3029 public function get_websites_by_group_name( $userid, $groupname ) {
3030 return $this->get_results_result( $this->get_sql_websites_by_group_name( $groupname, $userid ) );
3031 }
3032
3033 /**
3034 * Get child sites by group name.
3035 *
3036 * @param string $groupname Group name.
3037 * @param int $userid Current user ID.
3038 *
3039 * @return object|null Database query result or null on failure.
3040 *
3041 * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
3042 */
3043 public function get_sql_websites_by_group_name( $groupname, $userid = null ) {
3044 if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
3045
3046 /**
3047 * Current user global.
3048 *
3049 * @global string
3050 */
3051 global $current_user;
3052
3053 $userid = $current_user->ID;
3054 }
3055
3056 $view_selects = '';
3057 $view_joins = '';
3058
3059 $opts_view = $this->get_wp_options_join();
3060
3061 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3062 $view_selects = ',' . $opts_view['selects'];
3063 $view_joins = $opts_view['joins'];
3064 }
3065
3066 $sql = 'SELECT wp.*,wp_sync.*' . $view_selects . ' FROM ' . $this->table_name( 'wp' ) . ' wp
3067 INNER JOIN ' . $this->table_name( 'wp_group' ) . ' wpgroup ON wp.id = wpgroup.wpid
3068 JOIN ' . $this->table_name( 'group' ) . ' g ON wpgroup.groupid = g.id
3069 JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.id = wp_sync.wpid
3070 ' . $view_joins . '
3071 WHERE g.name="' . $this->escape( $groupname ) . '"';
3072 if ( null !== $userid ) {
3073 $sql .= ' AND g.userid = "' . intval( $userid ) . '"';
3074 }
3075
3076 return $sql;
3077 }
3078
3079 /**
3080 * Get child site IP address.
3081 *
3082 * @param int $wpid Child site ID.
3083 *
3084 * @return string|null Child site IP address or null on failure.
3085 */
3086 public function get_wp_ip( $wpid ) {
3087 $table_name = esc_sql( $this->table_name( 'request_log' ) );
3088 return $this->wpdb->get_var( $this->wpdb->prepare( "SELECT ip FROM {$table_name} WHERE wpid = %d", $wpid ) );
3089 }
3090
3091 /**
3092 * Add website to the MainWP Dashboard.
3093 *
3094 * @param int $userid Current user ID.
3095 * @param string $name Child site name.
3096 * @param string $url Child site URL.
3097 * @param string $admin Child site administrator username.
3098 * @param string $pubkey OpenSSL public key.
3099 * @param string $privkey OpenSSL private key.
3100 * @param array $params Other params.
3101 *
3102 * @return int|false Child site ID or false on failure.
3103 *
3104 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3105 */
3106 public function add_website( // phpcs:ignore -- NOSONAR - complex.
3107 $userid,
3108 $name,
3109 $url,
3110 $admin,
3111 $pubkey,
3112 $privkey,
3113 $params = array()
3114 ) {
3115
3116 if ( ! is_array( $params ) ) {
3117 $params = array();
3118 }
3119
3120 $groupids = isset( $params['groupids'] ) ? $params['groupids'] : array();
3121 $groupnames = isset( $params['groupnames'] ) ? $params['groupnames'] : array();
3122 $verifyCertificate = isset( $params['verifyCertificate'] ) ? (int) $params['verifyCertificate'] : 2;
3123 $uniqueId = isset( $params['uniqueId'] ) ? $params['uniqueId'] : '';
3124 $http_user = isset( $params['http_user'] ) ? $params['http_user'] : null;
3125 $http_pass = isset( $params['http_pass'] ) ? $params['http_pass'] : null;
3126 $sslVersion = isset( $params['sslVersion'] ) ? $params['sslVersion'] : 0;
3127 $wpe = isset( $params['wpe'] ) ? $params['wpe'] : 0;
3128 $isStaging = isset( $params['isStaging'] ) ? $params['isStaging'] : 0;
3129
3130 // MWP-1548: encrypt http_user / http_pass at rest. Empty / null
3131 // values pass through unchanged (encrypt_credential is a no-op
3132 // for those). A non-empty value that fails to encrypt produces
3133 // false here, which we treat as a hard failure -- refuse to
3134 // persist plaintext credentials when the encryption layer is
3135 // unhealthy (missing keyfile, un-writable uploads dir).
3136 $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' );
3137 $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' );
3138 if ( false === $encrypted_http_user || false === $encrypted_http_pass ) {
3139 return false;
3140 }
3141
3142 if ( MainWP_Utility::ctype_digit( $userid ) ) {
3143 if ( '/' !== substr( $url, - 1 ) ) {
3144 $url .= '/';
3145 }
3146
3147 $en_pk_data = MainWP_Encrypt_Data_Lib::instance()->encrypt_privkey( base64_decode( $privkey ) ); // phpcs:ignore -- NOSONAR - base64_encode trust.
3148 $en_privkey = isset( $en_pk_data['en_data'] ) ? $en_pk_data['en_data'] : '';
3149
3150 $values = array(
3151 'userid' => $userid,
3152 'adminname' => $this->escape( $admin ),
3153 'name' => $this->escape( wp_strip_all_tags( $name ) ),
3154 'url' => $this->escape( $url ),
3155 'pubkey' => $this->escape( $pubkey ),
3156 'privkey' => $this->escape( base64_encode( $en_privkey ) ), // phpcs:ignore -- NOSONAR - trust.
3157 'siteurl' => '',
3158 'ga_id' => '',
3159 'gas_id' => 0,
3160 'offline_checks_last' => 0,
3161 'offline_check_result' => 0,
3162 'note' => '',
3163 'statsUpdate' => 0,
3164 'directories' => '',
3165 'plugin_upgrades' => '',
3166 'theme_upgrades' => '',
3167 'translation_upgrades' => '',
3168 'securityIssues' => '',
3169 'premium_upgrades' => '',
3170 'themes' => '',
3171 'ignored_themes' => '',
3172 'plugins' => '',
3173 'ignored_plugins' => '',
3174 'users' => '',
3175 'categories' => '',
3176 'pluginDir' => '',
3177 'automatic_update' => 0,
3178 'backup_before_upgrade' => 2,
3179 'verify_certificate' => intval( $verifyCertificate ),
3180 'ssl_version' => $sslVersion,
3181 'uniqueId' => $uniqueId,
3182 'mainwpdir' => 0,
3183 'http_user' => $encrypted_http_user,
3184 'http_pass' => $encrypted_http_pass,
3185 'wpe' => $wpe,
3186 'is_staging' => $isStaging,
3187 );
3188
3189 $syncValues = array(
3190 'dtsSync' => 0,
3191 'dtsSyncStart' => 0,
3192 'dtsAutomaticSync' => 0,
3193 'dtsAutomaticSyncStart' => 0,
3194 'totalsize' => 0,
3195 'extauth' => '',
3196 'sync_errors' => '',
3197 );
3198 if ( $this->wpdb->insert( $this->table_name( 'wp' ), $values ) ) {
3199 $websiteid = $this->wpdb->insert_id;
3200 MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[Insert site=%s]', $this->get_last_query() ) ); // after: $this->wpdb->insert_id.
3201 MainWP_Encrypt_Data_Lib::instance()->encrypt_save_keys( $websiteid, $en_pk_data );
3202 $syncValues['wpid'] = $websiteid;
3203 $this->wpdb->insert( $this->table_name( 'wp_sync' ), $syncValues );
3204 MainWP_Logger::instance()->log_events( 'db-queries', sprintf( '[Insert sync data=%s]', $this->get_last_query() ) );
3205 $this->wpdb->insert(
3206 $this->table_name( 'wp_settings_backup' ),
3207 array(
3208 'wpid' => $websiteid,
3209 'archiveFormat' => 'global',
3210 )
3211 );
3212
3213 foreach ( $groupnames as $groupname ) {
3214 if ( $this->wpdb->insert(
3215 $this->table_name( 'group' ),
3216 array(
3217 'userid' => $userid,
3218 'name' => $this->escape( htmlspecialchars( $groupname ) ),
3219 )
3220 )
3221 ) {
3222 $groupids[] = $this->wpdb->insert_id;
3223 }
3224 }
3225 // add groupids.
3226 foreach ( $groupids as $groupid ) {
3227 $this->wpdb->insert(
3228 $this->table_name( 'wp_group' ),
3229 array(
3230 'wpid' => $websiteid,
3231 'groupid' => $groupid,
3232 )
3233 );
3234 }
3235 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3236 return $websiteid;
3237 }
3238 }
3239
3240 return false;
3241 }
3242
3243 /**
3244 * Remove child site from the MainWP Dashboard.
3245 *
3246 * @param int $websiteid Child site ID.
3247 *
3248 * @return int|boolean Return child site ID that was removed or false on failure.
3249 *
3250 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3251 */
3252 public function remove_website( $websiteid ) {
3253 if ( MainWP_Utility::ctype_digit( $websiteid ) ) {
3254 $nr = $this->wpdb->delete( $this->table_name( 'wp' ), array( 'id' => $websiteid ) );
3255 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3256 $this->wpdb->delete( $this->table_name( 'wp_sync' ), array( 'wpid' => $websiteid ) );
3257 $this->wpdb->delete( $this->table_name( 'wp_options' ), array( 'wpid' => $websiteid ) );
3258 MainWP_Encrypt_Data_Lib::remove_key_file( $websiteid );
3259 MainWP_DB_Uptime_Monitoring::instance()->delete_monitor( array( 'wpid' => $websiteid ) );
3260 MainWP_Manage_Sites_List_Table::invalidate_manage_sites_cache();
3261 return $nr;
3262 }
3263
3264 return false;
3265 }
3266
3267 /**
3268 * Update child site db values.
3269 *
3270 * @param int $websiteid Child site ID.
3271 * @param array $fields Database fields to update.
3272 *
3273 * @return int|boolean The number of rows updated, or false on error.
3274 */
3275 public function update_website_values( $websiteid, $fields ) {
3276 if ( ! empty( $fields ) ) {
3277 // MWP-1548: encrypt http_user / http_pass at rest if either
3278 // is present in $fields. Generic-fields updaters (clone
3279 // flow in extensions-handler, callers that pass arbitrary
3280 // column subsets) must go through the same fail-closed
3281 // contract as add_website / update_website.
3282 if ( array_key_exists( 'http_user', $fields ) ) {
3283 $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_user'], 'http_user' );
3284 if ( false === $encrypted ) {
3285 return false;
3286 }
3287 $fields['http_user'] = $encrypted;
3288 }
3289 if ( array_key_exists( 'http_pass', $fields ) ) {
3290 $encrypted = MainWP_Credential_Storage::encrypt_credential( $fields['http_pass'], 'http_pass' );
3291 if ( false === $encrypted ) {
3292 return false;
3293 }
3294 $fields['http_pass'] = $encrypted;
3295 }
3296 // Lock the data stream to prevent other processes from updating at the same time.
3297 $table_name = esc_sql( $this->table_name( 'wp' ) );
3298 $sql = $this->wpdb->prepare(
3299 "SELECT * FROM {$table_name} WHERE id = %d FOR UPDATE",
3300 $websiteid
3301 );
3302 $this->wpdb->get_row( $sql );
3303
3304 return $this->wpdb->update( $this->table_name( 'wp' ), $fields, array( 'id' => $websiteid ) );
3305 }
3306
3307 return false;
3308 }
3309
3310 /**
3311 * Update child site sync values.
3312 *
3313 * @param int $websiteid Child site ID.
3314 * @param array $fields Database fields to update.
3315 *
3316 * @return int|boolean The number of rows updated, or false on error.
3317 */
3318 public function update_website_sync_values( $websiteid, $fields ) {
3319 if ( ! empty( $fields ) ) {
3320 return $this->wpdb->update( $this->table_name( 'wp_sync' ), $fields, array( 'wpid' => $websiteid ) );
3321 }
3322
3323 return false;
3324 }
3325
3326 /**
3327 * Update child site.
3328 *
3329 * @param int $websiteid Website ID.
3330 * @param string $url Child site URL.
3331 * @param int $userid Current user ID.
3332 * @param string $name Child site name.
3333 * @param string $siteadmin Child site administrator username.
3334 * @param array $groupids Group IDs.
3335 * @param array $groupnames Group Names.
3336 * @param string $pluginDir Plugin directory.
3337 * @param mixed $maximumFileDescriptorsOverride Overwrite the Maximum File Descriptors option.
3338 * @param mixed $maximumFileDescriptorsAuto Auto set the Maximum File Descriptors option.
3339 * @param mixed $maximumFileDescriptors Set the Maximum File Descriptors option.
3340 * @param int $verifyCertificate Whether or not to verify SSL Certificate.
3341 * @param mixed $archiveFormat Backup archive formate.
3342 * @param string $uniqueId Unique security ID.
3343 * @param string $http_user HTTP Basic Authentication username.
3344 * @param string $http_pass HTTP Basic Authentication password.
3345 * @param int $sslVersion SSL Version.
3346 * @param bool $disableHealthChecking Disable Site health threshold.
3347 * @param int $healthThreshold Site health threshold.
3348 * @param string $backup_method Primary backup method.
3349 *
3350 * @return boolean ture on success or false on failure.
3351 *
3352 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
3353 * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
3354 */
3355 public function update_website( // phpcs:ignore -- NOSONAR - complex.
3356 $websiteid,
3357 $url,
3358 $userid,
3359 $name,
3360 $siteadmin,
3361 $groupids,
3362 $groupnames,
3363 $pluginDir,
3364 $maximumFileDescriptorsOverride,
3365 $maximumFileDescriptorsAuto,
3366 $maximumFileDescriptors,
3367 $verifyCertificate = 1,
3368 $archiveFormat = 'global',
3369 $uniqueId = '',
3370 $http_user = null,
3371 $http_pass = null,
3372 $sslVersion = 0,
3373 $disableHealthChecking = 1,
3374 $healthThreshold = 0,
3375 $backup_method = 'global'
3376 ) {
3377
3378 $wpe = 0; // going to update when sync.
3379
3380 if ( MainWP_Utility::ctype_digit( $websiteid ) && MainWP_Utility::ctype_digit( $userid ) ) {
3381 $website = $this->get_website_by_id( $websiteid );
3382 if ( MainWP_System_Utility::can_edit_website( $website ) ) {
3383 // MWP-1548: encrypt http_user / http_pass at rest. Same
3384 // fail-closed contract as add_website -- a non-empty
3385 // value that fails to encrypt aborts the update so we
3386 // never overwrite an existing encrypted row with
3387 // plaintext when the encryption layer is unhealthy.
3388 $encrypted_http_user = MainWP_Credential_Storage::encrypt_credential( $http_user, 'http_user' );
3389 $encrypted_http_pass = MainWP_Credential_Storage::encrypt_credential( $http_pass, 'http_pass' );
3390 if ( false === $encrypted_http_user || false === $encrypted_http_pass ) {
3391 return false;
3392 }
3393 // update admin.
3394 $this->wpdb->update(
3395 $this->table_name( 'wp' ),
3396 array(
3397 'url' => $url,
3398 'name' => wp_strip_all_tags( $name ),
3399 'adminname' => $siteadmin,
3400 'pluginDir' => $pluginDir,
3401 'verify_certificate' => intval( $verifyCertificate ),
3402 'ssl_version' => intval( $sslVersion ),
3403 'wpe' => intval( $wpe ),
3404 'uniqueId' => $uniqueId,
3405 'http_user' => $encrypted_http_user,
3406 'http_pass' => $encrypted_http_pass,
3407 'disable_health_check' => $disableHealthChecking,
3408 'health_threshold' => $healthThreshold,
3409 'primary_backup_method' => $backup_method,
3410 ),
3411 array( 'id' => $websiteid )
3412 );
3413 $this->wpdb->update(
3414 $this->table_name( 'wp_settings_backup' ),
3415 array( 'archiveFormat' => $archiveFormat ),
3416 array( 'wpid' => $websiteid )
3417 );
3418
3419 if ( get_option( 'mainwp_enableLegacyBackupFeature' ) ) {
3420 $this->wpdb->update(
3421 $this->table_name( 'wp' ),
3422 array(
3423 'maximumFileDescriptorsOverride' => (int) $maximumFileDescriptorsOverride,
3424 'maximumFileDescriptorsAuto' => (int) $maximumFileDescriptorsAuto,
3425 'maximumFileDescriptors' => (int) $maximumFileDescriptors,
3426 ),
3427 array( 'id' => $websiteid )
3428 );
3429 }
3430
3431 // remove groups.
3432 $this->wpdb->delete( $this->table_name( 'wp_group' ), array( 'wpid' => $websiteid ) );
3433 // Remove GA stats.
3434 $showErrors = $this->wpdb->hide_errors();
3435
3436 /**
3437 * Action: mainwp_ga_delete_site
3438 *
3439 * Fires upon site removal process in order to delete Google Analytics data.
3440 *
3441 * @param int $websiteid Child site ID.
3442 *
3443 * @since Unknown
3444 */
3445 do_action( 'mainwp_ga_delete_site', $websiteid );
3446
3447 if ( $showErrors ) {
3448 $this->wpdb->show_errors();
3449 }
3450 // add groups with groupnames.
3451 foreach ( $groupnames as $groupname ) {
3452 if ( $this->wpdb->insert(
3453 $this->table_name( 'group' ),
3454 array(
3455 'userid' => $userid,
3456 'name' => $this->escape( $groupname ),
3457 )
3458 )
3459 ) {
3460 $groupids[] = $this->wpdb->insert_id;
3461 }
3462 }
3463 // add groupids.
3464 foreach ( $groupids as $groupid ) {
3465 $this->wpdb->insert(
3466 $this->table_name( 'wp_group' ),
3467 array(
3468 'wpid' => $websiteid,
3469 'groupid' => $groupid,
3470 )
3471 );
3472 }
3473
3474 return true;
3475 }
3476 }
3477
3478 return false;
3479 }
3480
3481
3482 /**
3483 * Get website update stats via SQL.
3484 *
3485 * @return object|null Database query result of null on failure.
3486 */
3487 public function get_websites_stats_update_sql() {
3488 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3489 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';
3490 }
3491
3492 /**
3493 * Update child site statistics.
3494 *
3495 * Update whether or not a child site has been updated.
3496 *
3497 * @param mixed $websiteid Child site ID.
3498 * @param mixed $statsUpdated Child site Update status.
3499 *
3500 * @return (int|boolean) Number of rows effected in update or false on failure.
3501 */
3502 public function update_website_stats( $websiteid, $statsUpdated ) {
3503 return $this->wpdb->update(
3504 $this->table_name( 'wp' ),
3505 array( 'statsUpdate' => $statsUpdated ),
3506 array( 'id' => $websiteid )
3507 );
3508 }
3509
3510 /**
3511 * Get child site by url.
3512 *
3513 * @param string $url Child site URL.
3514 *
3515 * @return object|null Database query result or null on failure.
3516 */
3517 public function get_websites_by_url( $url ) {
3518 if ( '/' !== substr( $url, - 1 ) ) {
3519 $url .= '/';
3520 }
3521 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3522 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3523 $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 );
3524 if ( $results ) {
3525 return $results;
3526 }
3527
3528 if ( stristr( $url, '/www.' ) ) {
3529 // remove www if it's there!
3530 $url = str_replace( '/www.', '/', $url );
3531 } else {
3532 // add www if it's not there!
3533 $url = str_replace( 'https://', 'https://www.', $url );
3534 $url = str_replace( 'http://', 'http://www.', $url );
3535 }
3536
3537 $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 );
3538 if ( $results ) {
3539 return $results;
3540 }
3541
3542 $url = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://', 'www.' ), array( '', '', '', '', '' ), $url );
3543
3544 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 );
3545 }
3546
3547 /**
3548 * Method get_websites_to_notice_health_threshold()
3549 *
3550 * Get websites to notice site health.
3551 *
3552 * @param int $globalThreshold Global site health threshold.
3553 */
3554 public function get_websites_to_notice_health_threshold( $globalThreshold ) {
3555
3556 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3557 $extra_view = array( 'monitoring_notification_emails', 'settings_notification_emails' );
3558
3559 if ( 80 >= $globalThreshold ) { // actual is 80.
3560 // should-be-improved site health.
3561 $where_global_threshold = '( wp.health_threshold = 0 AND wp_sync.health_value < 80 )';
3562 } else {
3563 // good site health.
3564 $where_global_threshold = '( wp.health_threshold = 0 AND wp_sync.health_value >= 80 )';
3565 }
3566
3567 $where_site_threshold = ' ( wp.health_threshold = 80 AND wp_sync.health_value < 80 ) '; // should-be-improved site health.
3568 $where_site_threshold .= ' OR ( wp.health_threshold = 100 AND wp_sync.health_value >= 80 ) '; // good site health.
3569
3570 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3571 $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
3572
3573 $view_selects = '';
3574 $view_joins = '';
3575
3576 $opts_view = $this->get_wp_options_join( $extra_view );
3577
3578 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3579 $view_selects = ',' . $opts_view['selects'];
3580 $view_joins = $opts_view['joins'];
3581 }
3582 return $this->wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $option_view is a validated SQL subquery
3583 "SELECT wp.*,wp_sync.* {$view_selects} FROM {$wp_table} wp
3584 JOIN {$wp_sync_table} wp_sync ON wp.id = wp_sync.wpid
3585 {$view_joins}
3586 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 " .
3587 $where . ' GROUP BY wp.id ',
3588 OBJECT
3589 );
3590 }
3591
3592 /**
3593 * Get websites offline status.
3594 *
3595 * @return array Sites with offline status.
3596 */
3597 public function get_websites_http_check_status() {
3598 $where = $this->get_sql_where_allow_access_sites( 'wp' );
3599 $extra_view = array( 'settings_notification_emails' );
3600 $wp_table = esc_sql( $this->table_name( 'wp' ) );
3601
3602 $view_selects = '';
3603 $view_joins = '';
3604
3605 $opts_view = $this->get_wp_options_join( $extra_view );
3606
3607 if ( is_array( $opts_view ) && ! empty( $opts_view['selects'] ) ) {
3608 $view_selects = ',' . $opts_view['selects'];
3609 $view_joins = $opts_view['joins'];
3610 }
3611
3612 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- is a validated SQL subquery.
3613 return $this->wpdb->get_results(
3614 "SELECT wp.*{$view_selects} FROM {$wp_table} wp
3615 {$view_joins}" . ' WHERE wp.suspended = 0 AND wp.http_code_noticed = 0 AND wp.offline_check_result = -1 ' .
3616 $where . ' GROUP BY wp.id ',
3617 OBJECT
3618 );
3619 }
3620
3621 /**
3622 * Method set_website_noticed_http_check().
3623 *
3624 * @param array $site_id The site id .
3625 *
3626 * @return void
3627 */
3628 public function set_website_noticed_http_check( $site_id = array() ) {
3629 if ( empty( $site_id ) ) {
3630 return;
3631 }
3632 $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.
3633 }
3634
3635 /**
3636 * Get DB Sites.
3637 *
3638 * @since 4.6
3639 *
3640 * @param mixed $params params.
3641 *
3642 * @return array $dbwebsites.
3643 */
3644 public function get_db_sites( $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
3645
3646 $dbwebsites = array();
3647
3648 $data_fields = MainWP_System_Utility::get_default_map_site_fields();
3649 $data_fields[] = 'verify_certificate';
3650 $data_fields[] = 'client_id';
3651
3652 $fields = isset( $params['fields'] ) && is_array( $params['fields'] ) ? $params['fields'] : array();
3653 $sites = isset( $params['sites'] ) && is_array( $params['sites'] ) ? $params['sites'] : array();
3654 $groups = isset( $params['groups'] ) && is_array( $params['groups'] ) ? $params['groups'] : array();
3655 $clients = isset( $params['clients'] ) && is_array( $params['clients'] ) ? $params['clients'] : array();
3656 $schema_fields = isset( $params['schema_fields'] ) && is_array( $params['schema_fields'] ) ? $params['schema_fields'] : array(); // since 5.2.
3657 $selectgroups = isset( $params['selectgroups'] ) && ! empty( $params['selectgroups'] ) ? true : false; // since 5.2.
3658
3659 if ( ! empty( $schema_fields ) ) { // since 5.2.
3660 foreach ( $schema_fields as $field_name ) {
3661 if ( ! in_array( $field_name, $data_fields ) ) {
3662 $data_fields[] = $field_name;
3663 }
3664 }
3665 } elseif ( is_array( $fields ) ) {
3666 foreach ( $fields as $field_indx => $field_name ) {
3667
3668 $get_field = $field_name;
3669 if ( is_numeric( $get_field ) || is_bool( $get_field ) ) { // to compatible fix.
3670 $get_field = $field_indx;
3671 }
3672
3673 if ( in_array( $get_field, static::$possible_options ) && ! in_array( $get_field, $data_fields ) ) {
3674 $data_fields[] = $get_field;
3675 }
3676 }
3677 }
3678
3679 if ( ! empty( $sites ) ) {
3680 foreach ( $sites as $v ) {
3681 if ( MainWP_Utility::ctype_digit( $v ) ) {
3682 $website = static::instance()->get_website_by_id( $v, $selectgroups );
3683 if ( empty( $website ) ) {
3684 continue;
3685 }
3686 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3687 }
3688 }
3689 }
3690
3691 if ( ! empty( $groups ) ) {
3692 foreach ( $groups as $v ) {
3693 if ( MainWP_Utility::ctype_digit( $v ) ) {
3694 $websites = static::instance()->query( static::instance()->get_sql_websites_by_group_id( $v, $selectgroups ) );
3695 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
3696 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3697 }
3698 static::free_result( $websites );
3699 }
3700 }
3701 }
3702
3703 $params = array(
3704 'full_data' => true,
3705 'selectgroups' => $selectgroups,
3706 );
3707 $client_sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $clients, $params );
3708 if ( $client_sites ) {
3709 foreach ( $client_sites as $website ) {
3710 $dbwebsites[ $website->id ] = MainWP_Utility::map_site( $website, $data_fields );
3711 }
3712 }
3713 return $dbwebsites;
3714 }
3715
3716 /**
3717 * Get Sites.
3718 *
3719 * @param int $websiteid The id of the child site you wish to retrieve.
3720 * @param bool $for_manager Check Team Control.
3721 * @param array $others Array of others.
3722 *
3723 * @return array $output Array of content to output.
3724 *
3725 * @uses \MainWP\Dashboard\MainWP_System_Utility::can_edit_website()
3726 * @uses \MainWP\Dashboard\MainWP_Utility::get_nice_url()
3727 */
3728 public function get_sites( $websiteid = null, $for_manager = false, $others = array() ) { // phpcs:ignore -- NOSONAR - not quite complex function.
3729
3730 if ( ! is_array( $others ) ) {
3731 $others = array();
3732 }
3733
3734 $search_site = null;
3735 $orderBy = 'wp.url';
3736 $offset = false;
3737 $rowcount = false;
3738 $extraWhere = null;
3739
3740 if ( isset( $websiteid ) && ( null !== $websiteid ) ) {
3741 $website = static::instance()->get_website_by_id( $websiteid );
3742
3743 if ( ! MainWP_System_Utility::can_edit_website( $website ) ) {
3744 return false;
3745 }
3746
3747 if ( ! \mainwp_current_user_can( 'site', $websiteid ) ) {
3748 return false;
3749 }
3750
3751 return array(
3752 array(
3753 'id' => $websiteid,
3754 'url' => MainWP_Utility::get_nice_url( $website->url, true ),
3755 'name' => $website->name,
3756 'totalsize' => $website->totalsize,
3757 'sync_errors' => $website->sync_errors,
3758 ),
3759 );
3760 } else {
3761 if ( isset( $others['orderby'] ) ) {
3762 if ( 'site' === $others['orderby'] ) {
3763 $orderBy = 'wp.name ' . ( 'asc' === $others['order'] ? 'asc' : 'desc' );
3764 } elseif ( 'url' === $others['orderby'] ) {
3765 $orderBy = 'wp.url ' . ( 'asc' === $others['order'] ? 'asc' : 'desc' );
3766 }
3767 }
3768 if ( isset( $others['search'] ) ) {
3769 $search_site = trim( $others['search'] );
3770 }
3771
3772 if ( is_array( $others ) && isset( $others['plugins_slug'] ) ) {
3773 $slugs = explode( ',', $others['plugins_slug'] );
3774 $extraWhere = '';
3775 foreach ( $slugs as $slug ) {
3776 $slug = wp_json_encode( $slug );
3777 $slug = trim( $slug, '"' );
3778 $slug = str_replace( '\\', '.', $slug );
3779 $extraWhere .= ' wp.plugins REGEXP "' . $slug . '" OR';
3780 }
3781 $extraWhere = trim( rtrim( $extraWhere, 'OR' ) );
3782
3783 if ( '' === $extraWhere ) {
3784 $extraWhere = null;
3785 } else {
3786 $extraWhere = '(' . $extraWhere . ')';
3787 }
3788 }
3789 }
3790
3791 $totalRecords = '';
3792
3793 if ( isset( $others['per_page'] ) && ! empty( $others['per_page'] ) ) {
3794 $sql = static::instance()->get_sql_websites_for_current_user( false, $search_site, $orderBy, false, false, $extraWhere, $for_manager );
3795 $websites_total = static::instance()->query( $sql );
3796 $totalRecords = ( $websites_total ? static::num_rows( $websites_total ) : 0 );
3797
3798 if ( $websites_total ) {
3799 static::free_result( $websites_total );
3800 }
3801
3802 $rowcount = absint( $others['per_page'] );
3803 $pagenum = isset( $others['paged'] ) ? absint( $others['paged'] ) : 0;
3804 if ( $pagenum > $totalRecords ) {
3805 $pagenum = $totalRecords;
3806 }
3807 $pagenum = max( 1, $pagenum );
3808 $offset = ( $pagenum - 1 ) * $rowcount;
3809
3810 }
3811
3812 $sql = static::instance()->get_sql_websites_for_current_user( false, $search_site, $orderBy, $offset, $rowcount, $extraWhere, $for_manager );
3813 $websites = static::instance()->query( $sql );
3814
3815 $output = array();
3816 while ( $websites && ( $website = static::fetch_object( $websites ) ) ) {
3817 $re = array(
3818 'id' => $website->id,
3819 'url' => MainWP_Utility::get_nice_url( $website->url, true ),
3820 'name' => $website->name,
3821 'totalsize' => $website->totalsize,
3822 'sync_errors' => $website->sync_errors,
3823 'client_id' => $website->client_id,
3824 );
3825
3826 if ( 0 < $totalRecords ) {
3827 $re['totalRecords'] = $totalRecords;
3828 $totalRecords = 0;
3829 }
3830
3831 $output[] = $re;
3832 }
3833 static::free_result( $websites );
3834
3835 return $output;
3836 }
3837
3838 /**
3839 * Method get_lookup_items().
3840 *
3841 * Get bulk lookup items to reduce number of db queries.
3842 *
3843 * @param string $item_name lookup item name.
3844 * @param int $item_id lookup item id.
3845 * @param string $obj_name loockup object name.
3846 *
3847 * @return mixed Result
3848 */
3849 public function get_lookup_items( $item_name, $item_id, $obj_name ) {
3850 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.
3851 }
3852
3853 /**
3854 * Method insert_lookup_item().
3855 *
3856 * Insert lookup item, need checks existed before to prevent double values.
3857 *
3858 * @param string $item_name item name.
3859 * @param int $item_id item id.
3860 * @param string $obj_name object name.
3861 * @param int $obj_id object id.
3862 *
3863 * @return mixed Result
3864 */
3865 public function insert_lookup_item( $item_name, $item_id, $obj_name, $obj_id ) {
3866 if ( empty( $item_name ) || empty( $item_id ) || empty( $obj_name ) || empty( $obj_id ) ) {
3867 return false;
3868 }
3869 $data = array(
3870 'item_name' => 'cost',
3871 'item_id' => $item_id,
3872 'object_name' => $obj_name,
3873 'object_id' => $obj_id,
3874 );
3875 $this->wpdb->insert( $this->table_name( 'lookup_item_objects' ), $data );
3876 return $this->wpdb->insert_id; // must return lookup id.
3877 }
3878
3879 /**
3880 * Method delete_lookup_items().
3881 *
3882 * Delete bulk lookup items by lookup ids or object names with item id and item name, to reduce number of db queries.
3883 *
3884 * @param string $by Delete by.
3885 * @param array $params params.
3886 *
3887 * @return mixed Result
3888 */
3889 public function delete_lookup_items( $by = 'lookup_id', $params = array() ) { // phpcs:ignore -- NOSONAR - complex.
3890 if ( ! is_array( $params ) ) {
3891 return false;
3892 }
3893
3894 $lookup_ids = isset( $params['lookup_ids'] ) ? $params['lookup_ids'] : null;
3895 $item_id = isset( $params['item_id'] ) ? $params['item_id'] : null;
3896 $object_id = isset( $params['object_id'] ) ? $params['object_id'] : null;
3897 $item_name = isset( $params['item_name'] ) ? $params['item_name'] : null;
3898 $obj_names = isset( $params['object_names'] ) ? $params['object_names'] : null;
3899
3900 if ( 'object_name' === $by ) {
3901 if ( empty( $item_id ) || empty( $item_name ) ) {
3902 return false;
3903 }
3904
3905 $obj_names = $this->escape_array( $obj_names );
3906 if ( ! empty( $obj_names ) ) {
3907 $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.
3908 return true;
3909 }
3910 } elseif ( 'object_id' === $by ) {
3911 if ( empty( $object_id ) || empty( $item_name ) || empty( $obj_names ) ) {
3912 return false;
3913 }
3914
3915 $obj_names = $this->escape_array( $obj_names );
3916 if ( ! empty( $obj_names ) ) {
3917 $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.
3918 return true;
3919 }
3920 } elseif ( 'lookup_id' === $by ) {
3921 if ( empty( $lookup_ids ) ) {
3922 return false;
3923 }
3924 if ( is_numeric( $lookup_ids ) ) {
3925 $lookup_ids = array( $lookup_ids );
3926 } elseif ( is_array( $lookup_ids ) ) {
3927 $lookup_ids = MainWP_Utility::array_numeric_filter( $lookup_ids );
3928 } else {
3929 return false;
3930 }
3931 $this->wpdb->query( 'DELETE FROM ' . $this->table_name( 'lookup_item_objects' ) . ' WHERE lookup_id IN (' . implode( ',', $lookup_ids ) . ') ' ); //phpcs:ignore -- ok.
3932 return true;
3933 }
3934 return false;
3935 }
3936
3937
3938 /**
3939 * Insert a new REST API key row and return the credential payload.
3940 *
3941 * Returns an array with key_id, user_id, plaintext consumer_key,
3942 * plaintext consumer_secret, and key_permissions on success. Returns
3943 * false when the wpdb->insert() call fails (no current user, missing
3944 * table, schema mismatch, etc.). Callers must check the return type
3945 * before treating it as an array.
3946 *
3947 * @param string $consumer_key Consumer key.
3948 * @param string $consumer_secret Secret key.
3949 * @param string $scope scope.
3950 * @param string $description description.
3951 * @param int $enabled 1 or 0.
3952 * @param array $others others.
3953 *
3954 * @return array|false Credential payload on success, false on failure.
3955 */
3956 public function insert_rest_api_key( $consumer_key, $consumer_secret, $scope, $description, $enabled, $others = array() ) {
3957 global $current_user;
3958
3959 if ( $current_user ) {
3960 $user_id = $current_user->ID;
3961 }
3962
3963 if ( empty( $user_id ) ) {
3964 return false;
3965 }
3966
3967 unset( $others ); // Parameter retained for signature compatibility; key_pass/key_type fields are vestigial after MWP-1544 cleanup.
3968
3969 // Hash the consumer_secret with WordPress's password hasher so the
3970 // value at rest is no longer reversible by a DB-read primitive.
3971 // The plaintext is returned to the caller below so it can be shown
3972 // to the admin once at creation time. See MWP-1540.
3973 $hashed_secret = wp_hash_password( $consumer_secret );
3974
3975 // Created API keys.
3976 $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
3977 $inserted = $this->wpdb->insert(
3978 $this->table_name( 'api_keys' ),
3979 array(
3980 'user_id' => $user_id,
3981 'description' => $description,
3982 'permissions' => $permissions,
3983 'consumer_key' => mainwp_api_hash( $consumer_key ),
3984 'consumer_secret' => $hashed_secret,
3985 'truncated_key' => substr( $consumer_key, -7 ),
3986 'enabled' => $enabled,
3987 ),
3988 array(
3989 '%d',
3990 '%s',
3991 '%s',
3992 '%s',
3993 '%s',
3994 '%s',
3995 '%d',
3996 ),
3997 );
3998
3999 // wpdb->insert() returns false on failure. Without this guard the
4000 // function would still hand back the plaintext consumer_secret plus
4001 // $wpdb->insert_id, but that insert_id is the LAST successful insert
4002 // on the connection (typically from earlier in the same request),
4003 // not this row. The caller's empty-key_id check would pass and the
4004 // operator would receive a credential that was never persisted.
4005 // See MWP-1540 PR review feedback.
4006 if ( false === $inserted ) {
4007 return false;
4008 }
4009
4010 return array(
4011 'key_id' => $this->wpdb->insert_id,
4012 'user_id' => $user_id,
4013 'consumer_key' => $consumer_key,
4014 'consumer_secret' => $consumer_secret,
4015 'key_permissions' => $permissions,
4016 );
4017 }
4018
4019 /**
4020 * Update rest api key.
4021 *
4022 * @param int $key_id Consumer key.
4023 * @param string $scope scope.
4024 * @param string $description description.
4025 * @param int $enabled Enabled.
4026 *
4027 * @return array
4028 */
4029 public function update_rest_api_key( $key_id, $scope, $description, $enabled = 1 ) {
4030 $permissions = in_array( $scope, array( 'read', 'write', 'delete', 'read_write' ), true ) ? sanitize_text_field( $scope ) : 'read';
4031 return $this->wpdb->update(
4032 $this->table_name( 'api_keys' ),
4033 array(
4034 'description' => $description,
4035 'permissions' => $permissions,
4036 'enabled' => $enabled ? 1 : 0,
4037 ),
4038 array(
4039 'key_id' => $key_id,
4040 )
4041 );
4042 }
4043
4044
4045 /**
4046 * Method is_existed_enabled_rest_key().
4047 *
4048 * @return bool result.
4049 */
4050 public function is_existed_enabled_rest_key() {
4051 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4052 $enabled = $this->wpdb->get_row( "SELECT * FROM {$table_name} WHERE enabled = 1 LIMIT 1" );
4053 return $enabled ? true : false;
4054 }
4055
4056 /**
4057 * Method get_rest_api_key_by().
4058 *
4059 * @param int $id To get key.
4060 *
4061 * @return array
4062 */
4063 public function get_rest_api_key_by( $id ) {
4064 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4065 return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table_name} WHERE key_id = %d", $id ) );
4066 }
4067
4068 /**
4069 * Method remove_rest_api_key().
4070 *
4071 * @param string $id to delete.
4072 *
4073 * @return array
4074 */
4075 public function remove_rest_api_key( $id ) {
4076 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4077 return $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM {$table_name} WHERE key_id = %s", $id ) );
4078 }
4079
4080 /**
4081 * Method get_rest_api_keys().
4082 *
4083 * @return array
4084 */
4085 public function get_rest_api_keys() {
4086 $table_name = esc_sql( $this->table_name( 'api_keys' ) );
4087 return $this->wpdb->get_results( "SELECT * FROM {$table_name} ORDER BY key_id DESC" );
4088 }
4089
4090
4091 /**
4092 * Update regular process.
4093 *
4094 * @param array $data process data.
4095 * @return mixed
4096 */
4097 public function update_regular_process( $data ) {
4098 if ( isset( $data['process_id'] ) ) {
4099 $process_id = $data['process_id'];
4100 unset( $data['process_id'] );
4101 return $this->wpdb->update( $this->table_name( 'schedule_processes' ), $data, array( 'process_id' => $process_id ) );
4102 } elseif ( is_array( $data ) && isset( $data['type'] ) && isset( $data['process_slug'] ) ) {
4103 return $this->wpdb->insert( $this->table_name( 'schedule_processes' ), $data );
4104 }
4105 return false;
4106 }
4107
4108 /**
4109 * Delete regular process.
4110 *
4111 * @param int $process_id Process id.
4112 * @param int $item_id Item id.
4113 * @param string $pro_type Process type.
4114 * @param string $pro_slug Process slug.
4115 *
4116 * @return mixed
4117 */
4118 public function delete_regular_process( $process_id = false, $item_id = false, $pro_type = false, $pro_slug = false ) {
4119
4120 if ( is_numeric( $process_id ) && ! empty( $process_id ) ) {
4121 return $this->wpdb->delete(
4122 $this->table_name( 'schedule_processes' ),
4123 array(
4124 'process_id' => $process_id,
4125 )
4126 );
4127 } elseif ( ! empty( $pro_type ) || ! empty( $pro_slug ) ) {
4128
4129 $data = array();
4130
4131 if ( ! empty( $pro_type ) ) {
4132 $data['type'] = $pro_type;
4133 }
4134
4135 if ( ! empty( $pro_slug ) ) {
4136 $data['process_slug'] = $pro_slug;
4137 }
4138
4139 if ( ! empty( $item_id ) ) {
4140 $data['item_id'] = $item_id;
4141 }
4142 // Bulk delete.
4143 return $this->wpdb->delete( $this->table_name( 'schedule_processes' ), $data );
4144 }
4145 return false;
4146 }
4147
4148 /**
4149 * Method get_regular_process_by_item_id_type_slug
4150 *
4151 * @param integer $item_id item id.
4152 * @param string $type type.
4153 * @param string $process_slug process slug.
4154 *
4155 * @return mixed result
4156 */
4157 public function get_regular_process_by_item_id_type_slug( $item_id, $type, $process_slug ) {
4158 $table_name = esc_sql( $this->table_name( 'schedule_processes' ) );
4159 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 ) );
4160 }
4161
4162 /**
4163 * Log SQL queries for debugging via hook.
4164 *
4165 * This method provides a structured way to log SQL queries for development/debugging.
4166 * It does NOT use error_log() directly - instead, it fires the `mainwp_log_system_query`
4167 * action hook, allowing external listeners (logging plugins, debug tools) to handle
4168 * the log output appropriately.
4169 *
4170 * To enable query logging:
4171 * 1. Set `$params['dev_log_query'] = 1` when calling database methods
4172 * 2. Add a listener to the `mainwp_log_system_query` action hook
4173 *
4174 * Example listener:
4175 * ```php
4176 * add_action( 'mainwp_log_system_query', function( $params, $sql, $caller ) {
4177 * error_log( 'MainWP Query: ' . $sql );
4178 * }, 10, 3 );
4179 * ```
4180 *
4181 * @param array $params Query parameters. Set 'dev_log_query' to enable logging.
4182 * @param string $sql The SQL query string.
4183 * @param mixed $caller Instance of caller class (optional).
4184 * @return void
4185 */
4186 public function log_system_query( $params, $sql, $caller = false ) {
4187 $params = apply_filters( 'mainwp_log_system_query_params', $params, $sql, $caller );
4188 if ( is_array( $params ) && ! empty( $params['dev_log_query'] ) && ! empty( $sql ) ) {
4189 do_action( 'mainwp_log_system_query', $params, $sql, $caller );
4190 }
4191 }
4192 }
4193