PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.1.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.1.2
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.2, at class/class-mainwp-db.php

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