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

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