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 +428 -133 2.6.24 → 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,
@@ -75,41 +87,41 @@
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', 'wildcard') 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')) {
@@ -118,27 +130,38 @@
118 130 // destinations such as 'https://example.com/about' on a
119 131 // www-prefixed site count as external and silently flip the setting.
120 132 $home_host = wp_parse_url(home_url(), PHP_URL_HOST);
121 133 $home_host = is_string($home_host) ? strtolower(preg_replace('/^www\./i', '', $home_host)) : '';
122 - $params = ['http%'];
134 +
123 135 if ($home_host !== '') {
124 - $not_like = '';
125 - foreach (['http://', 'https://'] as $scheme) {
126 - foreach ([$home_host, 'www.' . $home_host] as $host) {
127 - $not_like .= ' AND url_redirect_to NOT LIKE %s';
128 - $params[] = $wpdb->esc_like($scheme . $host) . '%';
129 - }
130 - }
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 + );
131 153 } else {
132 - $not_like = ' AND url_redirect_to NOT LIKE %s';
133 - $params[] = $wpdb->esc_like(trailingslashit(home_url())) . '%';
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 + );
134 163 }
135 - $external_count = $wpdb->get_var(
136 - $wpdb->prepare(
137 - "SELECT COUNT(*) FROM {$tableNameRedirection} WHERE url_redirect_to LIKE %s{$not_like}",
138 - ...$params
139 - )
140 - );
141 164 if ($external_count > 0) {
142 165 update_option('metasync_allow_external_redirects', 1, true);
143 166 }
144 167 update_option('metasync_external_redirects_migrated', 1, true);
@@ -147,9 +170,10 @@
147 170 // Create HeartBeat Error Monitor Table
148 171 require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
149 172 $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
150 173
151 - 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
152 176 $table_sql = "CREATE TABLE {$tableNameHeartBeatErrorMonitor} (
153 177 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
154 178 attribute_name VARCHAR(25) NOT NULL DEFAULT '',
155 179 object_count VARCHAR(25) NOT NULL DEFAULT '',
@@ -160,14 +184,38 @@
160 184
161 185 dbDelta($table_sql);
162 186 }
163 187
164 - // 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
165 212 require_once dirname(__FILE__, 2) . '/sync-history/class-metasync-sync-history-database.php';
166 213 $tableNameSyncHistory = esc_sql($wpdb->prefix . Metasync_Sync_History_Database::$table_name);
167 214
168 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameSyncHistory)) != $tableNameSyncHistory) {
169 - $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 (
170 218 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
171 219 title VARCHAR(255) NOT NULL DEFAULT '',
172 220 source VARCHAR(50) NOT NULL DEFAULT '',
173 221 status VARCHAR(25) NOT NULL DEFAULT 'draft',
@@ -186,19 +234,19 @@
186 234 dbDelta($table_sql);
187 235 } else {
188 236 // PERFORMANCE OPTIMIZATION: Add composite indexes to existing tables
189 237 // Check and add indexes if they don't exist
190 - $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
191 239 $index_names = array_column($indexes, 'Key_name');
192 240
193 241 // Add deduplication index (source, created_at)
194 242 if (!in_array('idx_dedup', $index_names)) {
195 - $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
196 244 }
197 245
198 246 // Add search index (title(50), source, created_at)
199 247 if (!in_array('idx_search', $index_names)) {
200 - $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
201 249 }
202 250 }
203 251
204 252 // Create OTTO Excluded URLs Table
@@ -204,10 +252,11 @@
204 252 // Create OTTO Excluded URLs Table
205 253 require_once dirname(__FILE__, 2) . '/otto/class-metasync-otto-excluded-urls-database.php';
206 254 $tableNameOttoExcludedURLs = esc_sql($wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name);
207 255
208 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) {
209 - $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 (
210 259 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
211 260 url_pattern TEXT NOT NULL,
212 261 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
213 262 description TEXT NULL,
@@ -232,9 +281,9 @@
232 281 // Create Robots.txt Backups Table
233 282 require_once dirname(__FILE__, 2) . '/robots-txt/class-metasync-robots-txt-database.php';
234 283 $robots_db = Metasync_Robots_Txt_Database::get_instance();
235 284 $table_name_robots = esc_sql($wpdb->prefix . 'metasync_robots_txt_backups');
236 - 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
237 286 $robots_db->create_table();
238 287 }
239 288
240 289 }
@@ -258,13 +307,16 @@
258 307 /* drop wp_metasync_redirections table */
259 308 // $sql = "DROP TABLE IF EXISTS `$tableNameRedirection` ";
260 309 // $wpdb->query($sql);
261 310
262 - require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
263 - $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
264 - /* drop wp_metasync_redirections table */
265 - $sql = "DROP TABLE IF EXISTS `$tableNameHeartBeatErrorMonitor` ";
266 - $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);
267 319 }
268 320
269 321 /**
270 322 * Run version-specific migrations
@@ -288,8 +340,13 @@
288 340 if ($force_run || version_compare($to_version, '2.5.9', '>=')) {
289 341 self::migrate_otto_excluded_urls_v2_5_9();
290 342 }
291 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 +
292 349 // Add more version-specific migrations here as needed
293 350 // if (version_compare($from_version, '1.1.0', '<')) {
294 351 // self::migrate_something_v1_1();
295 352 // }
@@ -309,11 +366,12 @@
309 366 // Enhanced 404 Error Monitor Table
310 367 require_once dirname(__FILE__, 2) . '/404-monitor/class-metasync-404-monitor-database.php';
311 368 $tableName404Monitor = esc_sql($wpdb->prefix . Metasync_Error_Monitor_Database::$table_name);
312 369
313 - 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
314 371 // Table doesn't exist, create enhanced version
315 - $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 (
316 374 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
317 375 uri TEXT NOT NULL,
318 376 hits_count BIGINT(20) unsigned NOT NULL DEFAULT '1',
319 377 date_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -328,42 +386,42 @@
328 386
329 387 dbDelta($table_sql);
330 388 } else {
331 389 // Table exists, check for missing columns and add them
332 - $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
333 391
334 392 // Add referer column if it doesn't exist
335 393 if (!in_array('referer', $columns)) {
336 - $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
337 395 }
338 396
339 397 // Add ip_address column if it doesn't exist
340 398 if (!in_array('ip_address', $columns)) {
341 - $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
342 400 }
343 401
344 402 // Update uri column to TEXT if it's VARCHAR(255)
345 - $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
346 404 if ($uri_column && strpos($uri_column->Type, 'varchar') !== false) {
347 - $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
348 406 }
349 407
350 408 // Update user_agent column to TEXT if it's VARCHAR(255)
351 - $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
352 410 if ($ua_column && strpos($ua_column->Type, 'varchar') !== false) {
353 - $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
354 412 }
355 413
356 414 // Add missing indexes
357 - $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
358 416 $index_names = array_column($indexes, 'Key_name');
359 417
360 418 if (!in_array('hits_count', $index_names)) {
361 - $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
362 420 }
363 421
364 422 if (!in_array('date_time', $index_names)) {
365 - $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
366 424 }
367 425 }
368 426
369 427 // Enhanced Redirections Table with new columns
@@ -369,54 +427,54 @@
369 427 // Enhanced Redirections Table with new columns
370 428 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-database.php';
371 429 $tableNameRedirection = esc_sql($wpdb->prefix . Metasync_Redirection_Database::$table_name);
372 430
373 - 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
374 432 // Table exists, check for new columns
375 - $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
433 + $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_redirections");
376 434
377 435 // Add pattern_type column if it doesn't exist
378 436 if (!in_array('pattern_type', $columns)) {
379 - $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') 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
380 438 }
381 439
382 440 // Add regex_pattern column if it doesn't exist
383 441 if (!in_array('regex_pattern', $columns)) {
384 - $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
385 443 }
386 444
387 445 // Add description column if it doesn't exist
388 446 if (!in_array('description', $columns)) {
389 - $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
390 448 }
391 449
392 450 // Add timestamp columns if they don't exist
393 451 if (!in_array('created_at', $columns)) {
394 - $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
395 453 }
396 454
397 455 if (!in_array('updated_at', $columns)) {
398 - $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
399 457 }
400 458
401 459 if (!in_array('last_accessed_at', $columns)) {
402 - $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
403 461 }
404 462
405 463 // Add indexes if they don't exist
406 - $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
407 465 $index_names = array_column($indexes, 'Key_name');
408 466
409 467 if (!in_array('pattern_type', $index_names)) {
410 - $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
411 469 }
412 470
413 471 if (!in_array('created_at', $index_names)) {
414 - $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
415 473 }
416 474
417 475 // Set default pattern_type for existing records
418 - $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
419 477 }
420 478 }
421 479
422 480 /**
@@ -431,9 +489,9 @@
431 489 $robots_db = Metasync_Robots_Txt_Database::get_instance();
432 490 $table_name = esc_sql($wpdb->prefix . 'metasync_robots_txt_backups');
433 491
434 492 // Check if table already exists
435 - 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
436 494 // Table doesn't exist, create it
437 495 $robots_db->create_table();
438 496 }
439 497 }
@@ -453,11 +511,12 @@
453 511 require_once dirname(__FILE__, 2) . '/otto/class-metasync-otto-excluded-urls-database.php';
454 512 $tableNameOttoExcludedURLs = esc_sql($wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name);
455 513
456 514 // Check if table already exists
457 - 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
458 516 // Table doesn't exist, create it
459 - $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 (
460 519 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
461 520 url_pattern TEXT NOT NULL,
462 521 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
463 522 description TEXT NULL,
@@ -482,72 +541,72 @@
482 541 // Log successful migration
483 542 // error_log('MetaSync: OTTO Excluded URLs table created successfully (v2.5.9)');
484 543 } else {
485 544 // Table exists, verify structure and add any missing columns if needed
486 - $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
487 546
488 547 // Check for required columns and add if missing
489 548 $missing_columns = false;
490 549
491 550 if (!in_array('pattern_type', $columns)) {
492 - $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
493 552 $missing_columns = true;
494 553 }
495 554
496 555 if (!in_array('description', $columns)) {
497 - $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
498 557 $missing_columns = true;
499 558 }
500 559
501 560 if (!in_array('status', $columns)) {
502 - $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
503 562 $missing_columns = true;
504 563 }
505 564
506 565 if (!in_array('is_permanent', $columns)) {
507 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN is_permanent TINYINT(1) NOT NULL DEFAULT 0 AFTER status");
508 - $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
509 568 }
510 569
511 570 if (!in_array('auto_excluded', $columns)) {
512 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN auto_excluded TINYINT(1) NOT NULL DEFAULT 0 AFTER is_permanent");
513 - $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
514 573 // Backfill: mark existing 404 exclusions as auto_excluded
515 - $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
516 575 }
517 576
518 577 if (!in_array('recheck_after', $columns)) {
519 - $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN recheck_after DATETIME NULL DEFAULT NULL AFTER auto_excluded");
520 - $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
521 580 // Backfill: set recheck_after = created_at + 7 days for auto-excluded URLs
522 - $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
523 582 }
524 583
525 584 // Check and add indexes if they don't exist
526 - $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
527 586 $index_names = array_column($indexes, 'Key_name');
528 587
529 588 if (!in_array('status', $index_names)) {
530 - $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
531 590 }
532 591
533 592 if (!in_array('pattern_type', $index_names)) {
534 - $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
535 594 }
536 595
537 596 if (!in_array('created_at', $index_names)) {
538 - $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
539 598 }
540 599
541 600 // Add unique index on url_pattern + pattern_type to prevent duplicates at database level
542 601 // Note: TEXT columns need a prefix length for indexing (767 is max for UTF8)
543 602 if (!in_array('url_pattern_type_unique', $index_names)) {
544 - $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
545 604 }
546 605
547 606 // Composite index for the cache-miss path in metasync_is_otto_url_manually_excluded()
548 607 if (!in_array('status_auto_excluded', $index_names)) {
549 - $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
550 609 }
551 610
552 611 // if ($missing_columns) {
553 612 // error_log('MetaSync: OTTO Excluded URLs table structure updated (v2.5.9)');
@@ -571,46 +630,75 @@
571 630
572 631 $meta_keys = array('meta_canonical', '_metasync_canonical_url', '_yoast_wpseo_canonical', 'rank_math_canonical_url');
573 632 $bad_values = array('array', 'http://array', 'https://array');
574 633
575 - $keys_placeholders = implode(',', array_fill(0, count($meta_keys), '%s'));
576 - $vals_placeholders = implode(',', array_fill(0, count($bad_values), '%s'));
577 -
578 - // Normalized comparison: trailing slashes stripped in SQL so
579 - // "http://Array/" and "http://Array//" both match the literals.
580 - $norm_meta = "LOWER(TRIM(TRAILING '/' FROM TRIM(meta_value)))";
581 -
582 634 // 1. Post meta + term meta: delete exact-match corrupted rows.
583 635 // Batched and deleted by primary key so huge postmeta tables aren't
584 636 // range-locked in one statement, with per-object meta-cache
585 637 // invalidation — raw SQL alone would leave persistent object caches
586 638 // (Redis/Memcached) serving the deleted value to Yoast/RankMath
587 - // readers indefinitely.
588 - $meta_targets = array(
589 - array($wpdb->postmeta, 'post_id', 'post_meta'),
590 - array($wpdb->termmeta, 'term_id', 'term_meta'),
591 - );
592 - foreach ($meta_targets as $target) {
593 - list($table, $object_col, $cache_group) = $target;
594 - for ($batch = 0; $batch < 50; $batch++) {
595 - $rows = $wpdb->get_results($wpdb->prepare(
596 - "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",
597 - array_merge($meta_keys, $bad_values)
598 - ));
599 - if (empty($rows)) {
600 - break;
601 - }
602 - $meta_ids = implode(',', array_map('intval', wp_list_pluck($rows, 'meta_id')));
603 - $wpdb->query("DELETE FROM {$table} WHERE meta_id IN ({$meta_ids})");
604 - foreach ($rows as $row) {
605 - wp_cache_delete((int) $row->object_id, $cache_group);
606 - }
607 - if (count($rows) < 500) {
608 - break;
609 - }
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;
610 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 + }
611 671 }
612 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 +
613 701 // 2. Rows still stored as serialized arrays (the raw material the
614 702 // "Array" casts came from): repair MetaSync's own keys to the first
615 703 // usable URL inside; third-party keys are delete-only (never invent
616 704 // a value inside another plugin's storage). Written with direct SQL
@@ -619,11 +707,14 @@
619 707 // row; loops until exhausted (repaired rows stop matching LIKE).
620 708 if (class_exists('Metasync_Canonical_Sanitizer')) {
621 709 $own_keys = array('meta_canonical', '_metasync_canonical_url');
622 710 for ($batch = 0; $batch < 50; $batch++) {
623 - $rows = $wpdb->get_results($wpdb->prepare(
624 - "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",
625 - $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:%'))
626 717 ));
627 718 if (empty($rows)) {
628 719 break;
629 720 }
@@ -632,11 +723,11 @@
632 723 if (in_array($row->meta_key, $own_keys, true)) {
633 724 $repaired = Metasync_Canonical_Sanitizer::sanitize(maybe_unserialize($row->meta_value));
634 725 }
635 726 if ($repaired !== '') {
636 - $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
637 728 } else {
638 - $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
639 730 }
640 731 wp_cache_delete((int) $row->post_id, 'post_meta');
641 732 }
642 733 if (count($rows) < 500) {
@@ -647,25 +738,37 @@
647 738
648 739 // 3. Yoast indexable cache: null corrupted canonical columns so the
649 740 // frontend and sitemaps stop serving the bad value immediately.
650 741 $indexable_table = $wpdb->prefix . 'yoast_indexable';
651 - 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
652 743 $wpdb->query($wpdb->prepare(
653 - "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')) . ')',
654 747 $bad_values
655 748 ));
656 749 }
657 750
658 751 // 4. AIOSEO custom tables: null corrupted canonical_url columns.
659 - foreach (array('aioseo_posts', 'aioseo_terms') as $aioseo_table) {
660 - $table = $wpdb->prefix . $aioseo_table;
661 - if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table))) === $table) {
662 - $wpdb->query($wpdb->prepare(
663 - "UPDATE {$table} SET canonical_url = NULL WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical_url))) IN ({$vals_placeholders})",
664 - $bad_values
665 - ));
666 - }
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 + ));
667 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 + }
668 771
669 772 // 5. Yoast stores term canonicals in the wpseo_taxonomy_meta option,
670 773 // not termmeta. Strip only exact corruption literals.
671 774 if (class_exists('Metasync_Canonical_Sanitizer')) {
@@ -688,8 +791,200 @@
688 791 update_option('wpseo_taxonomy_meta', $tax_meta);
689 792 }
690 793 }
691 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];
692 987 }
693 988
694 989
695 990 }