PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
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-install.php +694 -356 5.0 → 6.2.1 View file →
@@ -8,8 +8,14 @@
8 8 */
9 9
10 10 namespace MainWP\Dashboard;
11 11
12 +// Exit if accessed directly.
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit;
15 +}
16 +
17 +
12 18 /**
13 19 * Class MainWP_Install
14 20 *
15 21 * @package MainWP\Dashboard
@@ -15,92 +21,110 @@
15 21 * @package MainWP\Dashboard
16 22 *
17 23 * @uses \MainWP\Dashboard\MainWP_DB_Base
18 24 */
19 -class MainWP_Install extends MainWP_DB_Base {
25 +class MainWP_Install extends MainWP_DB_Base { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
20 26
21 - // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.DB.PreparedSQL.NotPrepared -- unprepared SQL ok, accessing the database directly to custom database functions.
27 + // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.DB.PreparedSQL.NotPrepared -- unprepared SQL ok, accessing the database directly to custom database functions.
22 28
23 - /**
24 - * Private variable to hold the database version info.
25 - *
26 - * @var string DB version info.
27 - */
28 - protected $mainwp_db_version = '9.0.0.4';
29 + /**
30 + * Private variable to hold the database version info.
31 + *
32 + * @var string DB version info.
33 + */
34 + protected $mainwp_db_version = '9.0.2.5'; // NOSONAR - no IP. 9.0.2.4 drops the stray unique index on backup progress task_id. Bumped for MWP-1566 sibling-dir chmod migration (MWP-1558 follow-up). Original 9.0.2.0 bump for MWP-1557/1558.
29 35
30 - /**
31 - * Protected variable to hold the database option name.
32 - *
33 - * @var string DB version info.
34 - */
35 - protected $option_db_key = 'mainwp_db_version';
36 + /**
37 + * Protected variable to hold the database option name.
38 + *
39 + * @var string DB version info.
40 + */
41 + protected $option_db_key = 'mainwp_db_version';
36 42
37 - /**
38 - * Private static variable to hold the single instance of the class.
39 - *
40 - * @static
41 - *
42 - * @var mixed Default null
43 - */
44 - private static $instance = null;
43 + /**
44 + * Network-scoped like mainwp_db_version, so every blog on a multisite sees the pending repair.
45 + */
46 + const BACKUP_PROGRESS_INDEX_REPAIR_PENDING = 'mainwp_backup_progress_index_repair_pending';
45 47
46 - /**
47 - * Method instance()
48 - *
49 - * Return public static instance.
50 - *
51 - * @static
52 - * @return MainWP_DB
53 - */
54 - public static function instance() {
55 - if ( null === self::$instance ) {
56 - self::$instance = new self();
57 - }
48 + /**
49 + * Network-scoped like mainwp_db_version, so every blog on a multisite sees the pending repair.
50 + */
51 + const WP_GROUP_MISSING_PRIMARY_KEY_REPAIR_PENDING = 'mainwp_wp_group_missing_primary_key_repair_pending';
58 52
59 - self::$instance->test_connection();
53 + /**
54 + * Private static variable to hold the single instance of the class.
55 + *
56 + * @static
57 + *
58 + * @var mixed Default null
59 + */
60 + private static $instance = null;
60 61
61 - return self::$instance;
62 - }
62 + /**
63 + * Method instance()
64 + *
65 + * Return public static instance.
66 + *
67 + * @static
68 + * @return MainWP_DB
69 + */
70 + public static function instance() {
71 + if ( null === static::$instance ) {
72 + static::$instance = new self();
73 + }
63 74
64 - /**
65 - * Method install()
66 - *
67 - * Installs the new DB.
68 - *
69 - * @return void
70 - *
71 - * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
72 - */
73 - public function install() { // phpcs:ignore -- complex function. Current complexity is the only way to achieve desired results, pull request solutions appreciated.
74 - // get_site_option is multisite aware!
75 - $currentVersion = get_site_option( $this->option_db_key );
75 + static::$instance->test_connection();
76 76
77 - if ( empty( $currentVersion ) ) {
78 - update_option( 'mainwp_run_quick_setup', 'yes' );
79 - MainWP_Utility::update_option( 'mainwp_enableLegacyBackupFeature', 0 );
80 - } elseif ( false === get_option( 'mainwp_enableLegacyBackupFeature' ) ) {
81 - MainWP_Utility::update_option( 'mainwp_enableLegacyBackupFeature', 1 );
82 - }
77 + return static::$instance;
78 + }
83 79
84 - if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.8', '<' ) ) {
85 - MainWP_Utility::update_option( 'mainwp_selected_theme', 'default' );
86 - }
80 + /**
81 + * Method install()
82 + *
83 + * Installs the new DB.
84 + *
85 + * @return void
86 + *
87 + * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
88 + */
89 + public function install() { // phpcs:ignore -- NOSONAR - complex function. Current complexity is the only way to achieve desired results, pull request solutions appreciated.
90 + // get_site_option is multisite aware!
91 + $currentVersion = get_site_option( $this->option_db_key );
87 92
88 - $rslt = self::instance()->query( "SHOW TABLES LIKE '" . $this->table_name( 'wp' ) . "'" );
89 - if ( empty( self::num_rows( $rslt ) ) ) {
90 - $currentVersion = false;
91 - }
93 + if ( empty( $currentVersion ) ) {
94 + update_option( 'mainwp_run_quick_setup', 'yes' );
95 + MainWP_Utility::update_option( 'mainwp_enableLegacyBackupFeature', 0 );
96 + } elseif ( false === get_option( 'mainwp_enableLegacyBackupFeature' ) ) {
97 + MainWP_Utility::update_option( 'mainwp_enableLegacyBackupFeature', 1 );
98 + }
92 99
93 - if ( $currentVersion === $this->mainwp_db_version ) {
94 - return;
95 - }
100 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.8', '<' ) ) {
101 + MainWP_Utility::update_option( 'mainwp_selected_theme', 'default' );
102 + }
96 103
97 - $this->pre_update_tables();
104 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
105 + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name escaped via esc_sql.
106 + $rslt = static::instance()->query( "SHOW TABLES LIKE '{$wp_table}'" );
107 + if ( empty( static::num_rows( $rslt ) ) ) {
108 + $currentVersion = false;
109 + }
98 110
99 - $charset_collate = $this->wpdb->get_charset_collate();
111 + if ( $currentVersion === $this->mainwp_db_version ) {
112 + // The index repair keeps its own marker so a failed DROP is retried at
113 + // most hourly, without holding the DB version back and re-running every
114 + // older migration on each load.
115 + $this->maybe_retry_backup_progress_index_repair();
100 116
101 - $sql = array();
102 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp' ) . " (
117 + $this->maybe_retry_repair_wp_group_primary_key();
118 + return;
119 + }
120 +
121 + $this->pre_update_tables();
122 +
123 + $charset_collate = $this->wpdb->get_charset_collate();
124 +
125 + $sql = array();
126 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp' ) . " (
103 127 id int(11) NOT NULL auto_increment,
104 128 userid int(11) NOT NULL,
105 129 adminname text NOT NULL,
106 130 name text NOT NULL,
@@ -110,15 +134,13 @@
110 134 siteurl text NOT NULL,
111 135 ga_id text NOT NULL,
112 136 gas_id int(11) NOT NULL,
113 137 offline_checks_last int(11) NOT NULL,
114 - offline_check_result int(11) NOT NULL,
138 + offline_check_result int(11) NOT NULL,
115 139 http_response_code int(11) NOT NULL DEFAULT 0,
116 140 http_code_noticed tinyint(1) NOT NULL DEFAULT 1,
117 - disable_status_check tinyint(1) NOT NULL DEFAULT 0,
118 141 disable_health_check tinyint(1) NOT NULL DEFAULT 0,
119 - status_check_interval tinyint(1) NOT NULL DEFAULT 0,
120 - health_threshold int(11) NOT NULL DEFAULT 0,
142 + health_threshold int(11) NOT NULL DEFAULT 0,
121 143 note text NOT NULL,
122 144 statsUpdate int(11) NOT NULL,
123 145 directories longtext NOT NULL,
124 146 plugin_upgrades longtext NOT NULL,
@@ -147,8 +169,9 @@
147 169 uniqueId text NOT NULL,
148 170 maximumFileDescriptorsOverride tinyint(1) NOT NULL DEFAULT 0,
149 171 maximumFileDescriptorsAuto tinyint(1) NOT NULL DEFAULT 1,
150 172 maximumFileDescriptors int(11) NOT NULL DEFAULT 150,
173 + primary_backup_method varchar(64) NOT NULL DEFAULT '',
151 174 http_user text NOT NULL DEFAULT '',
152 175 http_pass text NOT NULL DEFAULT '',
153 176 wpe tinyint(1) NOT NULL,
154 177 is_staging tinyint(1) NOT NULL DEFAULT 0,
@@ -153,17 +176,21 @@
153 176 wpe tinyint(1) NOT NULL,
154 177 is_staging tinyint(1) NOT NULL DEFAULT 0,
155 178 client_id int(11) NOT NULL DEFAULT 0,
156 179 `suspended` tinyint(1) NOT NULL DEFAULT 0,
157 - KEY idx_userid (userid)";
158 - if ( empty( $currentVersion ) ) {
159 - $tbl .= ',
180 + KEY idx_wp_staging_name_id (is_staging, name(191), id),
181 + KEY idx_userid (userid),
182 + KEY idx_client_id (client_id),
183 + KEY idx_url (url(191))";
184 +
185 + if ( empty( $currentVersion ) ) {
186 + $tbl .= ',
160 187 PRIMARY KEY (id) ';
161 - }
162 - $tbl .= ') ' . $charset_collate;
163 - $sql[] = $tbl;
188 + }
189 + $tbl .= ') ' . $charset_collate;
190 + $sql[] = $tbl;
164 191
165 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_sync' ) . " (
192 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_sync' ) . " (
166 193 sync_id int(11) NOT NULL auto_increment,
167 194 wpid int(11) NOT NULL,
168 195 version text NOT NULL DEFAULT '',
169 196 sync_errors longtext NOT NULL DEFAULT '',
@@ -180,45 +207,46 @@
180 207 health_status tinyint(1) NOT NULL DEFAULT 0,
181 208 health_site_noticed tinyint(1) NOT NULL DEFAULT 1,
182 209 KEY idx_wpid (wpid)";
183 210
184 - if ( empty( $currentVersion ) ) {
185 - $tbl .= ',
186 - PRIMARY KEY (sync_id)';
187 - }
211 + if ( empty( $currentVersion ) ) {
212 + $tbl .= ',
213 + PRIMARY KEY (sync_id)';
214 + }
188 215
189 - $tbl .= ') ' . $charset_collate;
216 + $tbl .= ') ' . $charset_collate;
190 217
191 - $sql[] = $tbl;
218 + $sql[] = $tbl;
192 219
193 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_options' ) . " (
220 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_options' ) . " (
194 221 opt_id int(11) NOT NULL auto_increment,
195 222 wpid int(11) NOT NULL,
196 223 name text NOT NULL DEFAULT '',
197 224 value longtext NOT NULL DEFAULT '',
225 + KEY idx_options_wpid_name (wpid, name(191)),
198 226 KEY idx_wpid (wpid)";
199 227
200 - if ( empty( $currentVersion ) ) {
201 - $tbl .= ',
202 - PRIMARY KEY (opt_id)';
203 - }
204 - $tbl .= ') ' . $charset_collate;
205 - $sql[] = $tbl;
228 + if ( empty( $currentVersion ) ) {
229 + $tbl .= ',
230 + PRIMARY KEY (opt_id)';
231 + }
232 + $tbl .= ') ' . $charset_collate;
233 + $sql[] = $tbl;
206 234
207 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_settings_backup' ) . ' (
235 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_settings_backup' ) . ' (
208 236 set_id int(11) NOT NULL auto_increment,
209 237 wpid int(11) NOT NULL,
210 238 archiveFormat text NOT NULL,
211 239 KEY idx_wpid (wpid)';
212 240
213 - if ( empty( $currentVersion ) ) {
214 - $tbl .= ',
215 - PRIMARY KEY (set_id)';
216 - }
217 - $tbl .= ') ' . $charset_collate;
218 - $sql[] = $tbl;
241 + if ( empty( $currentVersion ) ) {
242 + $tbl .= ',
243 + PRIMARY KEY (set_id)';
244 + }
245 + $tbl .= ') ' . $charset_collate;
246 + $sql[] = $tbl;
219 247
220 - $tbl = 'CREATE TABLE ' . $this->table_name( 'users' ) . " (
248 + $tbl = 'CREATE TABLE ' . $this->table_name( 'users' ) . " (
221 249 userid int(11) NOT NULL,
222 250 user_email text NOT NULL DEFAULT '',
223 251 ignored_plugins longtext NOT NULL DEFAULT '',
224 252 trusted_plugins longtext NOT NULL DEFAULT '',
@@ -223,8 +251,9 @@
223 251 ignored_plugins longtext NOT NULL DEFAULT '',
224 252 trusted_plugins longtext NOT NULL DEFAULT '',
225 253 trusted_plugins_notes longtext NOT NULL DEFAULT '',
226 254 ignored_themes longtext NOT NULL DEFAULT '',
255 + ignored_wp_upgrades longtext NOT NULL DEFAULT '',
227 256 trusted_themes longtext NOT NULL DEFAULT '',
228 257 trusted_themes_notes longtext NOT NULL DEFAULT '',
229 258 site_view tinyint(1) NOT NULL DEFAULT '0',
230 259 pluginDir text NOT NULL DEFAULT '',
@@ -229,55 +258,69 @@
229 258 site_view tinyint(1) NOT NULL DEFAULT '0',
230 259 pluginDir text NOT NULL DEFAULT '',
231 260 dismissed_plugins longtext NOT NULL DEFAULT '',
232 261 dismissed_themes longtext NOT NULL DEFAULT ''";
233 - if ( empty( $currentVersion ) ) {
234 - $tbl .= ',
262 + if ( empty( $currentVersion ) ) {
263 + $tbl .= ',
235 264 PRIMARY KEY (userid) ';
236 - }
237 - $tbl .= ') ' . $charset_collate;
238 - $sql[] = $tbl;
265 + }
266 + $tbl .= ') ' . $charset_collate;
267 + $sql[] = $tbl;
239 268
240 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_status' ) . ' (
241 - statusid bigint(20) unsigned NOT NULL auto_increment,
242 - wpid int(11) NOT NULL,
243 - http_code smallint NOT NULL DEFAULT 0,
244 - status tinyint(1) NOT NULL DEFAULT 0,
245 - event_timestamp int(11) NOT NULL,
246 - duration int(11) NOT NULL DEFAULT 0';
247 - if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.31', '<=' ) ) {
248 - $tbl .= ',
249 - PRIMARY KEY (statusid) ';
250 - }
251 - $tbl .= ') ' . $charset_collate;
252 - $sql[] = $tbl;
269 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_status' ) . ' (
270 + statusid bigint(20) unsigned NOT NULL auto_increment,
271 + wpid int(11) NOT NULL,
272 + http_code smallint NOT NULL DEFAULT 0,
273 + status tinyint(1) NOT NULL DEFAULT 0,
274 + event_timestamp int(11) NOT NULL,
275 + duration int(11) NOT NULL DEFAULT 0';
276 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.31', '<=' ) ) {
277 + $tbl .= ',
278 + PRIMARY KEY (statusid) ';
279 + }
280 + $tbl .= ') ' . $charset_collate;
281 + $sql[] = $tbl;
253 282
254 - $tbl = 'CREATE TABLE ' . $this->table_name( 'group' ) . ' (
283 + $tbl = 'CREATE TABLE ' . $this->table_name( 'group' ) . ' (
255 284 id int(11) NOT NULL auto_increment,
256 285 userid int(11) NOT NULL,
257 286 name text NOT NULL,
258 287 color varchar(32) NOT NULL DEFAULT ""';
259 - if ( empty( $currentVersion ) ) {
260 - $tbl .= ',
288 + if ( empty( $currentVersion ) ) {
289 + $tbl .= ',
261 290 PRIMARY KEY (id) ';
262 - }
263 - $tbl .= ') ' . $charset_collate;
264 - $sql[] = $tbl;
291 + }
292 + $tbl .= ') ' . $charset_collate;
293 + $sql[] = $tbl;
265 294
266 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_group' ) . ' (
295 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_group' ) . ' (
267 296 wp_group_id int(11) NOT NULL auto_increment,
268 297 wpid int(11) NOT NULL,
269 298 groupid int(11) NOT NULL,
270 299 KEY idx_wpid (wpid),
271 - KEY idx_groupid (groupid)';
272 - if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.57', '<=' ) ) {
273 - $tbl .= ',
274 - PRIMARY KEY (wp_group_id) ';
275 - }
276 - $tbl .= ') ' . $charset_collate;
277 - $sql[] = $tbl;
300 + KEY idx_groupid (groupid),
301 + PRIMARY KEY (wp_group_id)
302 + ) ' . $charset_collate;
278 303
279 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_backup_progress' ) . " (
304 + $sql[] = $tbl;
305 +
306 + $tbl = 'CREATE TABLE ' . $this->table_name( 'lookup_item_objects' ) . ' (
307 + lookup_id bigint(20) unsigned NOT NULL auto_increment,
308 + item_id bigint(20) unsigned NOT NULL,
309 + item_name varchar(32) NOT NULL,
310 + object_id bigint(20) unsigned NOT NULL,
311 + object_name varchar(32) NOT NULL,
312 + KEY item_id (item_id),
313 + KEY object_id (object_id)';
314 +
315 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '9.0.0.5', '<' ) ) { // NOSONAR - none IP.
316 + $tbl .= ',
317 + PRIMARY KEY (lookup_id) ';
318 + }
319 + $tbl .= ') ' . $charset_collate . ';';
320 + $sql[] = $tbl;
321 +
322 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_backup_progress' ) . " (
280 323 task_id int(11) NOT NULL,
281 324 wp_id int(11) NOT NULL,
282 325 dtsFetched int(11) NOT NULL DEFAULT 0,
283 326 fetchResult text NOT NULL DEFAULT '',
@@ -289,16 +332,12 @@
289 332 attempts int(11) NOT NULL DEFAULT 0,
290 333 last_error text NOT NULL DEFAULT '',
291 334 pid int(11) NOT NULL DEFAULT 0,
292 335 KEY idx_task_id (task_id)";
293 - if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.53', '<=' ) ) {
294 - $tbl .= ',
295 - UNIQUE (task_id)';
296 - }
297 - $tbl .= ') ' . $charset_collate;
298 - $sql[] = $tbl;
336 + $tbl .= ') ' . $charset_collate;
337 + $sql[] = $tbl;
299 338
300 - $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_backup' ) . ' (
339 + $tbl = 'CREATE TABLE ' . $this->table_name( 'wp_backup' ) . ' (
301 340 id int(11) NOT NULL auto_increment,
302 341 userid int(11) NOT NULL,
303 342 name text NOT NULL,
304 343 schedule text NOT NULL,
@@ -325,30 +364,51 @@
325 364 loadFilesBeforeZip tinyint(1) NOT NULL DEFAULT 1,
326 365 maximumFileDescriptorsOverride tinyint(1) NOT NULL DEFAULT 0,
327 366 maximumFileDescriptorsAuto tinyint(1) NOT NULL DEFAULT 1,
328 367 maximumFileDescriptors int(11) NOT NULL DEFAULT 150';
329 - if ( empty( $currentVersion ) ) {
330 - $tbl .= ',
368 + if ( empty( $currentVersion ) ) {
369 + $tbl .= ',
331 370 PRIMARY KEY (id) ';
332 - }
333 - $tbl .= ') ' . $charset_collate;
334 - $sql[] = $tbl;
371 + }
372 + $tbl .= ') ' . $charset_collate;
373 + $sql[] = $tbl;
335 374
336 - $tbl = 'CREATE TABLE ' . $this->table_name( 'action_log' ) . " (
337 - id int(11) NOT NULL auto_increment,
338 - log_content mediumtext NOT NULL DEFAULT '',
339 - log_type tinyint(1) DEFAULT 0,
340 - log_color tinyint(1) DEFAULT 0,
341 - log_user varchar(128) NOT NULL DEFAULT '',
342 - log_timestamp int(11) NOT NULL DEFAULT 0";
343 - if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.50', '<=' ) ) {
344 - $tbl .= ',
345 - PRIMARY KEY (id) ';
346 - }
347 - $tbl .= ') ' . $charset_collate . ';';
348 - $sql[] = $tbl;
375 + $tbl = 'CREATE TABLE ' . $this->table_name( 'api_keys' ) . ' (
376 + key_id bigint(20) unsigned NOT NULL auto_increment,
377 + user_id bigint(20) unsigned NOT NULL,
378 + description varchar(200) NULL,
379 + permissions varchar(10) NOT NULL,
380 + consumer_key char(64) NOT NULL,
381 + consumer_secret varchar(255) NOT NULL,
382 + nonces longtext NULL,
383 + truncated_key char(7) NOT NULL,
384 + key_pass char(64) NOT NULL DEFAULT "",
385 + key_type tinyint(1) NOT NULL DEFAULT 0,
386 + `enabled` tinyint(1) DEFAULT 0,
387 + last_access datetime NULL default null,
388 + KEY consumer_key (consumer_key)';
389 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '9.0.0.9', '<=' ) ) { // NOSONAR - none IP.
390 + $tbl .= ',
391 + PRIMARY KEY (key_id) ';
392 + }
393 + $tbl .= ') ' . $charset_collate . ';';
394 + $sql[] = $tbl;
349 395
350 - $tbl = 'CREATE TABLE ' . $this->table_name( 'request_log' ) . " (
396 + $tbl = 'CREATE TABLE ' . $this->table_name( 'action_log' ) . " (
397 + id int(11) NOT NULL auto_increment,
398 + log_content mediumtext NOT NULL DEFAULT '',
399 + log_type tinyint(1) DEFAULT 0,
400 + log_color tinyint(1) DEFAULT 0,
401 + log_user varchar(128) NOT NULL DEFAULT '',
402 + log_timestamp int(11) NOT NULL DEFAULT 0";
403 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '8.50', '<=' ) ) {
404 + $tbl .= ',
405 + PRIMARY KEY (id) ';
406 + }
407 + $tbl .= ') ' . $charset_collate . ';';
408 + $sql[] = $tbl;
409 +
410 + $tbl = 'CREATE TABLE ' . $this->table_name( 'request_log' ) . " (
351 411 id int(11) NOT NULL auto_increment,
352 412 wpid int(11) NOT NULL,
353 413 ip text NOT NULL DEFAULT '',
354 414 subnet text NOT NULL DEFAULT '',
@@ -353,237 +413,515 @@
353 413 ip text NOT NULL DEFAULT '',
354 414 subnet text NOT NULL DEFAULT '',
355 415 micro_timestamp_stop DECIMAL( 12, 2 ) NOT NULL DEFAULT 0,
356 416 micro_timestamp_start DECIMAL( 12, 2 ) NOT NULL DEFAULT 0";
357 - if ( empty( $currentVersion ) || version_compare( $currentVersion, '5.7', '<=' ) ) {
358 - $tbl .= ',
417 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '5.7', '<=' ) ) {
418 + $tbl .= ',
359 419 PRIMARY KEY (id) ';
360 - }
361 - $tbl .= ') ' . $charset_collate . ';';
362 - $sql[] = $tbl;
420 + }
421 + $tbl .= ') ' . $charset_collate . ';';
422 + $sql[] = $tbl;
363 423
364 - $sql = apply_filters( 'mainwp_db_install_tables', $sql, $currentVersion, $charset_collate );
424 + $tbl = 'CREATE TABLE ' . $this->table_name( 'schedule_processes' ) . " (
425 + process_id int(11) NOT NULL auto_increment,
426 + item_id int(11) NOT NULL,
427 + `type` varchar(32) NOT NULL,
428 + `process_slug` varchar(64) NOT NULL,
429 + `status` varchar(32) NOT NULL DEFAULT '',
430 + dts_process_start int(11) NOT NULL DEFAULT 0,
431 + dts_process_init_time int(11) NOT NULL DEFAULT 0,
432 + dts_process_stop int(11) NOT NULL DEFAULT 0";
365 433
366 - require_once ABSPATH . 'wp-admin/includes/upgrade.php';
434 + if ( empty( $currentVersion ) || version_compare( $currentVersion, '9.0.0.45', '<' ) ) { //phpcs:ignore -- NOSONAR - no ip.
435 + $tbl .= ',
436 + PRIMARY KEY (process_id) ';
437 + }
367 438
368 - global $wpdb;
439 + $tbl .= ') ' . $charset_collate . ';';
440 + $sql[] = $tbl;
369 441
370 - if ( MainWP_Utility::instance()->is_disabled_functions( 'error_log' ) || ! function_exists( '\error_log' ) ) {
371 - error_reporting(0); // phpcs:ignore -- try to disabled the error_log somewhere in WP.
372 - }
442 + // End of tables.
373 443
374 - $suppress = $wpdb->suppress_errors();
375 - foreach ( $sql as $query ) {
376 - dbDelta( $query );
377 - }
378 - $wpdb->suppress_errors( $suppress );
444 + $sql = apply_filters( 'mainwp_db_install_tables', $sql, $currentVersion, $charset_collate );
379 445
380 - $this->post_update();
446 + MainWP_DB_Uptime_Monitoring::instance()->get_db_schema( $sql, $currentVersion );
381 447
382 - if ( ! is_multisite() ) {
383 - MainWP_Utility::update_option( $this->option_db_key, $this->mainwp_db_version );
384 - } else {
385 - update_site_option( $this->option_db_key, $this->mainwp_db_version );
386 - }
387 - }
448 + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; // NOSONAR - WP compatible.
388 449
389 - /**
390 - * Returns the database version.
391 - *
392 - * @return string
393 - */
394 - public function get_db_version() {
395 - return get_site_option( $this->option_db_key );
396 - }
450 + global $wpdb;
397 451
398 - /**
399 - * Method post_update()
400 - *
401 - * Update MainWP DB.
402 - *
403 - * @return void
404 - */
405 - public function post_update() {
452 + if ( MainWP_Utility::instance()->is_disabled_functions( 'error_log' ) || ! function_exists( '\error_log' ) ) {
453 + error_reporting(0); // phpcs:ignore -- try to disabled the error_log somewhere in WP.
454 + }
406 455
407 - // get_site_option is multisite aware!
408 - $currentVersion = get_site_option( $this->option_db_key );
456 + $suppress = $wpdb->suppress_errors();
457 + foreach ( $sql as $query ) {
458 + dbDelta( $query );
459 + }
460 + $wpdb->suppress_errors( $suppress );
409 461
410 - if ( false === $currentVersion ) {
411 - return;
412 - }
462 + $this->post_update();
413 463
414 - $suppress = $this->wpdb->suppress_errors();
464 + do_action( 'mainwp_db_after_update', $currentVersion, $this->mainwp_db_version ); // new version: $this->mainwp_db_version.
415 465
416 - $this->post_update_81( $currentVersion );
466 + if ( ! is_multisite() ) {
467 + MainWP_Utility::update_option( $this->option_db_key, $this->mainwp_db_version );
468 + } else {
469 + update_site_option( $this->option_db_key, $this->mainwp_db_version );
470 + }
471 + }
417 472
418 - if ( version_compare( $currentVersion, '8.984', '<' ) ) {
419 - $sslColumns = array(
420 - 'nossl',
421 - 'nosslkey',
422 - );
423 - foreach ( $sslColumns as $col ) {
424 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN ' . $col );
425 - }
426 - }
473 + /**
474 + * Returns the database version.
475 + *
476 + * @return string
477 + */
478 + public function get_db_version() {
479 + return get_site_option( $this->option_db_key );
480 + }
427 481
428 - // delete old columns.
429 - if ( version_compare( $currentVersion, '8.17', '<' ) ) {
430 - $rankColumns = array(
431 - 'pagerank',
432 - 'indexed',
433 - 'alexia',
434 - 'pagerank_old',
435 - 'indexed_old',
436 - 'alexia_old',
437 - 'last_db_backup_size',
438 - );
482 + /**
483 + * Method post_update()
484 + *
485 + * Update MainWP DB.
486 + *
487 + * @return void
488 + */
489 + public function post_update() { // phpcs:ignore -- NOSONAR - complex.
439 490
440 - foreach ( $rankColumns as $rankColumn ) {
441 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN ' . $rankColumn );
442 - }
491 + // get_site_option is multisite aware!
492 + $currentVersion = get_site_option( $this->option_db_key );
443 493
444 - $syncColumns = array( 'uptodate' );
445 - foreach ( $syncColumns as $column ) {
446 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp_sync' ) . ' DROP COLUMN ' . $column );
447 - }
448 - }
494 + if ( false === $currentVersion ) {
495 + return;
496 + }
449 497
450 - // delete old columns.
451 - if ( version_compare( $currentVersion, '8.35', '<' ) ) {
452 - $delColumns = array( 'offline_checks' );
453 - foreach ( $delColumns as $column ) {
454 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN ' . $column );
455 - }
456 - $delColumns = array( 'heatMap' );
457 - foreach ( $delColumns as $column ) {
458 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'users' ) . ' DROP COLUMN ' . $column );
459 - }
460 - }
498 + $suppress = $this->wpdb->suppress_errors();
461 499
462 - // change columns.
463 - if ( version_compare( $currentVersion, '8.40', '<' ) && version_compare( $currentVersion, '8.30', '>' ) ) {
464 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp_status' ) . ' CHANGE COLUMN `timestamp_status` `event_timestamp` int(11) NOT NULL' );
465 - }
500 + $this->post_update_81( $currentVersion );
466 501
467 - // delete columns.
468 - if ( version_compare( $currentVersion, '8.42', '<' ) ) {
469 - $delColumns = array( 'offlineChecksOnlineNotification' );
470 - foreach ( $delColumns as $column ) {
471 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'users' ) . ' DROP COLUMN ' . $column );
472 - }
473 - }
502 + if ( version_compare( $currentVersion, '8.984', '<' ) ) {
503 + $sslColumns = array(
504 + 'nossl',
505 + 'nosslkey',
506 + );
507 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
508 + foreach ( $sslColumns as $col ) {
509 + $col = esc_sql( $col );
510 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
511 + $this->wpdb->query( "ALTER TABLE {$wp_table} DROP COLUMN {$col}" );
512 + }
513 + }
474 514
475 - // fix missing PRIMARY keys.
476 - if ( version_compare( $currentVersion, '8.53', '<=' ) ) {
477 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp_options' ) . ' ADD opt_id int NOT NULL AUTO_INCREMENT PRIMARY KEY' );
478 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp_settings_backup' ) . ' ADD set_id int NOT NULL AUTO_INCREMENT PRIMARY KEY' );
479 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp_sync' ) . ' ADD sync_id int NOT NULL AUTO_INCREMENT PRIMARY KEY' );
480 - }
515 + // delete old columns.
516 + if ( version_compare( $currentVersion, '8.17', '<' ) ) {
517 + $rankColumns = array(
518 + 'pagerank',
519 + 'indexed',
520 + 'alexia',
521 + 'pagerank_old',
522 + 'indexed_old',
523 + 'alexia_old',
524 + 'last_db_backup_size',
525 + );
481 526
482 - $this->wpdb->suppress_errors( $suppress );
483 - MainWP_DB_Client::instance()->check_to_updates_reports_data_861( $currentVersion );
484 - }
527 + foreach ( $rankColumns as $rankColumn ) {
528 + $rankColumn = esc_sql( $rankColumn );
529 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
530 + $this->wpdb->query( "ALTER TABLE {$wp_table} DROP COLUMN {$rankColumn}" );
531 + }
485 532
486 - /**
487 - * Method pre_update_tables()
488 - *
489 - * Handle pre update tables.
490 - *
491 - * @return void
492 - */
493 - public function pre_update_tables() {
494 - // get_site_option is multisite aware!
495 - $currentVersion = get_site_option( $this->option_db_key );
533 + $syncColumns = array( 'uptodate' );
534 + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
535 + foreach ( $syncColumns as $column ) {
536 + $column = esc_sql( $column );
537 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
538 + $this->wpdb->query( "ALTER TABLE {$wp_sync_table} DROP COLUMN {$column}" );
539 + }
540 + }
496 541
497 - if ( false === $currentVersion ) {
498 - return;
499 - }
542 + // delete old columns.
543 + if ( version_compare( $currentVersion, '8.35', '<' ) ) {
544 + $delColumns = array( 'offline_checks' );
545 + foreach ( $delColumns as $column ) {
546 + $column = esc_sql( $column );
547 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
548 + $this->wpdb->query( "ALTER TABLE {$wp_table} DROP COLUMN {$column}" );
549 + }
550 + $delColumns = array( 'heatMap' );
551 + $users_table = esc_sql( $this->table_name( 'users' ) );
552 + foreach ( $delColumns as $column ) {
553 + $column = esc_sql( $column );
554 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
555 + $this->wpdb->query( "ALTER TABLE {$users_table} DROP COLUMN {$column}" );
556 + }
557 + }
500 558
501 - $suppress = $this->wpdb->suppress_errors();
559 + // delete columns.
560 + if ( version_compare( $currentVersion, '8.42', '<' ) ) {
561 + $delColumns = array( 'offlineChecksOnlineNotification' );
562 + foreach ( $delColumns as $column ) {
563 + $column = esc_sql( $column );
564 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
565 + $this->wpdb->query( "ALTER TABLE {$users_table} DROP COLUMN {$column}" );
566 + }
567 + }
502 568
503 - if ( version_compare( $currentVersion, '8.98', '<=' ) ) {
504 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN backups' );
505 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN note_lastupdate' );
506 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN pages' );
507 - }
569 + // fix missing PRIMARY keys.
570 + if ( version_compare( $currentVersion, '8.53', '<=' ) ) {
571 + $wp_options_table = esc_sql( $this->table_name( 'wp_options' ) );
572 + $wp_settings_backup_table = esc_sql( $this->table_name( 'wp_settings_backup' ) );
573 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name escaped via esc_sql.
574 + $this->wpdb->query( "ALTER TABLE {$wp_options_table} ADD opt_id int NOT NULL AUTO_INCREMENT PRIMARY KEY" );
575 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name escaped via esc_sql.
576 + $this->wpdb->query( "ALTER TABLE {$wp_settings_backup_table} ADD set_id int NOT NULL AUTO_INCREMENT PRIMARY KEY" );
577 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name escaped via esc_sql.
578 + $this->wpdb->query( "ALTER TABLE {$wp_sync_table} ADD sync_id int NOT NULL AUTO_INCREMENT PRIMARY KEY" );
579 + }
508 580
509 - $this->wpdb->suppress_errors( $suppress );
510 - }
581 + $this->update_optimize_indexes_55( $currentVersion );
511 582
512 - /**
513 - * Method post_update_81()
514 - *
515 - * Update MainWP DB for version 8.1.
516 - *
517 - * @param string $current_version Current version DB.
518 - *
519 - * @return void
520 - */
521 - public function post_update_81( $current_version ) {
583 + // dbDelta never drops an index, so the stray unique key needs an explicit migration.
584 + if ( version_compare( $currentVersion, '9.0.2.4', '<' ) ) { // NOSONAR - no ip.
585 + $this->repair_backup_progress_index();
586 + }
522 587
523 - if ( version_compare( $current_version, '8.1', '<' ) ) {
588 + if ( ! empty( $currentVersion ) && version_compare( $currentVersion, '9.0.2.5', '<' ) ) { // NOSONAR - no ip.
589 + $this->repair_wp_group_primary_key();
590 + }
524 591
525 - // We can't split up here!
526 - $wpSyncColumns = array(
527 - 'version',
528 - 'totalsize',
529 - 'dbsize',
530 - 'extauth',
531 - 'last_post_gmt',
532 - 'sync_errors',
533 - 'dtsSync',
534 - 'dtsSyncStart',
535 - 'dtsAutomaticSync',
536 - 'dtsAutomaticSyncStart',
537 - );
538 - foreach ( $wpSyncColumns as $wpSyncColumn ) {
539 - $rslts = $this->wpdb->get_results( 'SELECT id,' . $wpSyncColumn . ' FROM ' . $this->table_name( 'wp' ), ARRAY_A );
540 - if ( empty( $rslts ) ) {
541 - continue;
542 - }
592 + $this->wpdb->suppress_errors( $suppress );
593 + MainWP_DB_Client::instance()->check_to_updates_reports_data_861( $currentVersion );
594 + }
543 595
544 - foreach ( $rslts as $rslt ) {
545 - $exists = $this->wpdb->get_results( $this->wpdb->prepare( 'SELECT wpid FROM ' . $this->table_name( 'wp_sync' ) . ' WHERE wpid = %d', $rslt['id'] ), ARRAY_A );
546 - if ( empty( $exists ) ) {
547 - $this->wpdb->insert(
548 - $this->table_name( 'wp_sync' ),
549 - array(
550 - 'wpid' => $rslt['id'],
551 - $wpSyncColumn => $rslt[ $wpSyncColumn ],
552 - )
553 - );
554 - } else {
555 - $this->wpdb->update( $this->table_name( 'wp_sync' ), array( $wpSyncColumn => $rslt[ $wpSyncColumn ] ), array( 'wpid' => $rslt['id'] ) );
556 - }
557 - }
596 + /**
597 + * Handle optimize tables indexes.
598 + *
599 + * @param string $current_ver Current DB version.
600 + *
601 + * @return void
602 + */
603 + public function update_optimize_indexes_55( $current_ver ) {
604 + if ( ! empty( $current_ver ) && version_compare( $current_ver, '9.0.1.1', '<' ) ) { // NOSONAR - no ip.
605 + $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' ADD INDEX idx_wp_staging_name_id (is_staging, name(191), id)' ); //phpcs:ignore -- ok.
606 + $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp_options' ) . ' ADD INDEX KEY idx_options_wpid_name (wpid, name(191))' ); //phpcs:ignore -- ok.
607 + }
608 + if ( ! empty( $current_ver ) && version_compare( $current_ver, '9.0.1.3', '<' ) ) { // NOSONAR - no ip.
609 + $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' ADD INDEX idx_userid (userid)' ); //phpcs:ignore -- ok.
610 + $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' ADD INDEX idx_client_id (client_id)' ); //phpcs:ignore -- ok.
611 + $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' ADD INDEX idx_url (url(191))' ); //phpcs:ignore -- ok.
612 + }
613 + // MWP-1540: widen consumer_secret so wp_hash_password output (variable length,
614 + // typically 34 chars for $P$ but up to ~150 for argon2) fits without truncation,
615 + // and drop the unused KEY consumer_secret index (lookups are by HMAC'd consumer_key,
616 + // never by consumer_secret). dbDelta does not reliably MODIFY existing column types
617 + // or DROP indexes, so both changes are applied explicitly here.
618 + if ( ! empty( $current_ver ) && version_compare( $current_ver, '9.0.1.5', '<' ) ) { // NOSONAR - no ip.
619 + $api_keys_table = $this->table_name( 'api_keys' );
620 + // Drop the unused index FIRST so the column type change is unambiguous.
621 + $existing_indexes = $this->wpdb->get_col( "SHOW INDEX FROM {$api_keys_table} WHERE Key_name = 'consumer_secret'", 2 ); // phpcs:ignore -- table name is internal.
622 + if ( ! empty( $existing_indexes ) ) {
623 + $this->wpdb->query( "ALTER TABLE {$api_keys_table} DROP INDEX consumer_secret" ); // phpcs:ignore -- table name is internal.
624 + }
625 + $this->wpdb->query( "ALTER TABLE {$api_keys_table} MODIFY COLUMN consumer_secret varchar(255) NOT NULL" ); // phpcs:ignore -- table name is internal.
558 626
559 - $suppress = $this->wpdb->suppress_errors();
560 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN ' . $wpSyncColumn );
561 - $this->wpdb->suppress_errors( $suppress );
562 - }
627 + // Confirm the MODIFY actually took effect before we trust this migration.
628 + // WP 6.5+ produces bcrypt hashes around 60 characters, so a silent failure
629 + // would leave the column at char(43) and truncate every freshly hashed
630 + // secret on insert, quietly breaking auth on any newly created key. On a
631 + // width mismatch we set a flag option that the dashboard surfaces as an
632 + // admin notice; mainwp_notice_wp_mail_failed in class-mainwp-system.php
633 + // uses the same shape.
634 + $col_def = $this->wpdb->get_row( "SHOW COLUMNS FROM {$api_keys_table} LIKE 'consumer_secret'" ); // phpcs:ignore -- table name is internal.
635 + if ( empty( $col_def ) || false === stripos( (string) $col_def->Type, 'varchar(255)' ) ) {
636 + update_option( 'mainwp_notice_consumer_secret_migration_failed', current_time( 'mysql' ) );
637 + } else {
638 + delete_option( 'mainwp_notice_consumer_secret_migration_failed' );
639 + }
640 + }
641 + }
563 642
564 - $optionColumns = array(
565 - 'last_wp_upgrades',
566 - 'last_plugin_upgrades',
567 - 'last_theme_upgrades',
568 - 'wp_upgrades',
569 - 'recent_comments',
570 - 'recent_posts',
571 - 'recent_pages',
572 - );
573 - foreach ( $optionColumns as $optionColumn ) {
574 - $rslts = $this->wpdb->get_results( 'SELECT id,' . $optionColumn . ' FROM ' . $this->table_name( 'wp' ), ARRAY_A );
575 - if ( empty( $rslts ) ) {
576 - continue;
577 - }
643 + /**
644 + * Retry a pending backup progress index repair, no more than once an hour.
645 + *
646 + * A DB user without ALTER never clears the marker, so an unthrottled retry
647 + * would run two SHOW INDEX plus a failing ALTER on every single request.
648 + *
649 + * @return bool True when a repair attempt was made.
650 + */
651 + public function maybe_retry_repair_wp_group_primary_key() {
652 + $pending = (int) get_site_option( self::WP_GROUP_MISSING_PRIMARY_KEY_REPAIR_PENDING );
653 + if ( empty( $pending ) || time() - $pending < HOUR_IN_SECONDS ) {
654 + return false;
655 + }
578 656
579 - foreach ( $rslts as $rslt ) {
580 - self::update_website_option( (object) $rslt, $optionColumn, $rslt[ $optionColumn ] );
581 - }
657 + $suppress = $this->wpdb->suppress_errors();
658 + $this->repair_wp_group_primary_key();
659 + $this->wpdb->suppress_errors( $suppress );
582 660
583 - $suppress = $this->wpdb->suppress_errors();
584 - $this->wpdb->query( 'ALTER TABLE ' . $this->table_name( 'wp' ) . ' DROP COLUMN ' . $optionColumn );
585 - $this->wpdb->suppress_errors( $suppress );
586 - }
587 - }
588 - }
661 + return true;
662 + }
663 +
664 + /**
665 + * Repair the wp_group table primary key for legacy installations.
666 + *
667 + * To fix MWP-1794.
668 + *
669 + * @return void
670 + */
671 + private function repair_wp_group_primary_key() {
672 + $wp_group_table = esc_sql( $this->table_name( 'wp_group' ) );
673 +
674 + $column_exists = $this->wpdb->get_var(
675 + "SHOW COLUMNS FROM {$wp_group_table} LIKE 'wp_group_id'" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is escaped and query contains no user input.
676 + );
677 +
678 + if ( empty( $column_exists ) ) {
679 + $this->wpdb->query(
680 + "ALTER TABLE {$wp_group_table}
681 + ADD COLUMN wp_group_id int(11) NOT NULL auto_increment PRIMARY KEY FIRST" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is escaped and query contains no user input.
682 + );
683 + } else {
684 + $primary_key = $this->wpdb->get_var(
685 + "SHOW INDEX FROM {$wp_group_table}
686 + WHERE Key_name = 'PRIMARY'
687 + AND Column_name = 'wp_group_id'" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is escaped and query contains no user input.
688 + );
689 +
690 + if ( empty( $primary_key ) ) {
691 + $this->wpdb->query(
692 + "ALTER TABLE {$wp_group_table}
693 + ADD PRIMARY KEY (wp_group_id)" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is escaped and query contains no user input.
694 + );
695 + }
696 + }
697 +
698 + $primary_key = $this->wpdb->get_var(
699 + "SHOW INDEX FROM {$wp_group_table}
700 + WHERE Key_name = 'PRIMARY'
701 + AND Column_name = 'wp_group_id'" // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is escaped and query contains no user input.
702 + );
703 +
704 + if ( ! empty( $primary_key ) ) {
705 + delete_site_option( self::WP_GROUP_MISSING_PRIMARY_KEY_REPAIR_PENDING );
706 + } else {
707 + // A timestamp, not a formatted date: the retry throttle compares it against time().
708 + update_site_option( self::WP_GROUP_MISSING_PRIMARY_KEY_REPAIR_PENDING, time() );
709 + }
710 + }
711 +
712 + /**
713 + * Retry a pending backup progress index repair, no more than once an hour.
714 + *
715 + * A DB user without ALTER never clears the marker, so an unthrottled retry
716 + * would run two SHOW INDEX plus a failing ALTER on every single request.
717 + *
718 + * @return bool True when a repair attempt was made.
719 + */
720 + public function maybe_retry_backup_progress_index_repair() {
721 + $pending = (int) get_site_option( self::BACKUP_PROGRESS_INDEX_REPAIR_PENDING );
722 + if ( empty( $pending ) || time() - $pending < HOUR_IN_SECONDS ) {
723 + return false;
724 + }
725 +
726 + $this->repair_backup_progress_index();
727 +
728 + return true;
729 + }
730 +
731 + /**
732 + * Run the backup progress index repair and remember whether it still needs a retry.
733 + *
734 + * @return bool True when the repair is confirmed complete.
735 + */
736 + public function repair_backup_progress_index() {
737 + // The retry from install() runs outside post_update()'s suppression; a
738 + // persistently failing ALTER must not log or print on every request.
739 + $suppress = $this->wpdb->suppress_errors();
740 + $repaired = $this->drop_backup_progress_unique_index();
741 + $this->wpdb->suppress_errors( $suppress );
742 + if ( $repaired ) {
743 + delete_site_option( self::BACKUP_PROGRESS_INDEX_REPAIR_PENDING );
744 + } else {
745 + // A timestamp, not a formatted date: the retry throttle compares it against time().
746 + update_site_option( self::BACKUP_PROGRESS_INDEX_REPAIR_PENDING, time() );
747 + }
748 + return $repaired;
749 + }
750 +
751 + /**
752 + * Drop a stray unique index on the backup progress task_id column.
753 + *
754 + * Fresh installs made between DB 8.53 and the schema fix created
755 + * wp_backup_progress with UNIQUE (task_id). Progress rows are per task and
756 + * per site, so a task covering two or more sites could never insert its
757 + * second row. dbDelta never drops an index, so the stray key has to go here.
758 + *
759 + * @return bool True when no such key remains, false when one survived a failed DROP.
760 + */
761 + public function drop_backup_progress_unique_index() {
762 + $table = $this->table_name( 'wp_backup_progress' );
763 + $keys = $this->find_backup_progress_task_id_unique_keys();
764 + if ( null === $keys ) {
765 + return false;
766 + }
767 +
768 + foreach ( $keys as $key_name ) {
769 + $this->wpdb->query( "ALTER TABLE {$table} DROP INDEX `{$key_name}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL statement; table name is a hardcoded internal identifier, key name comes from SHOW INDEX and is allowlisted below.
770 + }
771 +
772 + // suppress_errors() hides a failed DROP, so the table is the only source of truth.
773 + return array() === $this->find_backup_progress_task_id_unique_keys();
774 + }
775 +
776 + /**
777 + * Names of the unique keys on the backup progress table that cover task_id alone.
778 + *
779 + * A composite unique key over task_id and wp_id is legitimate and is left out.
780 + *
781 + * @return array|null Key names, or null when the lookup itself failed and nothing can be concluded.
782 + */
783 + protected function find_backup_progress_task_id_unique_keys() {
784 + $table = $this->table_name( 'wp_backup_progress' );
785 +
786 + // suppress_errors() is on in post_update(), so a failed SHOW INDEX looks like an empty index list; last_error is the only tell.
787 + $this->wpdb->last_error = '';
788 + $indexes = $this->wpdb->get_results( "SHOW INDEX FROM {$table} WHERE Non_unique = 0", ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL introspection; table name is a hardcoded internal identifier, no user input involved.
789 + if ( '' !== $this->wpdb->last_error ) {
790 + return null;
791 + }
792 +
793 + $key_columns = array();
794 + foreach ( (array) $indexes as $index ) {
795 + if ( 'PRIMARY' === $index['Key_name'] ) {
796 + continue;
797 + }
798 + $key_columns[ $index['Key_name'] ][] = $index['Column_name'];
799 + }
800 +
801 + $keys = array();
802 + foreach ( $key_columns as $key_name => $columns ) {
803 + if ( array( 'task_id' ) === $columns && preg_match( '/^[A-Za-z0-9_]+$/', $key_name ) ) {
804 + $keys[] = $key_name;
805 + }
806 + }
807 +
808 + return $keys;
809 + }
810 +
811 + /**
812 + * Method pre_update_tables()
813 + *
814 + * Handle pre update tables.
815 + *
816 + * @return void
817 + */
818 + public function pre_update_tables() {
819 + // get_site_option is multisite aware!
820 + $currentVersion = get_site_option( $this->option_db_key );
821 +
822 + if ( false === $currentVersion ) {
823 + return;
824 + }
825 +
826 + $suppress = $this->wpdb->suppress_errors();
827 +
828 + if ( version_compare( $currentVersion, '8.98', '<=' ) ) {
829 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
830 + $existing_columns = $this->wpdb->get_col( "SHOW COLUMNS FROM {$wp_table}", 0 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL introspection; table name is a hardcoded internal identifier escaped via esc_sql(), no user input involved.
831 + foreach ( array( 'backups', 'note_lastupdate', 'pages' ) as $column ) {
832 + if ( in_array( $column, $existing_columns, true ) ) {
833 + $this->wpdb->query( 'ALTER TABLE ' . $wp_table . ' DROP COLUMN ' . esc_sql( $column ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL statement; table and column names are hardcoded internal identifiers escaped via esc_sql(), no user input involved.
834 + }
835 + }
836 + }
837 +
838 + $this->wpdb->suppress_errors( $suppress );
839 + }
840 +
841 + /**
842 + * Method post_update_81()
843 + *
844 + * Update MainWP DB for version 8.1.
845 + *
846 + * @param string $current_version Current version DB.
847 + *
848 + * @return void
849 + */
850 + public function post_update_81( $current_version ) { //phpcs:ignore -- NOSONAR - complex.
851 +
852 + if ( version_compare( $current_version, '8.1', '<' ) ) {
853 +
854 + // We can't split up here!
855 + $wpSyncColumns = array(
856 + 'version',
857 + 'totalsize',
858 + 'dbsize',
859 + 'extauth',
860 + 'last_post_gmt',
861 + 'sync_errors',
862 + 'dtsSync',
863 + 'dtsSyncStart',
864 + 'dtsAutomaticSync',
865 + 'dtsAutomaticSyncStart',
866 + );
867 + $wp_table = esc_sql( $this->table_name( 'wp' ) );
868 + $wp_sync_table = esc_sql( $this->table_name( 'wp_sync' ) );
869 + foreach ( $wpSyncColumns as $wpSyncColumn ) {
870 + $wpSyncColumn = esc_sql( $wpSyncColumn );
871 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
872 + $rslts = $this->wpdb->get_results( "SELECT id,{$wpSyncColumn} FROM {$wp_table}", ARRAY_A );
873 + if ( empty( $rslts ) ) {
874 + continue;
875 + }
876 +
877 + foreach ( $rslts as $rslt ) {
878 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name escaped via esc_sql and query uses proper prepare.
879 + $exists = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT wpid FROM {$wp_sync_table} WHERE wpid = %d", $rslt['id'] ), ARRAY_A );
880 + if ( empty( $exists ) ) {
881 + $this->wpdb->insert(
882 + $this->table_name( 'wp_sync' ),
883 + array(
884 + 'wpid' => $rslt['id'],
885 + $wpSyncColumn => $rslt[ $wpSyncColumn ],
886 + )
887 + );
888 + } else {
889 + $this->wpdb->update( $this->table_name( 'wp_sync' ), array( $wpSyncColumn => $rslt[ $wpSyncColumn ] ), array( 'wpid' => $rslt['id'] ) );
890 + }
891 + }
892 +
893 + $suppress = $this->wpdb->suppress_errors();
894 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
895 + $this->wpdb->query( "ALTER TABLE {$wp_table} DROP COLUMN {$wpSyncColumn}" );
896 + $this->wpdb->suppress_errors( $suppress );
897 + }
898 +
899 + $optionColumns = array(
900 + 'last_wp_upgrades',
901 + 'last_plugin_upgrades',
902 + 'last_theme_upgrades',
903 + 'wp_upgrades',
904 + 'recent_comments',
905 + 'recent_posts',
906 + 'recent_pages',
907 + );
908 + foreach ( $optionColumns as $optionColumn ) {
909 + $optionColumn = esc_sql( $optionColumn );
910 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
911 + $rslts = $this->wpdb->get_results( "SELECT id,{$optionColumn} FROM {$wp_table}", ARRAY_A );
912 + if ( empty( $rslts ) ) {
913 + continue;
914 + }
915 +
916 + foreach ( $rslts as $rslt ) {
917 + static::update_website_option( (object) $rslt, $optionColumn, $rslt[ $optionColumn ] );
918 + }
919 +
920 + $suppress = $this->wpdb->suppress_errors();
921 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column name escaped via esc_sql, table name escaped separately.
922 + $this->wpdb->query( "ALTER TABLE {$wp_table} DROP COLUMN {$optionColumn}" );
923 + $this->wpdb->suppress_errors( $suppress );
924 + }
925 + }
926 + }
589 927 }