PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | class/class-mainwp-db-common.php +1382 -751 5.0 → 6.2.2 View file →
@@ -8,873 +8,1504 @@
8 8 */
9 9
10 10 namespace MainWP\Dashboard;
11 11
12 +// Exit if accessed directly.
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit;
15 +}
16 +
12 17 /**
13 18 * Class MainWP_DB_Common
14 19 *
15 20 * @package MainWP\Dashboard
16 21 */
17 -class MainWP_DB_Common extends MainWP_DB {
22 +class MainWP_DB_Common extends MainWP_DB { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
18 23
19 - // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.DB.PreparedSQL.NotPrepared -- unprepared SQL ok, accessing the database directly to custom database functions.
24 + // 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.
20 25
21 - /**
22 - * Private static variable to hold the single instance of the class.
23 - *
24 - * @static
25 - *
26 - * @var mixed Default null
27 - */
28 - private static $instance = null;
26 + /**
27 + * Private static variable to hold the single instance of the class.
28 + *
29 + * @static
30 + *
31 + * @var mixed Default null
32 + */
33 + private static $instance = null;
29 34
30 - /**
31 - * Method instance()
32 - *
33 - * Create public static instance.
34 - *
35 - * @static
36 - * @return MainWP_DB_Common
37 - */
38 - public static function instance() {
39 - if ( null === self::$instance ) {
40 - self::$instance = new self();
41 - }
42 - return self::$instance;
43 - }
35 + /**
36 + * Method instance()
37 + *
38 + * Create public static instance.
39 + *
40 + * @static
41 + * @return MainWP_DB_Common
42 + */
43 + public static function instance() {
44 + if ( null === static::$instance ) {
45 + static::$instance = new self();
46 + }
47 + return static::$instance;
48 + }
44 49
45 - /**
46 - * Method get_last_sync_status()
47 - *
48 - * Get last sync status.
49 - *
50 - * @param string|null $userId User ID.
51 - *
52 - * @return string $return all_synced|not_synced|last_sync
53 - */
54 - public function get_last_sync_status( $userId = null ) {
55 - $sql = $this->get_sql_websites_for_current_user();
56 - $websites = $this->query( $sql );
50 + /**
51 + * Get the last sync status.
52 + *
53 + * @param string $is_staging Wether query on staging sites.
54 + *
55 + * @return array{
56 + * sync_status: string|false,
57 + * last_sync: int
58 + * }
59 + */
60 + public function get_last_sync_status( $is_staging = 'no' ) {
57 61
58 - $return = array(
59 - 'sync_status' => false,
60 - 'last_sync' => 0,
61 - );
62 + $cutoff = time() - DAY_IN_SECONDS;
62 63
63 - if ( ! $websites ) {
64 - $return['sync_status'] = 'all_synced';
65 - return $return;
66 - }
64 + $site_table = $this->table_name( 'wp' );
65 + $sync_table = $this->table_name( 'wp_sync' );
67 66
68 - $total_sites = 0;
69 - $synced_sites = 0;
70 - $last_sync = 0;
71 - self::data_seek( $websites, 0 );
72 - while ( $websites && ( $website = self::fetch_object( $websites ) ) ) {
73 - if ( empty( $website ) || '' !== $website->sync_errors ) {
74 - continue;
75 - }
76 - ++$total_sites;
77 - if ( 60 * 60 * 24 > time() - $website->dtsSync ) {
78 - ++$synced_sites;
79 - }
80 - if ( $last_sync < $website->dtsSync ) {
81 - $last_sync = $website->dtsSync;
82 - }
83 - }
67 + $sql = $this->wpdb->prepare(
68 + "
69 + SELECT
70 + COUNT(*) AS total_sites,
71 + SUM(CASE WHEN s.dtsSync >= %d THEN 1 ELSE 0 END) AS synced_sites,
72 + MAX(s.dtsSync) AS last_sync
73 + FROM {$site_table} w
74 + INNER JOIN {$sync_table} s ON s.wpid = w.id
75 + WHERE s.sync_errors = ''
76 + " . $this->get_sql_where_allow_access_sites( 'w', $is_staging ),
77 + $cutoff
78 + );
84 79
85 - if ( $total_sites === $synced_sites ) {
86 - $return['sync_status'] = 'all_synced';
87 - } elseif ( 0 === $synced_sites ) {
88 - $return['sync_status'] = 'not_synced';
89 - }
90 - $return['last_sync'] = $last_sync;
91 - return $return;
92 - }
80 + $stats = $this->wpdb->get_row( $sql );
93 81
94 - /**
95 - * Method get_group_by_name()
96 - *
97 - * Get group by name.
98 - *
99 - * @param mixed $name Group name.
100 - * @param null $userid user ID.
101 - *
102 - * @return object|null Database query result for chosen group name or null on failure
103 - *
104 - * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
105 - */
106 - public function get_group_by_name( $name, $userid = null ) {
107 - if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
82 + $return = array(
83 + 'sync_status' => false,
84 + 'last_sync' => 0,
85 + );
108 86
109 - /**
110 - * Current user global.
111 - *
112 - * @global string
113 - */
114 - global $current_user;
87 + if ( empty( $stats ) || 0 === (int) $stats->total_sites ) {
88 + $return['sync_status'] = 'all_synced';
89 + return $return;
90 + }
115 91
116 - $userid = $current_user->ID;
117 - }
118 - $where = ( null !== $userid ) ? ' AND userid=' . intval( $userid ) : '';
119 - $where .= $this->get_sql_where_allow_groups();
92 + $total_sites = (int) $stats->total_sites;
93 + $synced_sites = (int) $stats->synced_sites;
120 94
121 - return $this->wpdb->get_row( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'group' ) . ' WHERE 1 ' . $where . ' AND name= %s', $this->escape( $name ) ) );
122 - }
95 + if ( $total_sites === $synced_sites ) {
96 + $return['sync_status'] = 'all_synced';
97 + } elseif ( 0 === $synced_sites ) {
98 + $return['sync_status'] = 'not_synced';
99 + }
123 100
124 - /**
125 - * Method get_group_by_id()
126 - *
127 - * Get group by ID.
128 - *
129 - * @param mixed $id Group ID.
130 - *
131 - * @return object|null Database query result for chosen Group ID or null on failure.
132 - *
133 - * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
134 - */
135 - public function get_group_by_id( $id ) {
136 - if ( MainWP_Utility::ctype_digit( $id ) ) {
137 - $group = $this->wpdb->get_row( $this->wpdb->prepare( 'SELECT * FROM ' . $this->table_name( 'group' ) . ' WHERE id= %d', $id ) );
138 - return $group;
139 - }
101 + $return['last_sync'] = (int) $stats->last_sync;
140 102
141 - return null;
142 - }
103 + return $return;
104 + }
143 105
144 - /**
145 - * Method get_groups_for_manage_sites()
146 - *
147 - * Get groups for mananged sites.
148 - *
149 - * @return object|null Database query result for Managed Sites Groups or null on failure.
150 - *
151 - * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
152 - */
153 - public function get_groups_for_manage_sites() {
154 - $where = ' 1 ';
155 - if ( MainWP_System::instance()->is_multi_user() ) {
106 + /**
107 + * Method get_group_by_name()
108 + *
109 + * Get group by name.
110 + *
111 + * @param mixed $name Group name.
112 + * @param null $userid user ID.
113 + *
114 + * @return object|null Database query result for chosen group name or null on failure
115 + *
116 + * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
117 + */
118 + public function get_group_by_name( $name, $userid = null ) {
119 + if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
156 120
157 - /**
158 - * Current user global.
159 - *
160 - * @global string
161 - */
162 - global $current_user;
121 + /**
122 + * Current user global.
123 + *
124 + * @global string
125 + */
126 + global $current_user;
163 127
164 - $where = ' userid = ' . $current_user->ID . ' ';
165 - }
166 - $with_staging = 'yes';
167 - $staging_enabled = is_plugin_active( 'mainwp-staging-extension/mainwp-staging-extension.php' ) || is_plugin_active( 'mainwp-timecapsule-extension/mainwp-timecapsule-extension.php' );
128 + $userid = $current_user->ID;
129 + }
130 + $where = ( null !== $userid ) ? ' AND userid=' . intval( $userid ) : '';
131 + $where .= $this->get_sql_where_allow_groups();
168 132
169 - if ( ! $staging_enabled ) {
170 - $with_staging = 'no';
171 - }
133 + $table_group = esc_sql( $this->table_name( 'group' ) );
134 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM `{$table_group}` WHERE 1 " . esc_sql( $where ) . ' AND name= %s', $this->escape( $name ) ) );
135 + }
172 136
173 - $where .= $this->get_sql_where_allow_groups( '', $with_staging );
137 + /**
138 + * Method get_group_by_id()
139 + *
140 + * Get group by ID.
141 + *
142 + * @param mixed $id Group ID.
143 + *
144 + * @return object|null Database query result for chosen Group ID or null on failure.
145 + *
146 + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
147 + */
148 + public function get_group_by_id( $id ) {
149 + if ( MainWP_Utility::ctype_digit( $id ) ) {
150 + $table_group = esc_sql( $this->table_name( 'group' ) );
151 + return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM `{$table_group}` WHERE id= %d", $id ) );
152 + }
174 153
175 - return $this->wpdb->get_results( 'SELECT * FROM ' . $this->table_name( 'group' ) . ' WHERE ' . $where . ' ORDER BY name', OBJECT_K );
176 - }
154 + return null;
155 + }
177 156
178 - /**
179 - * Method get_groups_for_current_user()
180 - *
181 - * Get groups for current user.
182 - *
183 - * @return object|null Database query result for Current User Groups or null on failure.
184 - *
185 - * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
186 - */
187 - public function get_groups_for_current_user() {
188 - $where = ' 1 ';
189 - if ( MainWP_System::instance()->is_multi_user() ) {
157 + /**
158 + * Method get_groups_for_manage_sites()
159 + *
160 + * Get groups for mananged sites.
161 + *
162 + * @return object|null Database query result for Managed Sites Groups or null on failure.
163 + *
164 + * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
165 + */
166 + public function get_groups_for_manage_sites() {
167 + $where = ' 1 ';
168 + if ( MainWP_System::instance()->is_multi_user() ) {
190 169
191 - /**
192 - * Current user global.
193 - *
194 - * @global string
195 - */
196 - global $current_user;
170 + /**
171 + * Current user global.
172 + *
173 + * @global string
174 + */
175 + global $current_user;
197 176
198 - $where = ' userid = ' . $current_user->ID . ' ';
199 - }
200 - $where .= $this->get_sql_where_allow_groups();
177 + $where = ' userid = ' . $current_user->ID . ' ';
178 + }
179 + $with_staging = 'yes';
180 + $staging_enabled = is_plugin_active( 'mainwp-staging-extension/mainwp-staging-extension.php' ) || is_plugin_active( 'mainwp-timecapsule-extension/mainwp-timecapsule-extension.php' );
201 181
202 - return $this->wpdb->get_results( 'SELECT * FROM ' . $this->table_name( 'group' ) . ' WHERE ' . $where . ' ORDER BY name', OBJECT_K );
203 - }
182 + if ( ! $staging_enabled ) {
183 + $with_staging = 'no';
184 + }
204 185
205 - /**
206 - * Method get_groups_by_website_id()
207 - *
208 - * Get groups by website ID.
209 - *
210 - * @param mixed $websiteid Child Site ID.
211 - *
212 - * @return object|null Database query result for groups by website ID or null on failure.
213 - *
214 - * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
215 - */
216 - public function get_groups_by_website_id( $websiteid ) {
217 - if ( MainWP_Utility::ctype_digit( $websiteid ) ) {
218 - return $this->wpdb->get_results(
219 - $this->wpdb->prepare(
220 - 'SELECT * FROM ' . $this->table_name( 'group' ) . ' gr JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON gr.id = wpgr.groupid WHERE wpgr.wpid = %d ORDER BY name',
221 - $websiteid
222 - ),
223 - OBJECT_K
224 - );
225 - }
186 + $where .= $this->get_sql_where_allow_groups( '', $with_staging );
226 187
227 - return null;
228 - }
188 + $table_group = esc_sql( $this->table_name( 'group' ) );
189 + return $this->wpdb->get_results( "SELECT * FROM `{$table_group}` WHERE " . esc_sql( $where ) . ' ORDER BY name', OBJECT_K );
190 + }
229 191
230 - /**
231 - * Medthod get_groups_and_count()
232 - *
233 - * Get groups and count.
234 - *
235 - * @param null $userid Current user ID.
236 - * @param bool $for_manager Default: false.
237 - *
238 - * @return object|null Database query result for groups and count or null on failure.
239 - *
240 - * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
241 - */
242 - public function get_groups_and_count( $userid = null, $for_manager = false ) {
243 - if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
244 192
245 - /**
246 - * Current user global.
247 - *
248 - * @global string
249 - */
250 - global $current_user;
193 + /**
194 + * Method get_sql_where_wpopt_phpversion().
195 + *
196 + * @param string $coln Column compare.
197 + * @param string $operator Operator compare.
198 + * @param string $ver_str Version compare.
199 + *
200 + * @return string Sql version compare.
201 + */
202 + public function get_sql_where_wpopt_phpversion( $coln, $operator, $ver_str ) {
203 + $allowed_columns = array( 'phpversion' );
204 + $allowed_operators = array( '>', '>=', '<', '<=', '=', '!=' );
205 + if ( ! in_array( $coln, $allowed_columns, true ) || ! in_array( $operator, $allowed_operators, true ) ) {
206 + return ' 0 = 1 ';
207 + }
208 + $alias = 'owp_' . $coln;
209 + return $this->wpdb->prepare(
210 + " ( {$alias}.value IS NOT NULL AND INET_ATON( SUBSTRING_INDEX( CONCAT(SUBSTRING_INDEX( {$alias}.value, '-', 1), '.0.0.0.0'), '.', 4 ) ) {$operator} INET_ATON(%s) ) ", //phpcs:ignore -- NOSONAR escaped variables.
211 + $ver_str
212 + );
213 + }
251 214
252 - $userid = $current_user->ID;
253 - }
215 + /**
216 + * Method get_sql_version_compare().
217 + *
218 + * @param string $coln Column compare.
219 + * @param string $operator Operator compare.
220 + * @param string $ver_str Version compare.
221 + *
222 + * @return string Sql version compare.
223 + */
224 + public function get_sql_version_compare( $coln, $operator, $ver_str ) {
225 + // It's safe since it's not user input, but the AI still suggests escaping it.
226 + return ' INET_ATON( SUBSTRING_INDEX( CONCAT( SUBSTRING_INDEX(' . $this->escape( $coln ) . ", '-', 1), '.0.0.0.0' ), '.', 4) ) " .
227 + $this->escape( $operator ) . " INET_ATON('" . $this->escape( $ver_str ) . "') ";
228 + }
254 229
255 - $where = '';
230 + /**
231 + * Method get_groups_for_current_user()
232 + *
233 + * Get groups for current user.
234 + *
235 + * @return object|null Database query result for Current User Groups or null on failure.
236 + *
237 + * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
238 + */
239 + public function get_groups_for_current_user() {
240 + $where = ' 1 ';
241 + if ( MainWP_System::instance()->is_multi_user() ) {
256 242
257 - if ( ! empty( $userid ) ) {
258 - $where = ' AND gr.userid = ' . intval( $userid );
259 - }
243 + /**
244 + * Current user global.
245 + *
246 + * @global string
247 + */
248 + global $current_user;
260 249
261 - if ( ! $for_manager ) {
262 - $where .= $this->get_sql_where_allow_groups( 'gr' );
263 - }
250 + $where = ' userid = ' . $current_user->ID . ' ';
251 + }
252 + $where .= $this->get_sql_where_allow_groups();
264 253
265 - return $this->wpdb->get_results( 'SELECT gr.*, COUNT(DISTINCT(wpgr.wpid)) as nrsites FROM ' . $this->table_name( 'group' ) . ' gr LEFT JOIN ' . $this->table_name( 'wp_group' ) . ' wpgr ON gr.id = wpgr.groupid WHERE 1 ' . $where . ' GROUP BY gr.id ORDER BY gr.name', OBJECT_K );
266 - }
254 + $table_group = esc_sql( $this->table_name( 'group' ) );
255 + return $this->wpdb->get_results( "SELECT * FROM `{$table_group}` WHERE " . esc_sql( $where ) . ' ORDER BY name', OBJECT_K );
256 + }
267 257
268 - /**
269 - * Method get_not_empty_groups()
270 - *
271 - * Get non-empty groups.
272 - *
273 - * @param null $userid Current user ID.
274 - * @param bool $enableOfflineSites Include offline sites? Default: true.
275 - *
276 - * @return object|null Database query result for non-empty groups or null on failure.
277 - *
278 - * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
279 - */
280 - public function get_not_empty_groups( $userid = null, $enableOfflineSites = true ) {
281 - if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
258 + /**
259 + * Method get_groups_by_website_id()
260 + *
261 + * Get groups by website ID.
262 + *
263 + * @param mixed $websiteid Child Site ID.
264 + *
265 + * @return object|null Database query result for groups by website ID or null on failure.
266 + *
267 + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
268 + */
269 + public function get_groups_by_website_id( $websiteid ) {
270 + if ( MainWP_Utility::ctype_digit( $websiteid ) ) {
271 + $table_group = esc_sql( $this->table_name( 'group' ) );
272 + $table_wp_group = esc_sql( $this->table_name( 'wp_group' ) );
273 + return $this->wpdb->get_results(
274 + $this->wpdb->prepare(
275 + "SELECT * FROM `{$table_group}` gr JOIN `{$table_wp_group}` wpgr ON gr.id = wpgr.groupid WHERE wpgr.wpid = %d ORDER BY name",
276 + $websiteid
277 + ),
278 + OBJECT_K
279 + );
280 + }
282 281
283 - /**
284 - * Current user global.
285 - *
286 - * @global string
287 - */
288 - global $current_user;
282 + return null;
283 + }
289 284
290 - $userid = $current_user->ID;
291 - }
285 + /**
286 + * Medthod get_groups_and_count()
287 + *
288 + * Get groups and count.
289 + *
290 + * @param null $userid Current user ID.
291 + * @param bool $for_manager Default: false.
292 + *
293 + * @return object|null Database query result for groups and count or null on failure.
294 + *
295 + * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
296 + */
297 + public function get_groups_and_count( $userid = null, $for_manager = false ) {
298 + if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
292 299
293 - $where = ' WHERE 1 ';
294 - $where .= $this->get_sql_where_allow_groups( 'g' );
300 + /**
301 + * Current user global.
302 + *
303 + * @global string
304 + */
305 + global $current_user;
295 306
296 - if ( null !== $userid ) {
297 - $where .= ' AND g.userid = ' . intval( $userid );
298 - }
299 - if ( ! $enableOfflineSites ) {
300 - $where .= ' AND wp_sync.sync_errors = ""';
301 - }
307 + $userid = $current_user->ID;
308 + }
302 309
303 - return $this->wpdb->get_results( 'SELECT DISTINCT(g.id), g.name, count(wp.wpid) FROM ' . $this->table_name( 'group' ) . ' g JOIN ' . $this->table_name( 'wp_group' ) . ' wp ON g.id = wp.groupid JOIN ' . $this->table_name( 'wp' ) . ' wpsite ON wp.wpid = wpsite.id JOIN ' . $this->table_name( 'wp_sync' ) . ' wp_sync ON wp.wpid = wp_sync.wpid ' . $where . ' GROUP BY g.id HAVING count(wp.wpid) > 0 ORDER BY g.name', OBJECT_K );
304 - }
310 + $where = '';
305 311
306 - /**
307 - * Method get_sql_log()
308 - *
309 - * Get sql log.
310 - *
311 - * @return string sql query.
312 - */
313 - public function get_sql_log() {
314 - return 'SELECT log.*
315 - FROM ' . $this->table_name( 'action_log' ) . ' log
316 - WHERE 1 LIMIT 0, 300 ';
317 - }
312 + if ( ! empty( $userid ) ) {
313 + $where = ' AND gr.userid = ' . intval( $userid );
314 + }
318 315
319 - /**
320 - * Method insert_action_log()
321 - *
322 - * Insert action log.
323 - *
324 - * @param array $data log data.
325 - *
326 - * @return void
327 - */
328 - public function insert_action_log( $data ) {
329 - $this->wpdb->insert( $this->table_name( 'action_log' ), $data );
330 - }
316 + if ( ! $for_manager ) {
317 + $where .= $this->get_sql_where_allow_groups( 'gr' );
318 + }
331 319
332 - /**
333 - * Method delete_action_log()
334 - *
335 - * Delete action log.
336 - *
337 - * @param int $days number days.
338 - *
339 - * @return void
340 - */
341 - public function delete_action_log( $days = false ) {
342 - $where = '';
343 - if ( ! empty( $days ) ) {
344 - $where .= ' AND log_timestamp < ' . ( time() - $days * DAY_IN_SECONDS );
345 - }
346 - $this->wpdb->query( 'DELETE FROM `' . $this->table_name( 'action_log' ) . '` WHERE 1 ' . $where );
347 - }
320 + $table_group = esc_sql( $this->table_name( 'group' ) );
321 + $table_wp_group = esc_sql( $this->table_name( 'wp_group' ) );
322 + $where = esc_sql( $where );
323 + return $this->wpdb->get_results( "SELECT gr.*, COUNT(DISTINCT(wpgr.wpid)) as nrsites FROM `{$table_group}` gr LEFT JOIN `{$table_wp_group}` wpgr ON gr.id = wpgr.groupid WHERE 1 {$where} GROUP BY gr.id ORDER BY gr.name", OBJECT_K );
324 + }
348 325
349 - /**
350 - * Method insert_or_update_request_log()
351 - *
352 - * Insert or update request log.
353 - *
354 - * @param mixed $wpid WordPress ID.
355 - * @param mixed $ip IP address.
356 - * @param mixed $start Start time.
357 - * @param mixed $stop Stop Time.
358 - *
359 - * @return void
360 - */
361 - public function insert_or_update_request_log( $wpid, $ip, $start, $stop ) {
362 - $updateValues = array();
363 - if ( ! empty( $ip ) ) {
364 - $updateValues['ip'] = $ip;
365 - }
366 - if ( ! empty( $start ) ) {
367 - $updateValues['micro_timestamp_start'] = $start;
368 - }
369 - if ( ! empty( $stop ) ) {
370 - $updateValues['micro_timestamp_stop'] = $stop;
371 - }
372 326
373 - $var = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT id FROM ' . $this->table_name( 'request_log' ) . ' WHERE wpid = %d ', $wpid ) );
374 - if ( null !== $var ) {
375 - $this->wpdb->update( $this->table_name( 'request_log' ), $updateValues, array( 'wpid' => $wpid ) );
376 - } else {
377 - $updateValues['wpid'] = $wpid;
378 - $this->wpdb->insert( $this->table_name( 'request_log' ), $updateValues );
379 - }
380 - }
327 + /**
328 + * Method get_tags()
329 + *
330 + * Get tags (groups) with optional filtering and pagination.
331 + *
332 + * @since 5.1.1
333 + *
334 + * @param array $params Optional parameters for filtering and pagination.
335 + * - 's' (string) Search term for tag name or ID.
336 + * - 'exclude' (array) Tag IDs to exclude.
337 + * - 'include' (array) Tag IDs to include.
338 + * - 'page' (int) Page number for pagination.
339 + * - 'per_page' (int) Items per page for pagination.
340 + * - 'with_sites_ids' (bool) Include associated site IDs.
341 + * - 'count' (bool) Return count only instead of results.
342 + *
343 + * @return object[]|int When $params['count'] is true, returns an integer count of matching tags.
344 + * Otherwise, returns an array of tag objects keyed by ID, or empty array on failure.
345 + */
346 + public function get_tags( $params = array() ) { //phpcs:ignore -- NOSONAR - complex.
381 347
382 - /**
383 - * Method close_open_requests()
384 - *
385 - * Close open request.
386 - *
387 - * @return void
388 - */
389 - public function close_open_requests() {
390 - // Close requests open longer then 7 seconds.. something is wrong here.
391 - $this->wpdb->query( 'UPDATE ' . $this->table_name( 'request_log' ) . ' SET micro_timestamp_stop = micro_timestamp_start WHERE micro_timestamp_stop < micro_timestamp_start and ' . microtime( true ) . ' - micro_timestamp_start > 7' );
392 - }
348 + $s = '';
349 + $exclude = array();
350 + $include = array();
351 + $limit = '';
393 352
394 - /**
395 - * Method get_nrof_open_requests()
396 - *
397 - * Get number of requests.
398 - *
399 - * @param null $ip IP Address.
400 - *
401 - * @return (string|null) Database query result for number of requests or null on failure.
402 - */
403 - public function get_nrof_open_requests( $ip = null ) {
404 - if ( null === $ip ) {
405 - return $this->wpdb->get_var( 'select count(id) from ' . $this->table_name( 'request_log' ) . ' where micro_timestamp_stop < micro_timestamp_start' );
406 - }
353 + $where = '';
354 + $select = '';
407 355
408 - return $this->wpdb->get_var( 'select count(id) from ' . $this->table_name( 'request_log' ) . ' where micro_timestamp_stop < micro_timestamp_start and ip = "' . esc_sql( $ip ) . '"' );
409 - }
356 + if ( $params && is_array( $params ) ) {
357 + $s = isset( $params['s'] ) ? $params['s'] : '';
358 + $exclude = isset( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
359 + $include = isset( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array();
360 + $page = isset( $params['page'] ) ? intval( $params['page'] ) : false;
361 + $per_page = isset( $params['per_page'] ) ? intval( $params['per_page'] ) : false;
362 + $with_sites_ids = isset( $params['with_sites_ids'] ) && $params['with_sites_ids'] ? true : false;
363 + $count_only = ! empty( $params['count'] );
410 364
411 - /**
412 - * Method get_last_request_timestamp()
413 - *
414 - * Get timestamp of last request sent.
415 - *
416 - * @param null $ip Child Site IP address, default: null.
417 - *
418 - * @return (int|null) Database query result for timestamp of last request sent or null on failure.
419 - */
420 - public function get_last_request_timestamp( $ip = null ) {
421 - if ( null === $ip ) {
422 - return $this->wpdb->get_var( 'select micro_timestamp_start from ' . $this->table_name( 'request_log' ) . ' order by micro_timestamp_start desc limit 1' );
423 - }
365 + if ( $with_sites_ids ) {
366 + $select .= ', wp_tagview.* ';
367 + }
424 368
425 - return $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT micro_timestamp_start FROM ' . $this->table_name( 'request_log' ) . ' WHERE ip = %s order by micro_timestamp_start desc limit 1', esc_sql( $ip ) ) );
426 - }
369 + if ( ! empty( $s ) ) {
370 + $s = trim( $s );
371 + $like_pattern = '%' . $this->wpdb->esc_like( $s ) . '%';
372 + $where .= $this->wpdb->prepare(
373 + ' AND ( gr.name LIKE %s OR gr.id LIKE %s ) ',
374 + $like_pattern,
375 + $like_pattern
376 + );
377 + }
427 378
428 - /**
429 - * Method update_group_site()
430 - *
431 - * @param mixed $groupId Group ID.
432 - * @param mixed $websiteId Child Site ID.
433 - *
434 - * @return void
435 - */
436 - public function update_group_site( $groupId, $websiteId ) {
437 - $this->wpdb->insert(
438 - $this->table_name( 'wp_group' ),
439 - array(
440 - 'wpid' => $websiteId,
441 - 'groupid' => $groupId,
442 - )
443 - );
444 - }
379 + if ( ! empty( $exclude ) ) {
380 + $where .= ' AND gr.id NOT IN (' . implode( ',', $exclude ) . ') ';
381 + }
445 382
446 - /**
447 - * Method clear_group()
448 - *
449 - * Clear sites in group.
450 - *
451 - * @param mixed $groupId ID of group.
452 - */
453 - public function clear_group( $groupId ) {
454 - $this->wpdb->query( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE groupid=' . $groupId );
455 - }
383 + if ( ! empty( $include ) ) {
384 + $where .= ' AND gr.id IN (' . implode( ',', $include ) . ') ';
385 + }
456 386
387 + $gr_table = esc_sql( $this->table_name( 'group' ) );
457 388
458 - /**
459 - * Method add_group()
460 - *
461 - * Add group.
462 - *
463 - * @param mixed $userid Current User ID.
464 - * @param mixed $name Name of group to add.
465 - * @param mixed $color Color of group to add.
466 - *
467 - * @return boolean true
468 - *
469 - * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
470 - * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
471 - */
472 - public function add_group( $userid, $name, $color = '' ) {
473 - if ( MainWP_Utility::ctype_digit( $userid ) ) {
474 - if ( $this->wpdb->insert(
475 - $this->table_name( 'group' ),
476 - array(
477 - 'userid' => $userid,
478 - 'name' => $this->escape( $name ),
479 - 'color' => $this->escape( $color ),
480 - )
481 - )
482 - ) {
389 + // Return count only if requested.
390 + if ( $count_only ) {
391 + return (int) $this->wpdb->get_var( 'SELECT COUNT(*) FROM ' . $gr_table . ' gr WHERE 1 ' . $where ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $where is built with wpdb->prepare() and escaped table name, safe to use directly.
392 + }
483 393
484 - $groupId = $this->wpdb->insert_id;
394 + if ( ! empty( $page ) && ! empty( $per_page ) ) {
395 + $limit = $this->wpdb->prepare( ' LIMIT %d, %d', ( $page - 1 ) * $per_page, $per_page );
396 + }
485 397
486 - $group = $this->get_group_by_id( $groupId );
398 + $join = '';
487 399
488 - /**
489 - * Fires after a new sites tag has been created.
490 - *
491 - * @param object $group group created.
492 - * @param string group action.
493 - */
494 - do_action( 'mainwp_site_tag_action', $group, 'created' );
400 + if ( $with_sites_ids ) {
401 + $join = ' JOIN ' . $this->get_tag_view() . ' wp_tagview ON gr.id = wp_tagview.id ';
402 + }
403 + }
404 + $table_group = esc_sql( $this->table_name( 'group' ) );
405 + $table_wp_group = esc_sql( $this->table_name( 'wp_group' ) );
406 + $select_sql = $select ? $select : '';
407 + $join_sql = $join ? $join : '';
408 + $where_sql = $where ? $where : '';
409 + $limit_sql = $limit ? $limit : '';
410 + $query = "SELECT gr.* {$select_sql}, COUNT(DISTINCT(wpgr.wpid)) as count_sites FROM `{$table_group}` gr LEFT JOIN `{$table_wp_group}` wpgr ON gr.id = wpgr.groupid {$join_sql} WHERE 1 {$where_sql} GROUP BY gr.id ORDER BY gr.name {$limit_sql}";
411 + return $this->wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $query is built with proper escaping and parameterization above.
412 + }
495 413
496 - return $groupId;
497 - }
498 - }
414 + /**
415 + * Method get_tag_view().
416 + *
417 + * @return string tag view.
418 + */
419 + public function get_tag_view() {
420 + $view = "( SELECT intgr.id, ( SELECT GROUP_CONCAT(wp.id ORDER BY wp.id SEPARATOR ',') FROM `" . $this->table_name( 'wp' ) . '` wp ';
421 + $view .= ' LEFT JOIN `' . $this->table_name( 'wp_group' ) . '` wpgr ON wp.id = wpgr.wpid WHERE wpgr.groupid = intgr.id ) as sites_ids ';
422 + $view .= ' FROM `' . $this->table_name( 'group' ) . '` intgr )';
423 + return $view;
424 + }
499 425
500 - return false;
501 - }
502 426
503 - /**
504 - * Method remove_group()
505 - *
506 - * Remove group.
507 - *
508 - * @param mixed $groupid Group ID.
509 - *
510 - * @return int|boolean Group that was deleted or false on failure.
511 - *
512 - * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
513 - */
514 - public function remove_group( $groupid ) {
515 - if ( MainWP_Utility::ctype_digit( $groupid ) ) {
516 - $nr = $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'group' ) . ' WHERE id=%d', $groupid ) );
517 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE groupid=%d', $groupid ) );
427 + /**
428 + * Method get_not_empty_groups()
429 + *
430 + * Get non-empty groups.
431 + *
432 + * @param mixed $userid Current user ID.
433 + * @param bool $enableOfflineSites Include offline sites? Default: true.
434 + *
435 + * @return object|null Database query result for non-empty groups or null on failure.
436 + *
437 + * @uses \MainWP\Dashboard\MainWP_System::is_multi_user()
438 + */
439 + public function get_not_empty_groups( $userid = null, $enableOfflineSites = true ) { //phpcs:ignore --NOSONAR -- complex.
440 + if ( ( null === $userid ) && MainWP_System::instance()->is_multi_user() ) {
518 441
519 - return $nr;
520 - }
442 + /**
443 + * Current user global.
444 + *
445 + * @global string
446 + */
447 + global $current_user;
521 448
522 - return false;
523 - }
449 + $userid = $current_user->ID;
450 + }
524 451
525 - /**
526 - * Method update_note()
527 - *
528 - * Update Note.
529 - *
530 - * @param mixed $websiteid Child Site ID.
531 - * @param mixed $note Note data.
532 - *
533 - * @return void
534 - */
535 - public function update_note( $websiteid, $note ) {
536 - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'wp' ) . ' SET note= %s WHERE id=%d', $this->escape( $note ), $websiteid ) );
537 - }
452 + $table_group = esc_sql( $this->table_name( 'group' ) );
453 + $table_wp_group = esc_sql( $this->table_name( 'wp_group' ) );
454 + $table_wp = esc_sql( $this->table_name( 'wp' ) );
455 + $table_wp_sync = esc_sql( $this->table_name( 'wp_sync' ) );
538 456
539 - /**
540 - * Method update_group()
541 - *
542 - * Update group.
543 - *
544 - * @param mixed $groupid Group ID.
545 - * @param mixed $groupname Group Name.
546 - * @param string $groupcolor Group Color.
547 - *
548 - * @return boolean true|false.
549 - *
550 - * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
551 - */
552 - public function update_group( $groupid, $groupname, $groupcolor ) {
553 - if ( MainWP_Utility::ctype_digit( $groupid ) ) {
554 - // update groupname.
555 - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'group' ) . ' SET name=%s, color=%s WHERE id=%d', $this->escape( $groupname ), $this->escape( $groupcolor ), $groupid ) );
457 + $sql = "SELECT DISTINCT(g.id), g.name, count(wp.wpid) FROM `{$table_group}` g JOIN `{$table_wp_group}` wp ON g.id = wp.groupid JOIN `{$table_wp}` wpsite ON wp.wpid = wpsite.id JOIN `{$table_wp_sync}` wp_sync ON wp.wpid = wp_sync.wpid WHERE 1 = 1";
458 + $params = array();
556 459
557 - return true;
558 - }
460 + $staging_group = get_option( 'mainwp_stagingsites_group_id' );
461 + if ( $staging_group ) {
462 + $sql .= ' AND g.id <> %d';
463 + $params[] = absint( $staging_group );
464 + }
559 465
560 - return false;
561 - }
466 + $allowed_groups = apply_filters( 'mainwp_currentuserallowedaccessgroups', 'all' );
467 + if ( 'all' !== $allowed_groups ) {
468 + if ( is_array( $allowed_groups ) && ! empty( $allowed_groups ) ) {
469 + $allowed_groups = array_filter(
470 + $allowed_groups,
471 + function ( $e ) {
472 + return is_numeric( $e ) ? true : false;
473 + }
474 + );
562 475
563 - /**
564 - * Method get_user_notification_email()
565 - *
566 - * Get user notification email.
567 - *
568 - * @param mixed $userid Current user ID.
569 - *
570 - * @return string $user_email User email address.
571 - *
572 - * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
573 - */
574 - public function get_user_notification_email( $userid = 0 ) {
575 - $theUserId = $userid;
576 - if ( MainWP_System::instance()->is_single_user() ) {
577 - $theUserId = 0;
578 - }
579 - $user_email = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT user_email FROM ' . $this->table_name( 'users' ) . ' WHERE userid = %d', $theUserId ) );
476 + if ( ! empty( $allowed_groups ) ) {
477 + $placeholders = implode( ',', array_fill( 0, count( $allowed_groups ), '%d' ) );
478 + $sql .= ' AND g.id IN (' . $placeholders . ')';
479 + $params = array_merge( $params, array_map( 'intval', $allowed_groups ) );
480 + }
481 + } else {
482 + $sql .= ' AND 0';
483 + }
484 + }
580 485
581 - if ( null === $user_email || empty( $user_email ) ) {
582 - $user_email = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT user_email FROM ' . $this->wpdb->prefix . 'users WHERE id = %d', $userid ) );
583 - }
486 + if ( null !== $userid ) {
487 + $sql .= ' AND g.userid = %d';
488 + $params[] = intval( $userid );
489 + }
584 490
585 - return $user_email;
586 - }
491 + if ( ! $enableOfflineSites ) {
492 + $sql .= " AND wp_sync.sync_errors = ''";
493 + }
587 494
588 - /**
589 - * Method get_user_extension()
590 - *
591 - * Get user extension.
592 - *
593 - * @return boolean|int false|get_user_extension_by_user_id()
594 - *
595 - * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
596 - */
597 - public function get_user_extension() {
495 + $sql .= ' GROUP BY g.id HAVING count(wp.wpid) > 0 ORDER BY g.name';
598 496
599 - /**
600 - * Current user global.
601 - *
602 - * @global string
603 - */
604 - global $current_user;
497 + if ( ! empty( $params ) ) {
498 + $sql = $this->wpdb->prepare( $sql, ...$params );
499 + }
605 500
606 - if ( empty( $current_user ) ) {
607 - if ( MainWP_System::instance()->is_single_user() ) {
608 - $userid = 0;
609 - } else {
610 - return false;
611 - }
612 - } else {
613 - $userid = $current_user->ID;
614 - }
501 + return $this->wpdb->get_results( $sql, OBJECT_K ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is prepared via $wpdb->prepare() with all dynamic values properly parameterized.
502 + }
615 503
616 - return $this->get_user_extension_by_user_id( $userid );
617 - }
504 + /**
505 + * Method get_sql_log()
506 + *
507 + * Get sql log.
508 + *
509 + * @param int $paged paged.
510 + * @param int $order order.
511 + * @param array $params params.
512 + *
513 + * @return string sql query.
514 + */
515 + public function get_sql_log( $paged = 0, $order = '', $params = array() ) {
618 516
619 - /**
620 - * Method get_user_extension_by_user_id()
621 - *
622 - * Get user extension by user id.
623 - *
624 - * @param mixed $userid Current user ID.
625 - *
626 - * @return object $row User extension.
627 - *
628 - * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
629 - */
630 - public function get_user_extension_by_user_id( $userid ) {
631 - if ( MainWP_System::instance()->is_single_user() ) {
632 - $userid = 0;
633 - }
517 + $count_only = ! empty( $params['count'] ) ? true : false;
518 + $limit = ! empty( $params['limit'] ) ? intval( $params['limit'] ) : 500;
634 519
635 - $row = $this->wpdb->get_row( 'SELECT * FROM ' . $this->table_name( 'users' ) . ' WHERE userid= ' . $userid, OBJECT );
636 - if ( null === $row ) {
637 - $this->create_user_extension( $userid );
638 - $row = $this->wpdb->get_row( 'SELECT * FROM ' . $this->table_name( 'users' ) . ' WHERE userid= ' . $userid, OBJECT );
639 - }
520 + $last_hours = ! empty( $params['hour'] ) ? intval( $params['hour'] ) : 0;
640 521
641 - return $row;
642 - }
522 + $order = strtoupper( $order );
643 523
644 - /**
645 - * Method create_user_extension()
646 - *
647 - * Create user extension
648 - *
649 - * @param mixed $userId Current user ID.
650 - *
651 - * @return void
652 - */
653 - protected function create_user_extension( $userId ) {
654 - $fields = array(
655 - 'userid' => $userId,
656 - 'user_email' => '',
657 - 'ignored_plugins' => '',
658 - 'trusted_plugins' => '',
659 - 'trusted_plugins_notes' => '',
660 - 'ignored_themes' => '',
661 - 'trusted_themes' => '',
662 - 'trusted_themes_notes' => '',
663 - 'pluginDir' => '',
664 - );
524 + $order = 'DESC' === $order || 'ASC' === $order ? $order : 'DESC';
665 525
666 - $this->wpdb->insert( $this->table_name( 'users' ), $fields );
667 - }
526 + $start = ! empty( $paged ) ? absint( $paged * $limit ) : 0;
668 527
669 - /**
670 - * Method update_user_extension()
671 - *
672 - * Update user extension.
673 - *
674 - * @param mixed $userExtension User extention to update.
675 - *
676 - * @return object $row User extension.
677 - *
678 - * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
679 - */
680 - public function update_user_extension( $userExtension ) {
528 + if ( $count_only ) {
529 + return 'SELECT count(*)
530 + FROM `' . $this->table_name( 'action_log' ) . '` log
531 + WHERE 1 ';
532 + }
681 533
682 - if ( is_object( $userExtension ) ) {
683 - $userid = $userExtension->userid;
684 - } elseif ( is_array( $userExtension ) ) {
685 - $userid = $userExtension['userid'];
686 - } else {
687 - $userid = null;
688 - }
534 + if ( ! empty( $last_hours ) ) {
535 + return 'SELECT log.*
536 + FROM `' . $this->table_name( 'action_log' ) . '` log
537 + WHERE ' . $this->wpdb->prepare( ' log_timestamp > %d ', time() - $last_hours * HOUR_IN_SECONDS ) .
538 + ' ORDER BY log_timestamp ' . $this->escape( $order );
539 + }
689 540
690 - if ( null === $userid ) {
691 - if ( MainWP_System::instance()->is_single_user() ) {
692 - $userid = '0';
693 - } else {
541 + return 'SELECT log.*
542 + FROM `' . $this->table_name( 'action_log' ) . '` log
543 + WHERE 1 ORDER BY ' .
544 + $this->wpdb->prepare( 'log_timestamp ' . $this->escape( $order ) . ' LIMIT %d, %d', $start, $limit );
545 + }
694 546
695 - /**
696 - * Current user global.
697 - *
698 - * @global string
699 - */
700 - global $current_user;
547 + /**
548 + * Method insert_action_log()
549 + *
550 + * Insert action log.
551 + *
552 + * @param array $data log data.
553 + *
554 + * @return void
555 + */
556 + public function insert_action_log( $data ) {
557 + $this->wpdb->insert( $this->table_name( 'action_log' ), $data );
558 + }
701 559
702 - $userid = $current_user->ID;
703 - }
704 - }
705 - $row = $this->wpdb->get_row( 'SELECT * FROM ' . $this->table_name( 'users' ) . ' WHERE userid= ' . $userid, OBJECT );
706 - if ( null === $row ) {
707 - $this->create_user_extension( $userid );
708 - }
560 + /**
561 + * Method delete_action_log()
562 + *
563 + * Delete action log.
564 + *
565 + * @param int $days number days.
566 + *
567 + * @return void
568 + */
569 + public function delete_action_log( $days = false ) {
570 + $where = '';
571 + if ( ! empty( $days ) ) {
572 + $where .= ' AND log_timestamp < ' . ( time() - $days * DAY_IN_SECONDS );
573 + }
574 + $table_action_log = esc_sql( $this->table_name( 'action_log' ) );
575 + $where = esc_sql( $where );
576 + $this->wpdb->query( "DELETE FROM `{$table_action_log}` WHERE 1 {$where}" );
577 + }
709 578
710 - $fields = array();
711 - foreach ( $userExtension as $field => $value ) {
712 - if ( $value != $row->$field ) { //phpcs:ignore -- to valid.
713 - $fields[ $field ] = $value;
714 - }
715 - }
579 + /**
580 + * Method insert_or_update_request_log()
581 + *
582 + * Insert or update request log.
583 + *
584 + * @param mixed $wpid WordPress ID.
585 + * @param mixed $ip IP address.
586 + * @param mixed $start Start time.
587 + * @param mixed $stop Stop Time.
588 + *
589 + * @return void
590 + */
591 + public function insert_or_update_request_log( $wpid, $ip, $start, $stop ) {
592 + $updateValues = array();
593 + if ( ! empty( $ip ) ) {
594 + $updateValues['ip'] = $ip;
595 + }
596 + if ( ! empty( $start ) ) {
597 + $updateValues['micro_timestamp_start'] = $start;
598 + }
599 + if ( ! empty( $stop ) ) {
600 + $updateValues['micro_timestamp_stop'] = $stop;
601 + }
716 602
717 - if ( 0 < count( $fields ) ) {
718 - $this->wpdb->update( $this->table_name( 'users' ), $fields, array( 'userid' => $userid ) );
719 - }
603 + $table_request_log = esc_sql( $this->table_name( 'request_log' ) );
604 + $var = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT id FROM `{$table_request_log}` WHERE wpid = %d ", $wpid ) );
605 + if ( null !== $var ) {
606 + $this->wpdb->update( $this->table_name( 'request_log' ), $updateValues, array( 'wpid' => $wpid ) );
607 + } else {
608 + $updateValues['wpid'] = $wpid;
609 + $this->wpdb->insert( $this->table_name( 'request_log' ), $updateValues );
610 + }
611 + }
720 612
721 - $row = $this->wpdb->get_row( 'SELECT * FROM ' . $this->table_name( 'users' ) . ' WHERE userid= ' . $userid, OBJECT );
613 + /**
614 + * Method close_open_requests()
615 + *
616 + * Close open request.
617 + *
618 + * @return void
619 + */
620 + public function close_open_requests() {
621 + $table_request_log = esc_sql( $this->table_name( 'request_log' ) );
622 + $microtime_value = esc_sql( microtime( true ) );
623 + $this->wpdb->query( "UPDATE `{$table_request_log}` SET micro_timestamp_stop = micro_timestamp_start WHERE micro_timestamp_stop < micro_timestamp_start and {$microtime_value} - micro_timestamp_start > 7" );
624 + }
722 625
723 - return $row;
724 - }
626 + /**
627 + * Method get_nrof_open_requests()
628 + *
629 + * Get number of requests.
630 + *
631 + * @param null $ip IP Address.
632 + *
633 + * @return (string|null) Database query result for number of requests or null on failure.
634 + */
635 + public function get_nrof_open_requests( $ip = null ) {
636 + $table_request_log = esc_sql( $this->table_name( 'request_log' ) );
637 + if ( null === $ip ) {
638 + return $this->wpdb->get_var( "select count(id) from `{$table_request_log}` where micro_timestamp_stop < micro_timestamp_start" ); // phpcs:ignore -- NOSONAR - table name escaped.
639 + }
725 640
641 + return $this->wpdb->get_var(
642 + $this->wpdb->prepare(
643 + "SELECT COUNT(id) FROM `{$table_request_log}` WHERE micro_timestamp_stop < micro_timestamp_start AND ip = %s", // phpcs:ignore -- NOSONAR - table name escaped.
644 + $ip
645 + )
646 + );
647 + }
726 648
727 - /**
728 - * Method rest_api_update_website().
729 - *
730 - * Rest API update website.
731 - *
732 - * @param int $websiteid website ID.
733 - * @param array $data Update fields array.
734 - * 'http_user'.
735 - * 'http_pass'.
736 - * 'name'.
737 - * 'admin'.
738 - * 'sslversion'.
739 - * 'uniqueid'.
740 - * 'verify'.
741 - * 'protocol'.
742 - * 'disablechecking'.
743 - * 'checkinterval'.
744 - * 'disablehealthchecking'.
745 - * 'healththreshold'.
746 - * 'groupids'.
747 - * 'automatic_update'.
748 - * 'backup_before_upgrade'.
749 - * 'force_use_ipv4'.
750 - * 'ignore_core_updates'.
751 - * 'ignore_plugin_updates'.
752 - * 'ignore_theme_updates'.
753 - * 'monitoring_emails'.
754 - *
755 - * @return bool true|false.
756 - */
757 - public function rest_api_update_website( $websiteid, $data ) { // phpcs:ignore -- complex function.
649 + /**
650 + * Method get_last_request_timestamp()
651 + *
652 + * Get timestamp of last request sent.
653 + *
654 + * @param null $ip Child Site IP address, default: null.
655 + *
656 + * @return (int|null) Database query result for timestamp of last request sent or null on failure.
657 + */
658 + public function get_last_request_timestamp( $ip = null ) {
659 + $table_request_log = esc_sql( $this->table_name( 'request_log' ) );
660 + if ( null === $ip ) {
661 + return $this->wpdb->get_var( "select micro_timestamp_start from `{$table_request_log}` order by micro_timestamp_start desc limit 1" );
662 + }
758 663
759 - $website = MainWP_DB::instance()->get_website_by_id( $websiteid );
760 - if ( empty( $website ) ) {
761 - return false;
762 - }
664 + return $this->wpdb->get_var( $this->wpdb->prepare( "SELECT micro_timestamp_start FROM `{$table_request_log}` WHERE ip = %s order by micro_timestamp_start desc limit 1", esc_sql( $ip ) ) );
665 + }
763 666
764 - $map_fields = array(
765 - 'http_user' => 'http_user',
766 - 'http_pass' => 'http_pass',
767 - 'name' => 'name',
768 - 'adminname' => 'admin',
769 - 'ssl_version' => 'sslversion',
770 - 'uniqueId' => 'uniqueid',
771 - );
667 + /**
668 + * Method update_group_site()
669 + *
670 + * @param mixed $groupId Group ID.
671 + * @param mixed $websiteId Child Site ID.
672 + *
673 + * @return void
674 + */
675 + public function update_group_site( $groupId, $websiteId ) {
676 + $this->wpdb->insert(
677 + $this->table_name( 'wp_group' ),
678 + array(
679 + 'wpid' => $websiteId,
680 + 'groupid' => $groupId,
681 + )
682 + );
683 + }
772 684
773 - $sql_set = '';
685 + /**
686 + * Method clear_group()
687 + *
688 + * Clear sites in group.
689 + *
690 + * @param mixed $groupId ID of group.
691 + * @param mixed $exclude_wpids Empty or array of wp ids to exclude.
692 + */
693 + public function clear_group( $groupId, $exclude_wpids = array() ) {
694 + $this->wpdb->query( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE groupid=' . $groupId . ( ! empty( $exclude_wpids ) && is_array( $exclude_wpids ) ? ' AND wpid NOT IN (' . implode( ',', array_map( 'intval', $exclude_wpids ) ) . ')' : '' ) );
695 + }
774 696
775 - foreach ( $map_fields as $field => $name ) {
776 - if ( isset( $data[ $name ] ) && empty( ! $data[ $name ] ) ) {
777 - $sql_set .= ' `' . $this->escape( $field ) . '` = "' . $this->escape( $data[ $name ] ) . '",';
778 - }
779 - }
780 697
781 - if ( isset( $data['verify'] ) ) {
782 - $verify = intval( $data['verify'] );
783 - $sql_set .= ' verify_certificate = "' . $this->escape( $verify ) . '",';
784 - }
698 + /**
699 + * Method add_group()
700 + *
701 + * Add group.
702 + *
703 + * @param mixed $userid Current User ID.
704 + * @param mixed $name Name of group to add.
705 + * @param mixed $color Color of group to add.
706 + *
707 + * @return boolean true
708 + *
709 + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
710 + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
711 + */
712 + public function add_group( $userid, $name, $color = '' ) {
713 + if ( MainWP_Utility::ctype_digit( $userid ) && $this->wpdb->insert(
714 + $this->table_name( 'group' ),
715 + array(
716 + 'userid' => $userid,
717 + 'name' => $this->escape( $name ),
718 + 'color' => $this->escape( $color ),
719 + )
720 + ) ) {
785 721
786 - if ( isset( $data['protocol'] ) && ( 'http' === $data['protocol'] || 'https' === $data['protocol'] ) ) {
787 - $url = $data['protocol'] . '://' . MainWP_Utility::remove_http_prefix( $website->url, true );
788 - $sql_set .= ' url = "' . $this->escape( $url ) . '",';
789 - }
722 + $groupId = $this->wpdb->insert_id;
790 723
791 - if ( isset( $data['disablechecking'] ) ) {
792 - $sql_set .= ' disable_status_check= "' . ( $data['disablechecking'] ? 1 : 0 ) . '",';
793 - }
724 + $group = $this->get_group_by_id( $groupId );
794 725
795 - if ( isset( $data['checkinterval'] ) ) {
796 - $sql_set .= ' status_check_interval= "' . intval( $data['checkinterval'] ) . '",';
797 - }
726 + /**
727 + * Fires after a new sites tag has been created.
728 + *
729 + * @param object $group group created.
730 + * @param string group action.
731 + */
732 + do_action( 'mainwp_site_tag_action', $group, 'created' );
798 733
799 - if ( isset( $data['disablehealthchecking'] ) ) {
800 - $sql_set .= ' disable_health_check = "' . ( $data['disablehealthchecking'] ? 1 : 0 ) . '",';
801 - }
734 + return $groupId;
735 + }
802 736
803 - if ( isset( $data['healththreshold'] ) ) {
804 - $sql_set .= ' health_threshold = "' . intval( $data['healththreshold'] ) . '",';
805 - }
737 + return false;
738 + }
806 739
807 - if ( isset( $data['suspended'] ) ) {
808 - $sql_set .= ' suspended = "' . ( 1 === intval( $data['suspended'] ) ? 1 : 0 ) . '",';
809 - }
740 + /**
741 + * Method add_tag()
742 + *
743 + * Add Group.
744 + *
745 + * @param array $params params data.
746 + */
747 + public function add_tag( $params = array() ) {
748 + /**
749 + * Current user global.
750 + *
751 + * @global string
752 + */
753 + global $current_user;
754 + //phpcs:disable WordPress.Security.NonceVerification.Missing
755 + $groupId = isset( $params['id'] ) ? intval( $params['id'] ) : 0;
756 + $newName = isset( $params['name'] ) ? sanitize_text_field( wp_unslash( $params['name'] ) ) : '';
757 + $newColor = null;
810 758
811 - if ( ! empty( $sql_set ) ) {
812 - $sql_set = rtrim( $sql_set, ',' );
813 - $this->wpdb->query( $this->wpdb->prepare( 'UPDATE ' . $this->table_name( 'wp' ) . ' SET ' . $sql_set . ' WHERE id=%d', $websiteid ) );
814 - }
759 + if ( isset( $params['color'] ) ) {
760 + $newColor = sanitize_hex_color( wp_unslash( $params['color'] ) );
761 + }
762 + //phpcs:enable WordPress.Security.NonceVerification.Missing
815 763
816 - $groupids = array();
817 - if ( isset( $data['groupids'] ) && ! empty( $data['groupids'] ) ) {
818 - $groupids = explode( ',', sanitize_text_field( wp_unslash( $data['groupids'] ) ) );
819 - }
764 + if ( ! empty( $groupId ) ) {
765 + $table_group = esc_sql( $this->table_name( 'group' ) );
766 + if ( null !== $newColor ) {
767 + $this->wpdb->query( $this->wpdb->prepare( "UPDATE `{$table_group}` SET name=%s, color=%s WHERE id=%d", $newName, $newColor, $groupId ) );
768 + } else {
769 + $this->wpdb->query( $this->wpdb->prepare( "UPDATE `{$table_group}` SET name=%s WHERE id=%d", $newName, $groupId ) );
770 + }
771 + return $this->get_group_by_id( $groupId );
772 + } elseif ( ! empty( $newName ) ) {
773 + $groupId = $this->add_group( $current_user->ID, MainWP_Manage_Groups::check_group_name( $newName ), $newColor );
820 774
821 - if ( ! empty( $groupids ) ) {
822 - // remove groups.
823 - $this->wpdb->query( $this->wpdb->prepare( 'DELETE FROM ' . $this->table_name( 'wp_group' ) . ' WHERE wpid=%d', $websiteid ) );
775 + /**
776 + * New Group Added
777 + *
778 + * Fires after a new sites group has been created.
779 + *
780 + * @param int $groupId Group ID.
781 + */
782 + do_action( 'mainwp_added_new_group', $groupId );
783 + return $this->get_group_by_id( $groupId );
784 + }
785 + return false;
786 + }
824 787
825 - // update groups.
826 - foreach ( $groupids as $groupid ) {
827 - $this->wpdb->insert(
828 - $this->table_name( 'wp_group' ),
829 - array(
830 - 'wpid' => $websiteid,
831 - 'groupid' => $groupid,
832 - )
833 - );
834 - }
835 - }
788 + /**
789 + * Method remove_group()
790 + *
791 + * Remove group.
792 + *
793 + * @param mixed $groupid Group ID.
794 + *
795 + * @return int|boolean Group that was deleted or false on failure.
796 + *
797 + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
798 + */
799 + public function remove_group( $groupid ) {
800 + if ( MainWP_Utility::ctype_digit( $groupid ) ) {
801 + $group = $this->get_group_by_id( $groupid );
802 + $table_group = esc_sql( $this->table_name( 'group' ) );
803 + $table_wp_group = esc_sql( $this->table_name( 'wp_group' ) );
804 + $nr = $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM `{$table_group}` WHERE id=%d", $groupid ) );
805 + $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM `{$table_wp_group}` WHERE groupid=%d", $groupid ) );
806 + if ( $nr ) {
807 + /**
808 + * Fires after a tag has been deleted.
809 + *
810 + * @param object $group group created.
811 + * @param string group action.
812 + */
813 + do_action( 'mainwp_site_tag_action', $group, 'deleted' );
814 + }
815 + return $nr;
816 + }
836 817
837 - $newValues = array();
818 + return false;
819 + }
838 820
839 - if ( isset( $data['automatic_update'] ) ) {
840 - $newValues['automatic_update'] = $data['automatic_update'] ? 1 : 0;
841 - }
821 + /**
822 + * Method update_note()
823 + *
824 + * Update Note.
825 + *
826 + * @param mixed $websiteid Child Site ID.
827 + * @param mixed $note Note data.
828 + *
829 + * @return void
830 + */
831 + public function update_note( $websiteid, $note ) {
832 + $table_wp = esc_sql( $this->table_name( 'wp' ) );
833 + $this->wpdb->query( $this->wpdb->prepare( "UPDATE `{$table_wp}` SET note= %s WHERE id=%d", $this->escape( $note ), $websiteid ) );
834 + }
842 835
843 - if ( isset( $data['backup_before_upgrade'] ) ) {
844 - $newValues['backup_before_upgrade'] = $data['backup_before_upgrade'] ? 1 : 0;
845 - }
846 - if ( isset( $data['force_use_ipv4'] ) ) {
847 - $forceuseipv4 = intval( $data['force_use_ipv4'] );
848 - if ( 2 < $forceuseipv4 ) {
849 - $forceuseipv4 = 0;
850 - }
851 - $newValues['force_use_ipv4'] = $forceuseipv4;
852 - }
836 + /**
837 + * Method update_group()
838 + *
839 + * Update group.
840 + *
841 + * @param mixed $groupid Group ID.
842 + * @param mixed $groupname Group Name.
843 + * @param string $groupcolor Group Color.
844 + *
845 + * @return boolean true|false.
846 + *
847 + * @uses \MainWP\Dashboard\MainWP_Utility::ctype_digit()
848 + */
849 + public function update_group( $groupid, $groupname, $groupcolor ) {
850 + if ( MainWP_Utility::ctype_digit( $groupid ) ) {
851 + $table_group = esc_sql( $this->table_name( 'group' ) );
852 + $this->wpdb->query( $this->wpdb->prepare( "UPDATE `{$table_group}` SET name=%s, color=%s WHERE id=%d", $this->escape( $groupname ), $this->escape( $groupcolor ), $groupid ) );
853 853
854 - if ( isset( $data['ignore_core_updates'] ) ) {
855 - $newValues['is_ignoreCoreUpdates'] = $data['ignore_core_updates'] ? 1 : 0;
856 - }
854 + return true;
855 + }
857 856
858 - if ( isset( $data['ignore_plugin_updates'] ) ) {
859 - $newValues['is_ignorePluginUpdates'] = $data['ignore_plugin_updates'] ? 1 : 0;
860 - }
857 + return false;
858 + }
861 859
862 - if ( isset( $data['ignore_theme_updates'] ) ) {
863 - $newValues['is_ignoreThemeUpdates'] = $data['ignore_theme_updates'] ? 1 : 0;
864 - }
860 + /**
861 + * Method get_user_notification_email()
862 + *
863 + * Get user notification email.
864 + *
865 + * @param mixed $userid Current user ID.
866 + *
867 + * @return string $user_email User email address.
868 + *
869 + * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
870 + */
871 + public function get_user_notification_email( $userid = 0 ) {
872 + $theUserId = $userid;
873 + if ( MainWP_System::instance()->is_single_user() ) {
874 + $theUserId = 0;
875 + }
876 + $table_users = esc_sql( $this->table_name( 'users' ) );
877 + $user_email = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT user_email FROM `{$table_users}` WHERE userid = %d", $theUserId ) );
865 878
866 - if ( ! empty( $newValues ) ) {
867 - MainWP_DB::instance()->update_website_values( $website->id, $newValues );
868 - }
879 + if ( null === $user_email || empty( $user_email ) ) {
880 + $user_email = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT user_email FROM `' . $this->wpdb->prefix . 'users` WHERE id = %d', $userid ) );
881 + }
869 882
870 - if ( isset( $data['monitoring_emails'] ) ) {
871 - $monitoring_emails = MainWP_Utility::valid_input_emails( $data['monitoring_emails'] );
872 - MainWP_DB::instance()->update_website_option( $website, 'monitoring_notification_emails', $monitoring_emails );
873 - }
883 + return $user_email;
884 + }
874 885
875 - return array(
876 - 'message' => 'Site updated successfully.',
877 - 'site' => $website->url,
878 - );
879 - }
886 + /**
887 + * Method get_user_extension()
888 + *
889 + * Get user extension.
890 + *
891 + * @return boolean|int false|get_user_extension_by_user_id()
892 + *
893 + * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
894 + */
895 + public function get_user_extension() {
896 +
897 + /**
898 + * Current user global.
899 + *
900 + * @global string
901 + */
902 + global $current_user;
903 +
904 + if ( empty( $current_user ) ) {
905 + if ( MainWP_System::instance()->is_single_user() ) {
906 + $userid = 0;
907 + } else {
908 + return false;
909 + }
910 + } else {
911 + $userid = $current_user->ID;
912 + }
913 +
914 + return $this->get_user_extension_by_user_id( $userid );
915 + }
916 +
917 + /**
918 + * Method get_user_extension_by_user_id()
919 + *
920 + * Get user extension by user id.
921 + *
922 + * @param mixed $userid Current user ID.
923 + *
924 + * @return object $row User extension.
925 + *
926 + * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
927 + */
928 + public function get_user_extension_by_user_id( $userid = 0 ) {
929 + if ( MainWP_System::instance()->is_single_user() ) {
930 + $userid = 0;
931 + }
932 +
933 + $table_users = esc_sql( $this->table_name( 'users' ) );
934 + $row = $this->wpdb->get_row( "SELECT * FROM `{$table_users}` WHERE userid= " . intval( $userid ), OBJECT );
935 + if ( null === $row ) {
936 + $this->create_user_extension( $userid );
937 + $row = $this->wpdb->get_row( "SELECT * FROM `{$table_users}` WHERE userid= " . intval( $userid ), OBJECT );
938 + }
939 +
940 + return $row;
941 + }
942 +
943 + /**
944 + * Method create_user_extension()
945 + *
946 + * Create user extension
947 + *
948 + * @param mixed $userId Current user ID.
949 + *
950 + * @return void
951 + */
952 + protected function create_user_extension( $userId ) {
953 + $fields = array(
954 + 'userid' => $userId,
955 + 'user_email' => '',
956 + 'ignored_plugins' => '',
957 + 'trusted_plugins' => '',
958 + 'trusted_plugins_notes' => '',
959 + 'ignored_themes' => '',
960 + 'trusted_themes' => '',
961 + 'trusted_themes_notes' => '',
962 + 'pluginDir' => '',
963 + 'ignored_wp_upgrades' => '',
964 + );
965 +
966 + $this->wpdb->insert( $this->table_name( 'users' ), $fields );
967 + }
968 +
969 + /**
970 + * Method update_user_extension()
971 + *
972 + * Update user extension.
973 + *
974 + * @param mixed $userExtension User extention to update.
975 + *
976 + * @return object $row User extension.
977 + *
978 + * @uses \MainWP\Dashboard\MainWP_System::is_single_user()
979 + */
980 + public function update_user_extension( $userExtension ) {
981 +
982 + if ( is_object( $userExtension ) ) {
983 + $userid = $userExtension->userid;
984 + } elseif ( is_array( $userExtension ) ) {
985 + $userid = $userExtension['userid'];
986 + } else {
987 + $userid = null;
988 + }
989 +
990 + if ( null === $userid ) {
991 + if ( MainWP_System::instance()->is_single_user() ) {
992 + $userid = '0';
993 + } else {
994 +
995 + /**
996 + * Current user global.
997 + *
998 + * @global string
999 + */
1000 + global $current_user;
1001 +
1002 + $userid = $current_user->ID;
1003 + }
1004 + }
1005 + $table_users = esc_sql( $this->table_name( 'users' ) );
1006 + $row = $this->wpdb->get_row( "SELECT * FROM `{$table_users}` WHERE userid= " . intval( $userid ), OBJECT );
1007 + if ( null === $row ) {
1008 + $this->create_user_extension( $userid );
1009 + }
1010 +
1011 + $fields = array();
1012 + foreach ( $userExtension as $field => $value ) {
1013 + if ( $value != $row->$field ) { //phpcs:ignore -- to valid.
1014 + $fields[ $field ] = $value;
1015 + }
1016 + }
1017 +
1018 + if ( ! empty( $fields ) ) {
1019 + $this->wpdb->update( $this->table_name( 'users' ), $fields, array( 'userid' => $userid ) );
1020 + }
1021 +
1022 + return $this->wpdb->get_row( "SELECT * FROM `{$table_users}` WHERE userid= " . intval( $userid ), OBJECT );
1023 + }
1024 +
1025 +
1026 + /**
1027 + * Method rest_api_update_website().
1028 + *
1029 + * Rest API update website.
1030 + *
1031 + * @param int $websiteid website ID.
1032 + * @param array $data Update fields array.
1033 + * 'http_user'.
1034 + * 'http_pass'.
1035 + * 'name'.
1036 + * 'admin'.
1037 + * 'sslversion'.
1038 + * 'uniqueid'.
1039 + * 'verify'.
1040 + * 'protocol'.
1041 + * 'checkinterval'.
1042 + * 'disablehealthchecking'.
1043 + * 'healththreshold'.
1044 + * 'groupids'.
1045 + * 'automatic_update'.
1046 + * 'backup_before_upgrade'.
1047 + * 'force_use_ipv4'.
1048 + * 'ignore_core_updates'.
1049 + * 'ignore_plugin_updates'.
1050 + * 'ignore_theme_updates'.
1051 + * 'monitoring_emails'.
1052 + *
1053 + * @return mixed array|true|false.
1054 + */
1055 + public function rest_api_update_website( $websiteid, $data ) { // phpcs:ignore -- NOSONAR - complex function.
1056 +
1057 + $website = MainWP_DB::instance()->get_website_by_id( $websiteid );
1058 + if ( empty( $website ) ) {
1059 + return false;
1060 + }
1061 +
1062 + $success = false;
1063 +
1064 + $websiteid = (int) $websiteid;
1065 +
1066 + $group_update_success = null;
1067 + $new_groupids = array();
1068 +
1069 + // Validate group IDs before updating any website data.
1070 + if ( isset( $data['groupids'] ) && is_string( $data['groupids'] ) ) {
1071 + $group_update_success = true;
1072 + $groupids_string = trim( wp_unslash( $data['groupids'] ) );
1073 + $new_groupids = $this->validate_site_group_ids( $groupids_string );
1074 + if ( false === $new_groupids ) {
1075 + $group_update_success = false;
1076 + }
1077 + }
1078 +
1079 + // Do not modify any group associations until all group IDs are validated.
1080 + if ( false === $group_update_success ) {
1081 + return array(
1082 + 'group_updated' => false,
1083 + 'group_message' => 'Failed to update tags or invalid tag IDs.',
1084 + );
1085 + }
1086 +
1087 + $map_fields = array(
1088 + 'http_user' => 'http_user',
1089 + 'http_pass' => 'http_pass',
1090 + 'name' => 'name',
1091 + 'adminname' => 'admin',
1092 + 'ssl_version' => 'sslversion',
1093 + 'uniqueId' => 'uniqueid',
1094 + );
1095 +
1096 + $update_fields = array();
1097 +
1098 + foreach ( $map_fields as $field => $name ) {
1099 + if ( isset( $data[ $name ] ) && empty( ! $data[ $name ] ) ) {
1100 + $value = $data[ $name ];
1101 +
1102 + // MWP-1548: encrypt http_user / http_pass at rest before
1103 + // they hit the raw SQL UPDATE. Same fail-closed contract
1104 + // as the other write paths in MainWP_DB.
1105 + if ( 'http_user' === $field || 'http_pass' === $field ) {
1106 + $encrypted = MainWP_Credential_Storage::encrypt_credential( $value, $field );
1107 +
1108 + if ( false === $encrypted ) {
1109 + return false;
1110 + }
1111 +
1112 + $value = $encrypted;
1113 + }
1114 +
1115 + $update_fields[ $field ] = $value;
1116 + }
1117 + }
1118 +
1119 + if ( isset( $data['verify'] ) ) {
1120 + $verify = intval( $data['verify'] );
1121 + $update_fields['verify_certificate'] = $verify;
1122 + }
1123 +
1124 + if ( isset( $data['protocol'] ) && ( 'http' === $data['protocol'] || 'https' === $data['protocol'] ) ) {
1125 + $url = $data['protocol'] . '://' . MainWP_Utility::remove_http_prefix( $website->url, true );
1126 + $update_fields['url'] = $this->escape( $url );
1127 + }
1128 +
1129 + if ( isset( $data['disablehealthchecking'] ) ) {
1130 + $update_fields['disable_health_check'] = $data['disablehealthchecking'] ? 1 : 0;
1131 + }
1132 +
1133 + if ( isset( $data['healththreshold'] ) ) {
1134 + $update_fields['health_threshold'] = intval( $data['healththreshold'] );
1135 + }
1136 +
1137 + if ( isset( $data['suspended'] ) ) {
1138 + $update_fields['suspended'] = 1 === intval( $data['suspended'] ) ? 1 : 0;
1139 + }
1140 +
1141 + if ( ! empty( $update_fields ) ) {
1142 + $updated = $this->wpdb->update(
1143 + $this->table_name( 'wp' ),
1144 + $update_fields,
1145 + array( 'id' => $websiteid )
1146 + );
1147 +
1148 + if ( false === $updated ) {
1149 + return false;
1150 + }
1151 + $success = true;
1152 +
1153 + if ( isset( $update_fields['url'] ) ) {
1154 + // A user-set URL ( REST API / WP-CLI ) that diverges from the
1155 + // child-reported address locks auto-correction for the site.
1156 + MainWP_Site_Url_Corrector::after_user_set_url( $website, $update_fields['url'] );
1157 + }
1158 + }
1159 +
1160 + $groupids = array();
1161 + if ( isset( $data['groupids'] ) && ! empty( $data['groupids'] ) ) {
1162 + $groupids = explode( ',', sanitize_text_field( wp_unslash( $data['groupids'] ) ) );
1163 + }
1164 +
1165 + if ( ! empty( $groupids ) ) {
1166 + $table_wp_group = esc_sql( $this->table_name( 'wp_group' ) );
1167 + $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM `{$table_wp_group}` WHERE wpid=%d", $websiteid ) );
1168 +
1169 + // update groups.
1170 + foreach ( $groupids as $groupid ) {
1171 + $this->wpdb->insert(
1172 + $this->table_name( 'wp_group' ),
1173 + array(
1174 + 'wpid' => $websiteid,
1175 + 'groupid' => $groupid,
1176 + )
1177 + );
1178 + }
1179 + $success = true;
1180 + }
1181 +
1182 + $newValues = array();
1183 +
1184 + if ( isset( $data['automatic_update'] ) ) {
1185 + $newValues['automatic_update'] = $data['automatic_update'] ? 1 : 0;
1186 + }
1187 +
1188 + if ( isset( $data['backup_before_upgrade'] ) ) {
1189 + $newValues['backup_before_upgrade'] = $data['backup_before_upgrade'] ? 1 : 0;
1190 + }
1191 +
1192 + if ( isset( $data['force_use_ipv4'] ) ) {
1193 + $forceuseipv4 = intval( $data['force_use_ipv4'] );
1194 +
1195 + if ( 2 < $forceuseipv4 ) {
1196 + $forceuseipv4 = 0;
1197 + }
1198 +
1199 + $newValues['force_use_ipv4'] = $forceuseipv4;
1200 + }
1201 +
1202 + if ( isset( $data['ignore_core_updates'] ) ) {
1203 + $newValues['is_ignoreCoreUpdates'] = $data['ignore_core_updates'] ? 1 : 0;
1204 + }
1205 +
1206 + if ( isset( $data['ignore_plugin_updates'] ) ) {
1207 + $newValues['is_ignorePluginUpdates'] = $data['ignore_plugin_updates'] ? 1 : 0;
1208 + }
1209 +
1210 + if ( isset( $data['ignore_theme_updates'] ) ) {
1211 + $newValues['is_ignoreThemeUpdates'] = $data['ignore_theme_updates'] ? 1 : 0;
1212 + }
1213 +
1214 + if ( isset( $data['client_id'] ) ) {
1215 + $newValues['client_id'] = max( 0, intval( $data['client_id'] ) );
1216 + }
1217 +
1218 + if ( ! empty( $newValues ) ) {
1219 + MainWP_DB::instance()->update_website_values( $website->id, $newValues );
1220 + $success = true;
1221 + }
1222 +
1223 + if ( isset( $data['monitoring_emails'] ) ) {
1224 + $monitoring_emails = MainWP_Utility::valid_input_emails( $data['monitoring_emails'] );
1225 +
1226 + MainWP_DB::instance()->update_website_option(
1227 + $website,
1228 + 'monitoring_notification_emails',
1229 + $monitoring_emails
1230 + );
1231 + }
1232 +
1233 + // Update group associations last so that credential encryption and
1234 + // website updates cannot fail after the group changes are committed.
1235 + if ( null !== $group_update_success ) {
1236 + $group_update_success = $this->safe_update_site_group_ids(
1237 + $websiteid,
1238 + $new_groupids
1239 + );
1240 +
1241 + if ( false === $group_update_success ) {
1242 + return array(
1243 + 'group_updated' => false,
1244 + 'group_message' => 'Failed to update tags or invalid tag IDs.',
1245 + 'message' => 'Group associations could not be updated, but the other requested fields were saved.',
1246 + 'success' => false,
1247 + 'site' => $website->url,
1248 + );
1249 + }
1250 +
1251 + $success = true;
1252 + }
1253 +
1254 + $response = array(
1255 + 'site' => $website->url,
1256 + );
1257 +
1258 + if ( null !== $group_update_success ) {
1259 + $response['group_updated'] = true;
1260 + $response['group_message'] = 'Tags updated successfully.';
1261 + }
1262 +
1263 + $response['message'] = $success ? 'Site updated successfully.' : 'Site update failed.';
1264 + $response['success'] = $success;
1265 +
1266 + return $response;
1267 + }
1268 +
1269 + /**
1270 + * Validate site group IDs.
1271 + *
1272 + * Accepts a comma-separated string or an array of group IDs,
1273 + * validates that all IDs are positive integers, removes duplicates,
1274 + * and ensures the current user has access to the requested groups.
1275 + *
1276 + * @param string|array $groupids Group IDs to validate.
1277 + *
1278 + * @return array|false Validated group IDs or false on validation failure.
1279 + */
1280 + public function validate_site_group_ids( $groupids ) {
1281 + $tokens = array();
1282 +
1283 + // MWP-1741 - The add-site handler treats 1,abc as group ID 1 plus a new group named abc,
1284 + // while 0 can create a groupid = 0 association. It should enforce the same positive,
1285 + // permitted-ID validation as updates.
1286 + if ( is_string( $groupids ) ) {
1287 + $groupids_string = trim( wp_unslash( $groupids ) );
1288 + if ( '' !== $groupids_string ) {
1289 + $tokens = array_map( 'trim', explode( ',', $groupids_string ) );
1290 + }
1291 + } elseif ( is_array( $groupids ) ) {
1292 + foreach ( $groupids as $groupid ) {
1293 + if ( ! is_string( $groupid ) && ! is_int( $groupid ) ) {
1294 + return false;
1295 + }
1296 + }
1297 + $tokens = array_map( 'trim', $groupids );
1298 + } else {
1299 + return false;
1300 + }
1301 +
1302 + foreach ( $tokens as $token ) {
1303 + if ( '' === $token || ! ctype_digit( $token ) || 0 === (int) $token ) {
1304 + return false;
1305 + }
1306 + }
1307 +
1308 + $groupids = array_values(
1309 + array_unique(
1310 + array_map( 'absint', $tokens )
1311 + )
1312 + );
1313 +
1314 + if ( empty( $groupids ) ) {
1315 + return array();
1316 + }
1317 +
1318 + $groups = $this->get_groups_for_current_user();
1319 +
1320 + if ( empty( $groups ) ) {
1321 + return false;
1322 + }
1323 +
1324 + $allowed_groupids = array_map(
1325 + static function ( $group ) {
1326 + return (int) $group->id;
1327 + },
1328 + $groups
1329 + );
1330 +
1331 + if ( count( $groupids ) !== count( array_intersect( $groupids, $allowed_groupids ) ) ) {
1332 + return false;
1333 + }
1334 +
1335 + return $groupids;
1336 + }
1337 +
1338 + /**
1339 + * Safely update the group associations for a site.
1340 + *
1341 + * New group associations are inserted before obsolete associations are removed.
1342 + * If an insert fails, only the associations created during this operation are
1343 + * removed, preserving the site's existing group associations.
1344 + *
1345 + * @param int $site_id Site ID.
1346 + * @param array $new_group_ids New group IDs.
1347 + *
1348 + * @return bool True on success, false if validation or an insert fails.
1349 + */
1350 + private function safe_update_site_group_ids( $site_id, $new_group_ids ) { // phpcs:ignore -- NOSONAR -complex.
1351 + if ( empty( $site_id ) || ! is_array( $new_group_ids ) ) {
1352 + return false;
1353 + }
1354 +
1355 + global $wpdb;
1356 +
1357 + $context = DB_HOST . ':' . DB_NAME . ':' . $wpdb->prefix . ':' . (int) $site_id;
1358 + $lock_name = 'mainwp_primary_monitor_' . md5( $context ); // phpcs:ignore -- NOSONAR - Broader transaction/locking across all group access paths is outside the scope of this change.
1359 +
1360 + $locked = $this->wpdb->get_var(
1361 + $this->wpdb->prepare(
1362 + 'SELECT GET_LOCK( %s, 10 )',
1363 + $lock_name
1364 + )
1365 + );
1366 +
1367 + if ( '1' !== (string) $locked ) {
1368 + return false;
1369 + }
1370 +
1371 + try {
1372 + $current_groups = $this->get_groups_by_website_id( $site_id );
1373 +
1374 + $old_group_ids = $current_groups
1375 + ? array_map( 'intval', wp_list_pluck( $current_groups, 'id' ) )
1376 + : array();
1377 +
1378 + $table_name = esc_sql( $this->table_name( 'wp_group' ) );
1379 + $inserted_group_ids = array();
1380 + $deleted_group_ids = array();
1381 +
1382 + // Insert new associations first.
1383 + foreach ( array_diff( $new_group_ids, $old_group_ids ) as $group_id ) {
1384 + $result = $this->wpdb->insert(
1385 + $table_name,
1386 + array(
1387 + 'wpid' => $site_id,
1388 + 'groupid' => $group_id,
1389 + ),
1390 + array( '%d', '%d' )
1391 + );
1392 +
1393 + if ( false === $result ) {
1394 + $rollback_success = $this->rollback_site_group_ids(
1395 + $table_name,
1396 + $site_id,
1397 + $inserted_group_ids,
1398 + $deleted_group_ids
1399 + );
1400 +
1401 + if ( ! $rollback_success ) {
1402 + MainWP_Logger::instance()->warning(
1403 + sprintf(
1404 + 'Failed to fully rollback group associations for site ID %d after a group update failure.',
1405 + $site_id
1406 + )
1407 + );
1408 + }
1409 +
1410 + return false;
1411 + }
1412 +
1413 + $inserted_group_ids[] = $group_id;
1414 + }
1415 +
1416 + // Remove obsolete associations.
1417 + foreach ( array_diff( $old_group_ids, $new_group_ids ) as $old_group_id ) {
1418 + $result = $this->wpdb->delete(
1419 + $table_name,
1420 + array(
1421 + 'wpid' => $site_id,
1422 + 'groupid' => $old_group_id,
1423 + ),
1424 + array( '%d', '%d' )
1425 + );
1426 +
1427 + if ( false === $result ) {
1428 + $rollback_success = $this->rollback_site_group_ids(
1429 + $table_name,
1430 + $site_id,
1431 + $inserted_group_ids,
1432 + $deleted_group_ids
1433 + );
1434 +
1435 + if ( ! $rollback_success ) {
1436 + MainWP_Logger::instance()->warning(
1437 + sprintf(
1438 + 'Failed to fully rollback group associations for site ID %d after a group update failure.',
1439 + $site_id
1440 + )
1441 + );
1442 + }
1443 +
1444 + return false;
1445 + }
1446 +
1447 + $deleted_group_ids[] = $old_group_id;
1448 + }
1449 +
1450 + return true;
1451 + } finally {
1452 + $this->wpdb->get_var(
1453 + $this->wpdb->prepare(
1454 + 'SELECT RELEASE_LOCK( %s )',
1455 + $lock_name
1456 + )
1457 + );
1458 + }
1459 + }
1460 +
1461 + /**
1462 + * Roll back site group ID associations.
1463 + *
1464 + * Removes associations created by the current update and restores
1465 + * associations removed by the current update.
1466 + *
1467 + * @param string $table_name Database table name.
1468 + * @param int $site_id Website ID.
1469 + * @param array $inserted_group_ids Group IDs inserted by the update.
1470 + * @param array $deleted_group_ids Group IDs deleted by the update.
1471 + *
1472 + * @return bool True if all rollback operations succeed, false otherwise.
1473 + */
1474 + private function rollback_site_group_ids( $table_name, $site_id, $inserted_group_ids, $deleted_group_ids ) {
1475 + $rollback_success = true;
1476 +
1477 + // Remove associations created by this operation.
1478 + foreach ( $inserted_group_ids as $group_id ) {
1479 + $result = $this->wpdb->delete(
1480 + $table_name,
1481 + array(
1482 + 'wpid' => $site_id,
1483 + 'groupid' => $group_id,
1484 + ),
1485 + array( '%d', '%d' )
1486 + );
1487 +
1488 + if ( false === $result ) {
1489 + $rollback_success = false;
1490 + }
1491 + }
1492 +
1493 + // Restore associations removed by this operation.
1494 + foreach ( $deleted_group_ids as $group_id ) {
1495 + $result = $this->wpdb->insert(
1496 + $table_name,
1497 + array(
1498 + 'wpid' => $site_id,
1499 + 'groupid' => $group_id,
1500 + ),
1501 + array( '%d', '%d' )
1502 + );
1503 +
1504 + if ( false === $result ) {
1505 + $rollback_success = false;
1506 + }
1507 + }
1508 +
1509 + return $rollback_success;
1510 + }
880 1511 }