PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | database/class-db-migrations.php +438 -126 2.6.20 → 2.7.2 View file →
@@ -12,8 +12,18 @@
12 12 class MetaSync_DBMigration
13 13 {
14 14
15 15 /**
16 + * Option flag marking the leftover wp-config.php copy cleanup as done.
17 + */
18 + const WPCONFIG_BACKUP_CLEANUP_OPTION = 'metasync_wpconfig_backup_cleanup_done';
19 +
20 + /**
21 + * Age in seconds before an orphaned .metasync-tmp-* file is safe to delete.
22 + */
23 + const WPCONFIG_TMP_MAX_AGE = 300;
24 +
25 + /**
16 26 * activation of migration.
17 27 */
18 28 public static function activation()
19 29 {
@@ -33,9 +43,10 @@
33 43 // Create 404 Monitor Table
34 44 require_once dirname(__FILE__, 2) . '/404-monitor/class-metasync-404-monitor-database.php';
35 45 $tableName = esc_sql($wpdb->prefix . Metasync_Error_Monitor_Database::$table_name);
36 46
37 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tableName)) != $tableName) {
47 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tableName)) != $tableName) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
48 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
38 49 $table_sql = "CREATE TABLE {$tableName} (
39 50 id BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
40 51 uri VARCHAR(255) NOT NULL,
41 52 date_time DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
@@ -51,10 +62,11 @@
51 62 // Create Redirections Table
52 63 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-database.php';
53 64 $tableNameRedirection = esc_sql($wpdb->prefix . Metasync_Redirection_Database::$table_name);
54 65
55 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameRedirection)) != $tableNameRedirection) {
56 - $table_sql = "CREATE TABLE {$tableNameRedirection} (
66 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameRedirection)) != $tableNameRedirection) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
67 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
68 + $table_sql = "CREATE TABLE {$wpdb->prefix}metasync_redirections (
57 69 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
58 70 sources_from TEXT NOT NULL,
59 71 url_redirect_to TEXT NOT NULL,
60 72 http_code SMALLINT(4) unsigned NOT NULL DEFAULT 301,
@@ -59,9 +71,9 @@
59 71 url_redirect_to TEXT NOT NULL,
60 72 http_code SMALLINT(4) unsigned NOT NULL DEFAULT 301,
61 73 hits_count BIGINT(20) unsigned NOT NULL DEFAULT '0',
62 74 status VARCHAR(25) NOT NULL DEFAULT 'active',
63 - pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
75 + pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact',
64 76 regex_pattern TEXT NULL,
65 77 description TEXT NULL,
66 78 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
67 79 updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
@@ -75,53 +87,81 @@
75 87
76 88 dbDelta($table_sql);
77 89 } else {
78 90 // Check if new columns exist and add them if they don't
79 - $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
91 + $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_redirections"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
80 92
81 93 if (!in_array('pattern_type', $columns)) {
82 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact' AFTER status");
94 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact' AFTER status"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
83 95 }
84 96
85 97 if (!in_array('regex_pattern', $columns)) {
86 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type");
98 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
87 99 }
88 100
89 101 if (!in_array('description', $columns)) {
90 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN description TEXT NULL AFTER regex_pattern");
102 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN description TEXT NULL AFTER regex_pattern"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
91 103 }
92 104
93 105 // Add indexes if they don't exist
94 - $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameRedirection}");
106 + $indexes = $wpdb->get_results("SHOW INDEX FROM {$wpdb->prefix}metasync_redirections"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
95 107 $index_names = array_column($indexes, 'Key_name');
96 108
97 109 if (!in_array('pattern_type', $index_names)) {
98 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY pattern_type (pattern_type)");
110 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD KEY pattern_type (pattern_type)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
99 111 }
100 112
101 113 if (!in_array('created_at', $index_names)) {
102 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY created_at (created_at)");
114 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD KEY created_at (created_at)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
103 115 }
104 116
105 117 // PERFORMANCE OPTIMIZATION: Add composite index for active redirects lookup
106 118 if (!in_array('idx_active_redirects', $index_names)) {
107 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY idx_active_redirects (status, sources_from(191))");
119 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD KEY idx_active_redirects (status, sources_from(191))"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
108 120 }
109 121
110 122 // Set default pattern_type for existing records
111 - $wpdb->query("UPDATE {$tableNameRedirection} SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''");
123 + $wpdb->query("UPDATE {$wpdb->prefix}metasync_redirections SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
112 124 }
113 125
114 126 // One-time migration: auto-enable external redirects if the site already has any
115 127 if (!get_option('metasync_external_redirects_migrated')) {
116 - $home_prefix = $wpdb->esc_like(trailingslashit(home_url()));
117 - $external_count = $wpdb->get_var(
118 - $wpdb->prepare(
119 - "SELECT COUNT(*) FROM {$tableNameRedirection} WHERE url_redirect_to LIKE %s AND url_redirect_to NOT LIKE %s",
120 - 'http%',
121 - $home_prefix . '%'
122 - )
123 - );
128 + // Compare against every home-URL variant (http/https, www/non-www),
129 + // not just the exact home_url() string — otherwise same-site
130 + // destinations such as 'https://example.com/about' on a
131 + // www-prefixed site count as external and silently flip the setting.
132 + $home_host = wp_parse_url(home_url(), PHP_URL_HOST);
133 + $home_host = is_string($home_host) ? strtolower(preg_replace('/^www\./i', '', $home_host)) : '';
134 +
135 + if ($home_host !== '') {
136 + // NOT LIKE per home-URL variant, preserving the original
137 + // scheme × host ordering (http/https × bare/www).
138 + $external_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
139 + $wpdb->prepare(
140 + "SELECT COUNT(*) FROM {$wpdb->prefix}metasync_redirections
141 + WHERE url_redirect_to LIKE %s
142 + AND url_redirect_to NOT LIKE %s
143 + AND url_redirect_to NOT LIKE %s
144 + AND url_redirect_to NOT LIKE %s
145 + AND url_redirect_to NOT LIKE %s",
146 + 'http%',
147 + $wpdb->esc_like('http://' . $home_host) . '%',
148 + $wpdb->esc_like('http://www.' . $home_host) . '%',
149 + $wpdb->esc_like('https://' . $home_host) . '%',
150 + $wpdb->esc_like('https://www.' . $home_host) . '%'
151 + )
152 + );
153 + } else {
154 + $external_count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
155 + $wpdb->prepare(
156 + "SELECT COUNT(*) FROM {$wpdb->prefix}metasync_redirections
157 + WHERE url_redirect_to LIKE %s
158 + AND url_redirect_to NOT LIKE %s",
159 + 'http%',
160 + $wpdb->esc_like(trailingslashit(home_url())) . '%'
161 + )
162 + );
163 + }
124 164 if ($external_count > 0) {
125 165 update_option('metasync_allow_external_redirects', 1, true);
126 166 }
127 167 update_option('metasync_external_redirects_migrated', 1, true);
@@ -130,9 +170,10 @@
130 170 // Create HeartBeat Error Monitor Table
131 171 require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
132 172 $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
133 173
134 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameHeartBeatErrorMonitor)) != $tableNameHeartBeatErrorMonitor) {
174 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameHeartBeatErrorMonitor)) != $tableNameHeartBeatErrorMonitor) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
175 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
135 176 $table_sql = "CREATE TABLE {$tableNameHeartBeatErrorMonitor} (
136 177 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
137 178 attribute_name VARCHAR(25) NOT NULL DEFAULT '',
138 179 object_count VARCHAR(25) NOT NULL DEFAULT '',
@@ -143,14 +184,38 @@
143 184
144 185 dbDelta($table_sql);
145 186 }
146 187
147 - // Create Sync History Table
188 + // Create Headless Refresh History Table
189 + require_once dirname(__FILE__, 2) . '/headless-refresh-history/class-metasync-headless-refresh-history-database.php';
190 + $tableNameHeadlessRefreshHistory = esc_sql($wpdb->prefix . Metasync_Headless_Refresh_History::$table_name);
191 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameHeadlessRefreshHistory)) != $tableNameHeadlessRefreshHistory) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
192 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
193 + $table_sql = "CREATE TABLE {$tableNameHeadlessRefreshHistory} (
194 + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
195 + object_type VARCHAR(20) NOT NULL DEFAULT '',
196 + object_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
197 + url_hash CHAR(64) NOT NULL DEFAULT '',
198 + outcome VARCHAR(32) NOT NULL DEFAULT '',
199 + reason VARCHAR(64) NOT NULL DEFAULT '',
200 + duration_ms INT UNSIGNED NOT NULL DEFAULT 0,
201 + object_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
202 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
203 + PRIMARY KEY (id),
204 + KEY idx_created_at (created_at),
205 + KEY idx_object (object_type, object_id, created_at),
206 + KEY idx_outcome (outcome, created_at)
207 + ) $collate;";
208 + dbDelta($table_sql);
209 + }
210 +
211 + // Create Sync History Table
148 212 require_once dirname(__FILE__, 2) . '/sync-history/class-metasync-sync-history-database.php';
149 213 $tableNameSyncHistory = esc_sql($wpdb->prefix . Metasync_Sync_History_Database::$table_name);
150 214
151 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameSyncHistory)) != $tableNameSyncHistory) {
152 - $table_sql = "CREATE TABLE {$tableNameSyncHistory} (
215 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameSyncHistory)) != $tableNameSyncHistory) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
216 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
217 + $table_sql = "CREATE TABLE {$wpdb->prefix}metasync_sync_history (
153 218 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
154 219 title VARCHAR(255) NOT NULL DEFAULT '',
155 220 source VARCHAR(50) NOT NULL DEFAULT '',
156 221 status VARCHAR(25) NOT NULL DEFAULT 'draft',
@@ -169,19 +234,19 @@
169 234 dbDelta($table_sql);
170 235 } else {
171 236 // PERFORMANCE OPTIMIZATION: Add composite indexes to existing tables
172 237 // Check and add indexes if they don't exist
173 - $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameSyncHistory}");
238 + $indexes = $wpdb->get_results("SHOW INDEX FROM {$wpdb->prefix}metasync_sync_history"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
174 239 $index_names = array_column($indexes, 'Key_name');
175 240
176 241 // Add deduplication index (source, created_at)
177 242 if (!in_array('idx_dedup', $index_names)) {
178 - $wpdb->query("ALTER TABLE {$tableNameSyncHistory} ADD KEY idx_dedup (source, created_at)");
243 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_sync_history ADD KEY idx_dedup (source, created_at)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
179 244 }
180 245
181 246 // Add search index (title(50), source, created_at)
182 247 if (!in_array('idx_search', $index_names)) {
183 - $wpdb->query("ALTER TABLE {$tableNameSyncHistory} ADD KEY idx_search (title(50), source, created_at)");
248 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_sync_history ADD KEY idx_search (title(50), source, created_at)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
184 249 }
185 250 }
186 251
187 252 // Create OTTO Excluded URLs Table
@@ -187,10 +252,11 @@
187 252 // Create OTTO Excluded URLs Table
188 253 require_once dirname(__FILE__, 2) . '/otto/class-metasync-otto-excluded-urls-database.php';
189 254 $tableNameOttoExcludedURLs = esc_sql($wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name);
190 255
191 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) {
192 - $table_sql = "CREATE TABLE {$tableNameOttoExcludedURLs} (
256 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
257 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
258 + $table_sql = "CREATE TABLE {$wpdb->prefix}metasync_otto_excluded_urls (
193 259 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
194 260 url_pattern TEXT NOT NULL,
195 261 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
196 262 description TEXT NULL,
@@ -215,9 +281,9 @@
215 281 // Create Robots.txt Backups Table
216 282 require_once dirname(__FILE__, 2) . '/robots-txt/class-metasync-robots-txt-database.php';
217 283 $robots_db = Metasync_Robots_Txt_Database::get_instance();
218 284 $table_name_robots = esc_sql($wpdb->prefix . 'metasync_robots_txt_backups');
219 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name_robots)) != $table_name_robots) {
285 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name_robots)) != $table_name_robots) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
220 286 $robots_db->create_table();
221 287 }
222 288
223 289 }
@@ -241,13 +307,16 @@
241 307 /* drop wp_metasync_redirections table */
242 308 // $sql = "DROP TABLE IF EXISTS `$tableNameRedirection` ";
243 309 // $wpdb->query($sql);
244 310
245 - require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
246 - $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
247 - /* drop wp_metasync_redirections table */
248 - $sql = "DROP TABLE IF EXISTS `$tableNameHeartBeatErrorMonitor` ";
249 - $wpdb->query($sql);
311 + // Data tables are preserved on deactivation so a deactivate/reactivate cycle
312 + // does not silently destroy logged data (e.g. heartbeat error history).
313 + // Removal belongs to uninstall, not deactivation.
314 + // require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
315 + // $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
316 + /* drop wp_metasync_heartbeat_error_logs table */
317 + // $sql = "DROP TABLE IF EXISTS `$tableNameHeartBeatErrorMonitor` ";
318 + // $wpdb->query($sql);
250 319 }
251 320
252 321 /**
253 322 * Run version-specific migrations
@@ -271,8 +340,13 @@
271 340 if ($force_run || version_compare($to_version, '2.5.9', '>=')) {
272 341 self::migrate_otto_excluded_urls_v2_5_9();
273 342 }
274 343
344 + // Migration for versions 2.5.20+ - Remove insecure wp-config.php backup copies from the web root
345 + if ($force_run || version_compare($to_version, '2.5.20', '>=')) {
346 + self::migrate_remove_wpconfig_backups_v2_5_20();
347 + }
348 +
275 349 // Add more version-specific migrations here as needed
276 350 // if (version_compare($from_version, '1.1.0', '<')) {
277 351 // self::migrate_something_v1_1();
278 352 // }
@@ -292,11 +366,12 @@
292 366 // Enhanced 404 Error Monitor Table
293 367 require_once dirname(__FILE__, 2) . '/404-monitor/class-metasync-404-monitor-database.php';
294 368 $tableName404Monitor = esc_sql($wpdb->prefix . Metasync_Error_Monitor_Database::$table_name);
295 369
296 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableName404Monitor)) != $tableName404Monitor) {
370 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableName404Monitor)) != $tableName404Monitor) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
297 371 // Table doesn't exist, create enhanced version
298 - $table_sql = "CREATE TABLE {$tableName404Monitor} (
372 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
373 + $table_sql = "CREATE TABLE {$wpdb->prefix}metasync_404_logs (
299 374 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
300 375 uri TEXT NOT NULL,
301 376 hits_count BIGINT(20) unsigned NOT NULL DEFAULT '1',
302 377 date_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -311,42 +386,42 @@
311 386
312 387 dbDelta($table_sql);
313 388 } else {
314 389 // Table exists, check for missing columns and add them
315 - $columns = $wpdb->get_col("DESCRIBE {$tableName404Monitor}");
390 + $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_404_logs"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
316 391
317 392 // Add referer column if it doesn't exist
318 393 if (!in_array('referer', $columns)) {
319 - $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD COLUMN referer TEXT NULL AFTER user_agent");
394 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_404_logs ADD COLUMN referer TEXT NULL AFTER user_agent"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
320 395 }
321 396
322 397 // Add ip_address column if it doesn't exist
323 398 if (!in_array('ip_address', $columns)) {
324 - $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD COLUMN ip_address VARCHAR(45) NULL AFTER referer");
399 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_404_logs ADD COLUMN ip_address VARCHAR(45) NULL AFTER referer"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
325 400 }
326 401
327 402 // Update uri column to TEXT if it's VARCHAR(255)
328 - $uri_column = $wpdb->get_row("SHOW COLUMNS FROM {$tableName404Monitor} LIKE 'uri'");
403 + $uri_column = $wpdb->get_row("SHOW COLUMNS FROM {$wpdb->prefix}metasync_404_logs LIKE 'uri'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
329 404 if ($uri_column && strpos($uri_column->Type, 'varchar') !== false) {
330 - $wpdb->query("ALTER TABLE {$tableName404Monitor} MODIFY COLUMN uri TEXT NOT NULL");
405 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_404_logs MODIFY COLUMN uri TEXT NOT NULL"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
331 406 }
332 407
333 408 // Update user_agent column to TEXT if it's VARCHAR(255)
334 - $ua_column = $wpdb->get_row("SHOW COLUMNS FROM {$tableName404Monitor} LIKE 'user_agent'");
409 + $ua_column = $wpdb->get_row("SHOW COLUMNS FROM {$wpdb->prefix}metasync_404_logs LIKE 'user_agent'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
335 410 if ($ua_column && strpos($ua_column->Type, 'varchar') !== false) {
336 - $wpdb->query("ALTER TABLE {$tableName404Monitor} MODIFY COLUMN user_agent TEXT NULL");
411 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_404_logs MODIFY COLUMN user_agent TEXT NULL"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
337 412 }
338 413
339 414 // Add missing indexes
340 - $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableName404Monitor}");
415 + $indexes = $wpdb->get_results("SHOW INDEX FROM {$wpdb->prefix}metasync_404_logs"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
341 416 $index_names = array_column($indexes, 'Key_name');
342 417
343 418 if (!in_array('hits_count', $index_names)) {
344 - $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD KEY hits_count (hits_count)");
419 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_404_logs ADD KEY hits_count (hits_count)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
345 420 }
346 421
347 422 if (!in_array('date_time', $index_names)) {
348 - $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD KEY date_time (date_time)");
423 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_404_logs ADD KEY date_time (date_time)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
349 424 }
350 425 }
351 426
352 427 // Enhanced Redirections Table with new columns
@@ -352,54 +427,54 @@
352 427 // Enhanced Redirections Table with new columns
353 428 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-database.php';
354 429 $tableNameRedirection = esc_sql($wpdb->prefix . Metasync_Redirection_Database::$table_name);
355 430
356 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameRedirection)) == $tableNameRedirection) {
431 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameRedirection)) == $tableNameRedirection) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
357 432 // Table exists, check for new columns
358 - $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
433 + $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_redirections");
359 434
360 435 // Add pattern_type column if it doesn't exist
361 436 if (!in_array('pattern_type', $columns)) {
362 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact' AFTER status");
437 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact' AFTER status"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
363 438 }
364 439
365 440 // Add regex_pattern column if it doesn't exist
366 441 if (!in_array('regex_pattern', $columns)) {
367 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type");
442 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
368 443 }
369 444
370 445 // Add description column if it doesn't exist
371 446 if (!in_array('description', $columns)) {
372 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN description TEXT NULL AFTER regex_pattern");
447 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN description TEXT NULL AFTER regex_pattern"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
373 448 }
374 449
375 450 // Add timestamp columns if they don't exist
376 451 if (!in_array('created_at', $columns)) {
377 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER description");
452 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER description"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
378 453 }
379 454
380 455 if (!in_array('updated_at', $columns)) {
381 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER created_at");
456 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER created_at"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
382 457 }
383 458
384 459 if (!in_array('last_accessed_at', $columns)) {
385 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN last_accessed_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER updated_at");
460 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD COLUMN last_accessed_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER updated_at"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
386 461 }
387 462
388 463 // Add indexes if they don't exist
389 - $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameRedirection}");
464 + $indexes = $wpdb->get_results("SHOW INDEX FROM {$wpdb->prefix}metasync_redirections"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
390 465 $index_names = array_column($indexes, 'Key_name');
391 466
392 467 if (!in_array('pattern_type', $index_names)) {
393 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY pattern_type (pattern_type)");
468 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD KEY pattern_type (pattern_type)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
394 469 }
395 470
396 471 if (!in_array('created_at', $index_names)) {
397 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY created_at (created_at)");
472 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_redirections ADD KEY created_at (created_at)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
398 473 }
399 474
400 475 // Set default pattern_type for existing records
401 - $wpdb->query("UPDATE {$tableNameRedirection} SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''");
476 + $wpdb->query("UPDATE {$wpdb->prefix}metasync_redirections SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
402 477 }
403 478 }
404 479
405 480 /**
@@ -414,9 +489,9 @@
414 489 $robots_db = Metasync_Robots_Txt_Database::get_instance();
415 490 $table_name = esc_sql($wpdb->prefix . 'metasync_robots_txt_backups');
416 491
417 492 // Check if table already exists
418 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
493 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
419 494 // Table doesn't exist, create it
420 495 $robots_db->create_table();
421 496 }
422 497 }
@@ -436,11 +511,12 @@
436 511 require_once dirname(__FILE__, 2) . '/otto/class-metasync-otto-excluded-urls-database.php';
437 512 $tableNameOttoExcludedURLs = esc_sql($wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name);
438 513
439 514 // Check if table already exists
440 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) {
515 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
441 516 // Table doesn't exist, create it
442 - $table_sql = "CREATE TABLE {$tableNameOttoExcludedURLs} (
517 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
518 + $table_sql = "CREATE TABLE {$wpdb->prefix}metasync_otto_excluded_urls (
443 519 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
444 520 url_pattern TEXT NOT NULL,
445 521 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
446 522 description TEXT NULL,
@@ -465,72 +541,72 @@
465 541 // Log successful migration
466 542 // error_log('MetaSync: OTTO Excluded URLs table created successfully (v2.5.9)');
467 543 } else {
468 544 // Table exists, verify structure and add any missing columns if needed
469 - $columns = $wpdb->get_col("DESCRIBE {$tableNameOttoExcludedURLs}");
545 + $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_otto_excluded_urls"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
470 546
471 547 // Check for required columns and add if missing
472 548 $missing_columns = false;
473 549
474 550 if (!in_array('pattern_type', $columns)) {
475 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact' AFTER url_pattern");
551 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact' AFTER url_pattern"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
476 552 $missing_columns = true;
477 553 }
478 554
479 555 if (!in_array('description', $columns)) {
480 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN description TEXT NULL AFTER pattern_type");
556 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD COLUMN description TEXT NULL AFTER pattern_type"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
481 557 $missing_columns = true;
482 558 }
483 559
484 560 if (!in_array('status', $columns)) {
485 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN status VARCHAR(25) NOT NULL DEFAULT 'active' AFTER description");
561 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD COLUMN status VARCHAR(25) NOT NULL DEFAULT 'active' AFTER description"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
486 562 $missing_columns = true;
487 563 }
488 564
489 565 if (!in_array('is_permanent', $columns)) {
490 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN is_permanent TINYINT(1) NOT NULL DEFAULT 0 AFTER status");
491 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY is_permanent (is_permanent)");
566 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD COLUMN is_permanent TINYINT(1) NOT NULL DEFAULT 0 AFTER status"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
567 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY is_permanent (is_permanent)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
492 568 }
493 569
494 570 if (!in_array('auto_excluded', $columns)) {
495 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN auto_excluded TINYINT(1) NOT NULL DEFAULT 0 AFTER is_permanent");
496 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY auto_excluded (auto_excluded)");
571 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD COLUMN auto_excluded TINYINT(1) NOT NULL DEFAULT 0 AFTER is_permanent"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
572 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY auto_excluded (auto_excluded)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
497 573 // Backfill: mark existing 404 exclusions as auto_excluded
498 - $wpdb->query("UPDATE {$tableNameOttoExcludedURLs} SET auto_excluded = 1 WHERE description = 'Auto-excluded: 404'");
574 + $wpdb->query("UPDATE {$wpdb->prefix}metasync_otto_excluded_urls SET auto_excluded = 1 WHERE description = 'Auto-excluded: 404'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
499 575 }
500 576
501 577 if (!in_array('recheck_after', $columns)) {
502 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN recheck_after DATETIME NULL DEFAULT NULL AFTER auto_excluded");
503 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY recheck_after (recheck_after)");
578 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD COLUMN recheck_after DATETIME NULL DEFAULT NULL AFTER auto_excluded"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
579 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY recheck_after (recheck_after)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
504 580 // Backfill: set recheck_after = created_at + 7 days for auto-excluded URLs
505 - $wpdb->query("UPDATE {$tableNameOttoExcludedURLs} SET recheck_after = DATE_ADD(created_at, INTERVAL 7 DAY) WHERE auto_excluded = 1 AND (recheck_after IS NULL OR recheck_after = '0000-00-00 00:00:00')");
581 + $wpdb->query("UPDATE {$wpdb->prefix}metasync_otto_excluded_urls SET recheck_after = DATE_ADD(created_at, INTERVAL 7 DAY) WHERE auto_excluded = 1 AND (recheck_after IS NULL OR recheck_after = '0000-00-00 00:00:00')"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
506 582 }
507 583
508 584 // Check and add indexes if they don't exist
509 - $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameOttoExcludedURLs}");
585 + $indexes = $wpdb->get_results("SHOW INDEX FROM {$wpdb->prefix}metasync_otto_excluded_urls"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
510 586 $index_names = array_column($indexes, 'Key_name');
511 587
512 588 if (!in_array('status', $index_names)) {
513 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY status (status)");
589 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY status (status)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
514 590 }
515 591
516 592 if (!in_array('pattern_type', $index_names)) {
517 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY pattern_type (pattern_type)");
593 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY pattern_type (pattern_type)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
518 594 }
519 595
520 596 if (!in_array('created_at', $index_names)) {
521 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY created_at (created_at)");
597 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY created_at (created_at)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
522 598 }
523 599
524 600 // Add unique index on url_pattern + pattern_type to prevent duplicates at database level
525 601 // Note: TEXT columns need a prefix length for indexing (767 is max for UTF8)
526 602 if (!in_array('url_pattern_type_unique', $index_names)) {
527 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD UNIQUE KEY url_pattern_type_unique (url_pattern(191), pattern_type)");
603 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD UNIQUE KEY url_pattern_type_unique (url_pattern(191), pattern_type)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
528 604 }
529 605
530 606 // Composite index for the cache-miss path in metasync_is_otto_url_manually_excluded()
531 607 if (!in_array('status_auto_excluded', $index_names)) {
532 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY status_auto_excluded (status, auto_excluded)");
608 + $wpdb->query("ALTER TABLE {$wpdb->prefix}metasync_otto_excluded_urls ADD KEY status_auto_excluded (status, auto_excluded)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
533 609 }
534 610
535 611 // if ($missing_columns) {
536 612 // error_log('MetaSync: OTTO Excluded URLs table structure updated (v2.5.9)');
@@ -554,46 +630,75 @@
554 630
555 631 $meta_keys = array('meta_canonical', '_metasync_canonical_url', '_yoast_wpseo_canonical', 'rank_math_canonical_url');
556 632 $bad_values = array('array', 'http://array', 'https://array');
557 633
558 - $keys_placeholders = implode(',', array_fill(0, count($meta_keys), '%s'));
559 - $vals_placeholders = implode(',', array_fill(0, count($bad_values), '%s'));
560 -
561 - // Normalized comparison: trailing slashes stripped in SQL so
562 - // "http://Array/" and "http://Array//" both match the literals.
563 - $norm_meta = "LOWER(TRIM(TRAILING '/' FROM TRIM(meta_value)))";
564 -
565 634 // 1. Post meta + term meta: delete exact-match corrupted rows.
566 635 // Batched and deleted by primary key so huge postmeta tables aren't
567 636 // range-locked in one statement, with per-object meta-cache
568 637 // invalidation — raw SQL alone would leave persistent object caches
569 638 // (Redis/Memcached) serving the deleted value to Yoast/RankMath
570 - // readers indefinitely.
571 - $meta_targets = array(
572 - array($wpdb->postmeta, 'post_id', 'post_meta'),
573 - array($wpdb->termmeta, 'term_id', 'term_meta'),
574 - );
575 - foreach ($meta_targets as $target) {
576 - list($table, $object_col, $cache_group) = $target;
577 - for ($batch = 0; $batch < 50; $batch++) {
578 - $rows = $wpdb->get_results($wpdb->prepare(
579 - "SELECT meta_id, {$object_col} AS object_id FROM {$table} WHERE meta_key IN ({$keys_placeholders}) AND {$norm_meta} IN ({$vals_placeholders}) ORDER BY meta_id LIMIT 500",
580 - array_merge($meta_keys, $bad_values)
581 - ));
582 - if (empty($rows)) {
583 - break;
584 - }
585 - $meta_ids = implode(',', array_map('intval', wp_list_pluck($rows, 'meta_id')));
586 - $wpdb->query("DELETE FROM {$table} WHERE meta_id IN ({$meta_ids})");
587 - foreach ($rows as $row) {
588 - wp_cache_delete((int) $row->object_id, $cache_group);
589 - }
590 - if (count($rows) < 500) {
591 - break;
592 - }
639 + // readers indefinitely. The two targets are written out explicitly
640 + // so every table/column identifier is a literal in the statement.
641 + //
642 + // Normalized comparison: trailing slashes stripped in SQL so
643 + // "http://Array/" and "http://Array//" both match the literals.
644 +
645 + // 1a. Post meta.
646 + for ($batch = 0; $batch < 50; $batch++) {
647 + $rows = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
648 + 'SELECT meta_id, post_id AS object_id FROM ' . $wpdb->postmeta
649 + . ' WHERE meta_key IN (' . implode(',', array_fill(0, count($meta_keys), '%s')) . ')'
650 + . " AND LOWER(TRIM(TRAILING '/' FROM TRIM(meta_value)))"
651 + . ' IN (' . implode(',', array_fill(0, count($bad_values), '%s')) . ')'
652 + . ' ORDER BY meta_id LIMIT 500',
653 + array_merge($meta_keys, $bad_values)
654 + ));
655 + if (empty($rows)) {
656 + break;
593 657 }
658 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
659 + $wpdb->prepare(
660 + 'DELETE FROM ' . $wpdb->postmeta
661 + . ' WHERE meta_id IN (' . implode(',', array_fill(0, count($rows), '%d')) . ')',
662 + wp_list_pluck($rows, 'meta_id')
663 + )
664 + );
665 + foreach ($rows as $row) {
666 + wp_cache_delete((int) $row->object_id, 'post_meta');
667 + }
668 + if (count($rows) < 500) {
669 + break;
670 + }
594 671 }
595 672
673 + // 1b. Term meta.
674 + for ($batch = 0; $batch < 50; $batch++) {
675 + $rows = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
676 + 'SELECT meta_id, term_id AS object_id FROM ' . $wpdb->termmeta
677 + . ' WHERE meta_key IN (' . implode(',', array_fill(0, count($meta_keys), '%s')) . ')'
678 + . " AND LOWER(TRIM(TRAILING '/' FROM TRIM(meta_value)))"
679 + . ' IN (' . implode(',', array_fill(0, count($bad_values), '%s')) . ')'
680 + . ' ORDER BY meta_id LIMIT 500',
681 + array_merge($meta_keys, $bad_values)
682 + ));
683 + if (empty($rows)) {
684 + break;
685 + }
686 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
687 + $wpdb->prepare(
688 + 'DELETE FROM ' . $wpdb->termmeta
689 + . ' WHERE meta_id IN (' . implode(',', array_fill(0, count($rows), '%d')) . ')',
690 + wp_list_pluck($rows, 'meta_id')
691 + )
692 + );
693 + foreach ($rows as $row) {
694 + wp_cache_delete((int) $row->object_id, 'term_meta');
695 + }
696 + if (count($rows) < 500) {
697 + break;
698 + }
699 + }
700 +
596 701 // 2. Rows still stored as serialized arrays (the raw material the
597 702 // "Array" casts came from): repair MetaSync's own keys to the first
598 703 // usable URL inside; third-party keys are delete-only (never invent
599 704 // a value inside another plugin's storage). Written with direct SQL
@@ -602,11 +707,14 @@
602 707 // row; loops until exhausted (repaired rows stop matching LIKE).
603 708 if (class_exists('Metasync_Canonical_Sanitizer')) {
604 709 $own_keys = array('meta_canonical', '_metasync_canonical_url');
605 710 for ($batch = 0; $batch < 50; $batch++) {
606 - $rows = $wpdb->get_results($wpdb->prepare(
607 - "SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key IN ({$keys_placeholders}) AND meta_value LIKE 'a:%%' ORDER BY meta_id LIMIT 500",
608 - $meta_keys
711 + $rows = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
712 + 'SELECT meta_id, post_id, meta_key, meta_value FROM ' . $wpdb->postmeta
713 + . ' WHERE meta_key IN (' . implode(',', array_fill(0, count($meta_keys), '%s')) . ')'
714 + . " AND meta_value LIKE %s"
715 + . ' ORDER BY meta_id LIMIT 500',
716 + array_merge($meta_keys, array('a:%'))
609 717 ));
610 718 if (empty($rows)) {
611 719 break;
612 720 }
@@ -615,11 +723,11 @@
615 723 if (in_array($row->meta_key, $own_keys, true)) {
616 724 $repaired = Metasync_Canonical_Sanitizer::sanitize(maybe_unserialize($row->meta_value));
617 725 }
618 726 if ($repaired !== '') {
619 - $wpdb->update($wpdb->postmeta, array('meta_value' => $repaired), array('meta_id' => (int) $row->meta_id));
727 + $wpdb->update($wpdb->postmeta, array('meta_value' => $repaired), array('meta_id' => (int) $row->meta_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
620 728 } else {
621 - $wpdb->delete($wpdb->postmeta, array('meta_id' => (int) $row->meta_id));
729 + $wpdb->delete($wpdb->postmeta, array('meta_id' => (int) $row->meta_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
622 730 }
623 731 wp_cache_delete((int) $row->post_id, 'post_meta');
624 732 }
625 733 if (count($rows) < 500) {
@@ -630,25 +738,37 @@
630 738
631 739 // 3. Yoast indexable cache: null corrupted canonical columns so the
632 740 // frontend and sitemaps stop serving the bad value immediately.
633 741 $indexable_table = $wpdb->prefix . 'yoast_indexable';
634 - if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($indexable_table))) === $indexable_table) {
742 + if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($indexable_table))) === $indexable_table) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
635 743 $wpdb->query($wpdb->prepare(
636 - "UPDATE {$indexable_table} SET canonical = NULL WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical))) IN ({$vals_placeholders})",
744 + 'UPDATE ' . $wpdb->prefix . 'yoast_indexable SET canonical = NULL'
745 + . " WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical)))"
746 + . ' IN (' . implode(',', array_fill(0, count($bad_values), '%s')) . ')',
637 747 $bad_values
638 748 ));
639 749 }
640 750
641 751 // 4. AIOSEO custom tables: null corrupted canonical_url columns.
642 - foreach (array('aioseo_posts', 'aioseo_terms') as $aioseo_table) {
643 - $table = $wpdb->prefix . $aioseo_table;
644 - if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table))) === $table) {
645 - $wpdb->query($wpdb->prepare(
646 - "UPDATE {$table} SET canonical_url = NULL WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical_url))) IN ({$vals_placeholders})",
647 - $bad_values
648 - ));
649 - }
752 + // Both table names are fixed literals, written out explicitly.
753 + $aioseo_posts = $wpdb->prefix . 'aioseo_posts';
754 + if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($aioseo_posts))) === $aioseo_posts) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
755 + $wpdb->query($wpdb->prepare(
756 + 'UPDATE ' . $wpdb->prefix . 'aioseo_posts SET canonical_url = NULL'
757 + . " WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical_url)))"
758 + . ' IN (' . implode(',', array_fill(0, count($bad_values), '%s')) . ')',
759 + $bad_values
760 + ));
650 761 }
762 + $aioseo_terms = $wpdb->prefix . 'aioseo_terms';
763 + if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($aioseo_terms))) === $aioseo_terms) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- activation/upgrade migration routine — schema changes are gated to activation and version bumps, never ordinary requests
764 + $wpdb->query($wpdb->prepare(
765 + 'UPDATE ' . $wpdb->prefix . 'aioseo_terms SET canonical_url = NULL'
766 + . " WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical_url)))"
767 + . ' IN (' . implode(',', array_fill(0, count($bad_values), '%s')) . ')',
768 + $bad_values
769 + ));
770 + }
651 771
652 772 // 5. Yoast stores term canonicals in the wpseo_taxonomy_meta option,
653 773 // not termmeta. Strip only exact corruption literals.
654 774 if (class_exists('Metasync_Canonical_Sanitizer')) {
@@ -671,8 +791,200 @@
671 791 update_option('wpseo_taxonomy_meta', $tax_meta);
672 792 }
673 793 }
674 794 }
795 + }
796 +
797 + /**
798 + * One-time repair of Local Business logo values corrupted to
799 + * "http://<attachment-id>" (and the https:// / trailing-slash variants).
800 + *
801 + * The legacy save path ran the logo through
802 + * sanitize_url() (= esc_url_raw()), which prepends a scheme to any value
803 + * that has none — so a stored attachment ID "45589" became "http://45589",
804 + * the admin preview rendered a broken image, and the front-end JSON-LD
805 + * published "logo": "http://45589" as structured data.
806 + *
807 + * The corrupted shape is unambiguous (a scheme followed by digits only — no
808 + * dot, no path, so it can never be a resolvable host) and encodes the
809 + * original ID exactly, so restoring the digits is lossless and needs no
810 + * rollback path. Idempotent by construction: repaired values no longer
811 + * match, so a second run writes nothing.
812 + *
813 + * Uses the same shared helper as the admin preview and the schema output
814 + * (metasync_repair_scheme_prefixed_media_id()), so the three can never
815 + * disagree about what "corrupted" means.
816 + *
817 + * @see metasync_repair_scheme_prefixed_media_id()
818 + */
819 + public static function repair_corrupted_local_seo_logo()
820 + {
821 + $options = get_option('metasync_options');
822 +
823 + if (!is_array($options) || !isset($options['localseo']['local_seo_logo'])) {
824 + return;
825 + }
826 +
827 + $stored = $options['localseo']['local_seo_logo'];
828 + $repaired = metasync_repair_scheme_prefixed_media_id($stored);
829 +
830 + if ($repaired === $stored) {
831 + return; // already clean: URL, plain attachment ID, or empty
832 + }
833 +
834 + $options['localseo']['local_seo_logo'] = $repaired;
835 + update_option('metasync_options', $options, true);
836 + }
837 +
838 +
839 + /**
840 + * Remove insecure wp-config.php backup copies left in the web root by prior versions.
841 + *
842 + * Older versions wrote a full copy of wp-config.php (containing DB credentials and
843 + * auth salts) to `wp-config.php.metasync-backup-<time()>` in the same directory as
844 + * wp-config.php before each debug-mode write. This cleanup deletes any such leftover
845 + * copies, plus any `.metasync-tmp-*` file orphaned by an interrupted atomic save.
846 + *
847 + * Claimed via an option so it runs once rather than on every later version bump, and
848 + * left unclaimed if anything could not be deleted so a later upgrade retries.
849 + *
850 + * @return void
851 + */
852 + private static function migrate_remove_wpconfig_backups_v2_5_20()
853 + {
854 + if (get_option(self::WPCONFIG_BACKUP_CLEANUP_OPTION)) {
855 + return;
856 + }
857 +
858 + // This runs from `init` at priority 1, so a wp_dlct_config_file_manager_path filter
859 + // another plugin registers at the default priority is not added yet and the filtered
860 + // value can still be the default. Sweep every candidate directory rather than
861 + // trusting one resolution, and only claim the cleanup once wp-config.php was
862 + // actually found in one of them - otherwise a later upgrade retries.
863 + $candidates = [
864 + ABSPATH . 'wp-config.php',
865 + dirname(ABSPATH) . '/wp-config.php',
866 + apply_filters('wp_dlct_config_file_manager_path', ABSPATH . 'wp-config.php'),
867 + ];
868 +
869 + $sweptDirs = [];
870 + $located = false;
871 + $failed = 0;
872 + $scan_failed = 0;
873 +
874 + foreach ($candidates as $config_file) {
875 + if (!is_string($config_file) || '' === $config_file) {
876 + continue;
877 + }
878 +
879 + $config_dir = dirname($config_file);
880 + if (isset($sweptDirs[$config_dir])) {
881 + continue;
882 + }
883 + $sweptDirs[$config_dir] = true;
884 +
885 + if (@file_exists($config_file)) {
886 + $located = true;
887 + }
888 +
889 + $result = self::remove_wpconfig_backups($config_dir);
890 + $failed += $result['failed'];
891 + $scan_failed += $result['scan_failed'];
892 + }
893 +
894 + if ($scan_failed) {
895 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
896 + 'MetaSync: could not inspect %d wp-config.php backup directory scan(s); cleanup will be retried.',
897 + $scan_failed
898 + ));
899 + return;
900 + }
901 +
902 + if ($failed) {
903 + // A copy left behind is exactly the exposure this cleanup exists to remove,
904 + // so surface it rather than failing silently.
905 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
906 + 'MetaSync: could not delete %d leftover wp-config.php copy/copies in %s - remove them manually.',
907 + $failed,
908 + implode(', ', array_keys($sweptDirs))
909 + ));
910 + return;
911 + }
912 +
913 + if (!$located) {
914 + // wp-config.php was not in any directory we swept, so it is relocated somewhere
915 + // we could not resolve. Leave the flag unset so a later upgrade tries again.
916 + error_log('MetaSync: wp-config.php was not found while cleaning up legacy backup copies in ' // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
917 + . implode(', ', array_keys($sweptDirs)) . ' - cleanup will be retried.');
918 + return;
919 + }
920 +
921 + update_option(self::WPCONFIG_BACKUP_CLEANUP_OPTION, 1, false);
922 + }
923 +
924 + /**
925 + * Delete leftover full copies of wp-config.php from a directory.
926 + *
927 + * Covers the legacy `wp-config.php.metasync-backup-*` copies and `.metasync-tmp-*`
928 + * files orphaned by an interrupted atomic save. Temp files are only removed once
929 + * they are older than WPCONFIG_TMP_MAX_AGE, so a save running concurrently in
930 + * another request never has its temp file pulled out from under it.
931 + *
932 + * @param string $config_dir Directory that may contain leftover copies.
933 + *
934 + * @return array{removed:int,failed:int,scan_failed:int} Cleanup and scan-failure counts.
935 + */
936 + public static function remove_wpconfig_backups($config_dir)
937 + {
938 + $dir = rtrim($config_dir, '/');
939 + $removed = 0;
940 + $failed = 0;
941 + $scan_failed = 0;
942 +
943 + $backups = glob($dir . '/wp-config.php.metasync-backup-*');
944 + if (false === $backups) {
945 + $scan_failed++;
946 + $backups = [];
947 + }
948 +
949 + foreach ($backups as $backup) {
950 + if (!is_file($backup)) {
951 + continue;
952 + }
953 +
954 + if (@unlink($backup)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- boolean return value is consumed
955 + $removed++;
956 + } else {
957 + $failed++;
958 + }
959 + }
960 +
961 + $temps = glob($dir . '/.metasync-tmp-*');
962 + if (false === $temps) {
963 + $scan_failed++;
964 + $temps = [];
965 + }
966 +
967 + foreach ($temps as $temp) {
968 + if (!is_file($temp)) {
969 + continue;
970 + }
971 +
972 + // abs() so a future mtime (clock skew, NFS) still ages out instead of being
973 + // skipped forever.
974 + $mtime = @filemtime($temp);
975 + if (false === $mtime || abs(time() - $mtime) < self::WPCONFIG_TMP_MAX_AGE) {
976 + continue;
977 + }
978 +
979 + if (@unlink($temp)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- boolean return value is consumed
980 + $removed++;
981 + } else {
982 + $failed++;
983 + }
984 + }
985 +
986 + return ['removed' => $removed, 'failed' => $failed, 'scan_failed' => $scan_failed];
675 987 }
676 988
677 989
678 990 }