PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.21
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.21
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / database / class-db-migrations.php

class-db-migrations.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.21, at database/class-db-migrations.php

679 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The database migration for the plugin.
5 *
6 * @since 1.0.0
7 * @package Metasync
8 * @subpackage Metasync/database
9 * @author Engineering Team <support@searchatlas.com>
10 */
11 // Some Plugins declare class name DBMigration to avoid conflict, renamed the class
12 class MetaSync_DBMigration
13 {
14
15 /**
16 * activation of migration.
17 */
18 public static function activation()
19 {
20 self::run_migrations();
21 }
22
23 /**
24 * Run all database migrations
25 */
26 public static function run_migrations()
27 {
28 global $wpdb;
29 $collate = $wpdb->get_charset_collate();
30
31 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
32
33 // Create 404 Monitor Table
34 require_once dirname(__FILE__, 2) . '/404-monitor/class-metasync-404-monitor-database.php';
35 $tableName = esc_sql($wpdb->prefix . Metasync_Error_Monitor_Database::$table_name);
36
37 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tableName)) != $tableName) {
38 $table_sql = "CREATE TABLE {$tableName} (
39 id BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
40 uri VARCHAR(255) NOT NULL,
41 date_time DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
42 hits_count BIGINT(20) unsigned NOT NULL DEFAULT 1,
43 user_agent VARCHAR(255) NOT NULL DEFAULT '',
44 PRIMARY KEY id (id),
45 KEY uri (uri(191))
46 ) $collate;";
47
48 dbDelta($table_sql);
49 }
50
51 // Create Redirections Table
52 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-database.php';
53 $tableNameRedirection = esc_sql($wpdb->prefix . Metasync_Redirection_Database::$table_name);
54
55 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameRedirection)) != $tableNameRedirection) {
56 $table_sql = "CREATE TABLE {$tableNameRedirection} (
57 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
58 sources_from TEXT NOT NULL,
59 url_redirect_to TEXT NOT NULL,
60 http_code SMALLINT(4) unsigned NOT NULL DEFAULT 301,
61 hits_count BIGINT(20) unsigned NOT NULL DEFAULT '0',
62 status VARCHAR(25) NOT NULL DEFAULT 'active',
63 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
64 regex_pattern TEXT NULL,
65 description TEXT NULL,
66 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
67 updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
68 last_accessed_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
69 PRIMARY KEY id (id),
70 KEY status (status),
71 KEY pattern_type (pattern_type),
72 KEY created_at (created_at),
73 KEY idx_active_redirects (status, sources_from(191))
74 ) $collate;";
75
76 dbDelta($table_sql);
77 } else {
78 // Check if new columns exist and add them if they don't
79 $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
80
81 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");
83 }
84
85 if (!in_array('regex_pattern', $columns)) {
86 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type");
87 }
88
89 if (!in_array('description', $columns)) {
90 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN description TEXT NULL AFTER regex_pattern");
91 }
92
93 // Add indexes if they don't exist
94 $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameRedirection}");
95 $index_names = array_column($indexes, 'Key_name');
96
97 if (!in_array('pattern_type', $index_names)) {
98 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY pattern_type (pattern_type)");
99 }
100
101 if (!in_array('created_at', $index_names)) {
102 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY created_at (created_at)");
103 }
104
105 // PERFORMANCE OPTIMIZATION: Add composite index for active redirects lookup
106 if (!in_array('idx_active_redirects', $index_names)) {
107 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY idx_active_redirects (status, sources_from(191))");
108 }
109
110 // Set default pattern_type for existing records
111 $wpdb->query("UPDATE {$tableNameRedirection} SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''");
112 }
113
114 // One-time migration: auto-enable external redirects if the site already has any
115 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 );
124 if ($external_count > 0) {
125 update_option('metasync_allow_external_redirects', 1, true);
126 }
127 update_option('metasync_external_redirects_migrated', 1, true);
128 }
129
130 // Create HeartBeat Error Monitor Table
131 require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
132 $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
133
134 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameHeartBeatErrorMonitor)) != $tableNameHeartBeatErrorMonitor) {
135 $table_sql = "CREATE TABLE {$tableNameHeartBeatErrorMonitor} (
136 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
137 attribute_name VARCHAR(25) NOT NULL DEFAULT '',
138 object_count VARCHAR(25) NOT NULL DEFAULT '',
139 error_description TEXT NULL,
140 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
141 PRIMARY KEY id (id)
142 ) $collate;";
143
144 dbDelta($table_sql);
145 }
146
147 // Create Sync History Table
148 require_once dirname(__FILE__, 2) . '/sync-history/class-metasync-sync-history-database.php';
149 $tableNameSyncHistory = esc_sql($wpdb->prefix . Metasync_Sync_History_Database::$table_name);
150
151 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameSyncHistory)) != $tableNameSyncHistory) {
152 $table_sql = "CREATE TABLE {$tableNameSyncHistory} (
153 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
154 title VARCHAR(255) NOT NULL DEFAULT '',
155 source VARCHAR(50) NOT NULL DEFAULT '',
156 status VARCHAR(25) NOT NULL DEFAULT 'draft',
157 content_type VARCHAR(50) NOT NULL DEFAULT '',
158 url TEXT NULL,
159 meta_data TEXT NULL,
160 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
161 PRIMARY KEY id (id),
162 KEY source (source),
163 KEY status (status),
164 KEY created_at (created_at),
165 KEY idx_dedup (source, created_at),
166 KEY idx_search (title(50), source, created_at)
167 ) $collate;";
168
169 dbDelta($table_sql);
170 } else {
171 // PERFORMANCE OPTIMIZATION: Add composite indexes to existing tables
172 // Check and add indexes if they don't exist
173 $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameSyncHistory}");
174 $index_names = array_column($indexes, 'Key_name');
175
176 // Add deduplication index (source, created_at)
177 if (!in_array('idx_dedup', $index_names)) {
178 $wpdb->query("ALTER TABLE {$tableNameSyncHistory} ADD KEY idx_dedup (source, created_at)");
179 }
180
181 // Add search index (title(50), source, created_at)
182 if (!in_array('idx_search', $index_names)) {
183 $wpdb->query("ALTER TABLE {$tableNameSyncHistory} ADD KEY idx_search (title(50), source, created_at)");
184 }
185 }
186
187 // Create OTTO Excluded URLs Table
188 require_once dirname(__FILE__, 2) . '/otto/class-metasync-otto-excluded-urls-database.php';
189 $tableNameOttoExcludedURLs = esc_sql($wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name);
190
191 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) {
192 $table_sql = "CREATE TABLE {$tableNameOttoExcludedURLs} (
193 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
194 url_pattern TEXT NOT NULL,
195 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
196 description TEXT NULL,
197 status VARCHAR(25) NOT NULL DEFAULT 'active',
198 is_permanent TINYINT(1) NOT NULL DEFAULT 0,
199 auto_excluded TINYINT(1) NOT NULL DEFAULT 0,
200 recheck_after DATETIME NULL DEFAULT NULL,
201 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
202 PRIMARY KEY id (id),
203 KEY status (status),
204 KEY pattern_type (pattern_type),
205 KEY created_at (created_at),
206 KEY is_permanent (is_permanent),
207 KEY auto_excluded (auto_excluded),
208 KEY recheck_after (recheck_after),
209 UNIQUE KEY url_pattern_type_unique (url_pattern(191), pattern_type)
210 ) $collate;";
211
212 dbDelta($table_sql);
213 }
214
215 // Create Robots.txt Backups Table
216 require_once dirname(__FILE__, 2) . '/robots-txt/class-metasync-robots-txt-database.php';
217 $robots_db = Metasync_Robots_Txt_Database::get_instance();
218 $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) {
220 $robots_db->create_table();
221 }
222
223 }
224
225 /**
226 * deactivation of migration.
227 */
228 public static function deactivation()
229 {
230 global $wpdb;
231 // require_once dirname(__FILE__, 2) . '/404-monitor/class-metasync-404-monitor-database.php';
232 // $tableName = esc_sql($wpdb->prefix . Metasync_Error_Monitor_Database::$table_name);
233
234 /* drop wp_metasync_404_logs table */
235 // $sql = "DROP TABLE IF EXISTS `$tableName` ";
236 // $wpdb->query($sql);
237
238 // require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-database.php';
239 // $tableNameRedirection = esc_sql($wpdb->prefix . Metasync_Redirection_Database::$table_name);
240
241 /* drop wp_metasync_redirections table */
242 // $sql = "DROP TABLE IF EXISTS `$tableNameRedirection` ";
243 // $wpdb->query($sql);
244
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);
250 }
251
252 /**
253 * Run version-specific migrations
254 */
255 public static function run_version_migrations($from_version, $to_version)
256 {
257 // If from_version is 9.9.9, always run all migrations
258 $force_run = ($from_version === '9.9.9');
259
260 // Migration for versions 2.5.4+ - Enhanced 404 monitor and redirections
261 if ($force_run || version_compare($to_version, '2.5.4', '>=')) {
262 self::migrate_enhanced_features_v2_5_4();
263 }
264
265 // Migration for versions 2.5.6+ - Robots.txt management
266 if ($force_run || version_compare($to_version, '2.5.6', '>=')) {
267 self::migrate_robots_txt_v2_5_6();
268 }
269
270 // Migration for versions 2.5.9+ - OTTO Excluded URLs
271 if ($force_run || version_compare($to_version, '2.5.9', '>=')) {
272 self::migrate_otto_excluded_urls_v2_5_9();
273 }
274
275 // Add more version-specific migrations here as needed
276 // if (version_compare($from_version, '1.1.0', '<')) {
277 // self::migrate_something_v1_1();
278 // }
279 }
280
281 /**
282 * Migrate enhanced features for version 2.5.4+
283 */
284 private static function migrate_enhanced_features_v2_5_4()
285 {
286 global $wpdb;
287 $collate = $wpdb->get_charset_collate();
288
289 // Load WordPress upgrade functions for dbDelta
290 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
291
292 // Enhanced 404 Error Monitor Table
293 require_once dirname(__FILE__, 2) . '/404-monitor/class-metasync-404-monitor-database.php';
294 $tableName404Monitor = esc_sql($wpdb->prefix . Metasync_Error_Monitor_Database::$table_name);
295
296 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableName404Monitor)) != $tableName404Monitor) {
297 // Table doesn't exist, create enhanced version
298 $table_sql = "CREATE TABLE {$tableName404Monitor} (
299 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
300 uri TEXT NOT NULL,
301 hits_count BIGINT(20) unsigned NOT NULL DEFAULT '1',
302 date_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
303 user_agent TEXT NULL,
304 referer TEXT NULL,
305 ip_address VARCHAR(45) NULL,
306 PRIMARY KEY id (id),
307 KEY uri (uri(191)),
308 KEY hits_count (hits_count),
309 KEY date_time (date_time)
310 ) $collate;";
311
312 dbDelta($table_sql);
313 } else {
314 // Table exists, check for missing columns and add them
315 $columns = $wpdb->get_col("DESCRIBE {$tableName404Monitor}");
316
317 // Add referer column if it doesn't exist
318 if (!in_array('referer', $columns)) {
319 $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD COLUMN referer TEXT NULL AFTER user_agent");
320 }
321
322 // Add ip_address column if it doesn't exist
323 if (!in_array('ip_address', $columns)) {
324 $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD COLUMN ip_address VARCHAR(45) NULL AFTER referer");
325 }
326
327 // Update uri column to TEXT if it's VARCHAR(255)
328 $uri_column = $wpdb->get_row("SHOW COLUMNS FROM {$tableName404Monitor} LIKE 'uri'");
329 if ($uri_column && strpos($uri_column->Type, 'varchar') !== false) {
330 $wpdb->query("ALTER TABLE {$tableName404Monitor} MODIFY COLUMN uri TEXT NOT NULL");
331 }
332
333 // Update user_agent column to TEXT if it's VARCHAR(255)
334 $ua_column = $wpdb->get_row("SHOW COLUMNS FROM {$tableName404Monitor} LIKE 'user_agent'");
335 if ($ua_column && strpos($ua_column->Type, 'varchar') !== false) {
336 $wpdb->query("ALTER TABLE {$tableName404Monitor} MODIFY COLUMN user_agent TEXT NULL");
337 }
338
339 // Add missing indexes
340 $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableName404Monitor}");
341 $index_names = array_column($indexes, 'Key_name');
342
343 if (!in_array('hits_count', $index_names)) {
344 $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD KEY hits_count (hits_count)");
345 }
346
347 if (!in_array('date_time', $index_names)) {
348 $wpdb->query("ALTER TABLE {$tableName404Monitor} ADD KEY date_time (date_time)");
349 }
350 }
351
352 // Enhanced Redirections Table with new columns
353 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-database.php';
354 $tableNameRedirection = esc_sql($wpdb->prefix . Metasync_Redirection_Database::$table_name);
355
356 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameRedirection)) == $tableNameRedirection) {
357 // Table exists, check for new columns
358 $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
359
360 // Add pattern_type column if it doesn't exist
361 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");
363 }
364
365 // Add regex_pattern column if it doesn't exist
366 if (!in_array('regex_pattern', $columns)) {
367 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type");
368 }
369
370 // Add description column if it doesn't exist
371 if (!in_array('description', $columns)) {
372 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN description TEXT NULL AFTER regex_pattern");
373 }
374
375 // Add timestamp columns if they don't exist
376 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");
378 }
379
380 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");
382 }
383
384 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");
386 }
387
388 // Add indexes if they don't exist
389 $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameRedirection}");
390 $index_names = array_column($indexes, 'Key_name');
391
392 if (!in_array('pattern_type', $index_names)) {
393 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY pattern_type (pattern_type)");
394 }
395
396 if (!in_array('created_at', $index_names)) {
397 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD KEY created_at (created_at)");
398 }
399
400 // Set default pattern_type for existing records
401 $wpdb->query("UPDATE {$tableNameRedirection} SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''");
402 }
403 }
404
405 /**
406 * Migrate robots.txt management for version 2.5.6+
407 */
408 private static function migrate_robots_txt_v2_5_6()
409 {
410 global $wpdb;
411
412 // Create Robots.txt Backups Table
413 require_once dirname(__FILE__, 2) . '/robots-txt/class-metasync-robots-txt-database.php';
414 $robots_db = Metasync_Robots_Txt_Database::get_instance();
415 $table_name = esc_sql($wpdb->prefix . 'metasync_robots_txt_backups');
416
417 // Check if table already exists
418 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
419 // Table doesn't exist, create it
420 $robots_db->create_table();
421 }
422 }
423
424 /**
425 * Migrate OTTO Excluded URLs for version 2.5.9+
426 */
427 private static function migrate_otto_excluded_urls_v2_5_9()
428 {
429 global $wpdb;
430 $collate = $wpdb->get_charset_collate();
431
432 // Load WordPress upgrade functions for dbDelta
433 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
434
435 // Create OTTO Excluded URLs Table
436 require_once dirname(__FILE__, 2) . '/otto/class-metasync-otto-excluded-urls-database.php';
437 $tableNameOttoExcludedURLs = esc_sql($wpdb->prefix . Metasync_Otto_Excluded_URLs_Database::$table_name);
438
439 // Check if table already exists
440 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tableNameOttoExcludedURLs)) != $tableNameOttoExcludedURLs) {
441 // Table doesn't exist, create it
442 $table_sql = "CREATE TABLE {$tableNameOttoExcludedURLs} (
443 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
444 url_pattern TEXT NOT NULL,
445 pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
446 description TEXT NULL,
447 status VARCHAR(25) NOT NULL DEFAULT 'active',
448 is_permanent TINYINT(1) NOT NULL DEFAULT 0,
449 auto_excluded TINYINT(1) NOT NULL DEFAULT 0,
450 recheck_after DATETIME NULL DEFAULT NULL,
451 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
452 PRIMARY KEY id (id),
453 KEY status (status),
454 KEY pattern_type (pattern_type),
455 KEY created_at (created_at),
456 KEY is_permanent (is_permanent),
457 KEY auto_excluded (auto_excluded),
458 KEY status_auto_excluded (status, auto_excluded),
459 KEY recheck_after (recheck_after),
460 UNIQUE KEY url_pattern_type_unique (url_pattern(191), pattern_type)
461 ) $collate;";
462
463 dbDelta($table_sql);
464
465 // Log successful migration
466 // error_log('MetaSync: OTTO Excluded URLs table created successfully (v2.5.9)');
467 } else {
468 // Table exists, verify structure and add any missing columns if needed
469 $columns = $wpdb->get_col("DESCRIBE {$tableNameOttoExcludedURLs}");
470
471 // Check for required columns and add if missing
472 $missing_columns = false;
473
474 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");
476 $missing_columns = true;
477 }
478
479 if (!in_array('description', $columns)) {
480 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN description TEXT NULL AFTER pattern_type");
481 $missing_columns = true;
482 }
483
484 if (!in_array('status', $columns)) {
485 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD COLUMN status VARCHAR(25) NOT NULL DEFAULT 'active' AFTER description");
486 $missing_columns = true;
487 }
488
489 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)");
492 }
493
494 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)");
497 // Backfill: mark existing 404 exclusions as auto_excluded
498 $wpdb->query("UPDATE {$tableNameOttoExcludedURLs} SET auto_excluded = 1 WHERE description = 'Auto-excluded: 404'");
499 }
500
501 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)");
504 // 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')");
506 }
507
508 // Check and add indexes if they don't exist
509 $indexes = $wpdb->get_results("SHOW INDEX FROM {$tableNameOttoExcludedURLs}");
510 $index_names = array_column($indexes, 'Key_name');
511
512 if (!in_array('status', $index_names)) {
513 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY status (status)");
514 }
515
516 if (!in_array('pattern_type', $index_names)) {
517 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY pattern_type (pattern_type)");
518 }
519
520 if (!in_array('created_at', $index_names)) {
521 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY created_at (created_at)");
522 }
523
524 // Add unique index on url_pattern + pattern_type to prevent duplicates at database level
525 // Note: TEXT columns need a prefix length for indexing (767 is max for UTF8)
526 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)");
528 }
529
530 // Composite index for the cache-miss path in metasync_is_otto_url_manually_excluded()
531 if (!in_array('status_auto_excluded', $index_names)) {
532 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY status_auto_excluded (status, auto_excluded)");
533 }
534
535 // if ($missing_columns) {
536 // error_log('MetaSync: OTTO Excluded URLs table structure updated (v2.5.9)');
537 // }
538 }
539 }
540
541 /**
542 * One-time cleanup of canonical meta corrupted to the literal
543 * "Array" (and its esc_url'd forms "http://Array" / "https://Array").
544 *
545 * Deletions are exact-match only — a legitimate URL can never match.
546 * Also repairs legacy rows still stored as (possibly nested) serialized
547 * arrays, and clears the mirrored corruption from Yoast / RankMath /
548 * AIOSEO storage that plugin-sync propagated, so cleaned sites are not
549 * re-polluted by stale third-party caches. Idempotent by construction.
550 */
551 public static function cleanup_corrupted_canonicals()
552 {
553 global $wpdb;
554
555 $meta_keys = array('meta_canonical', '_metasync_canonical_url', '_yoast_wpseo_canonical', 'rank_math_canonical_url');
556 $bad_values = array('array', 'http://array', 'https://array');
557
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 // 1. Post meta + term meta: delete exact-match corrupted rows.
566 // Batched and deleted by primary key so huge postmeta tables aren't
567 // range-locked in one statement, with per-object meta-cache
568 // invalidation — raw SQL alone would leave persistent object caches
569 // (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 }
593 }
594 }
595
596 // 2. Rows still stored as serialized arrays (the raw material the
597 // "Array" casts came from): repair MetaSync's own keys to the first
598 // usable URL inside; third-party keys are delete-only (never invent
599 // a value inside another plugin's storage). Written with direct SQL
600 // by meta_id so the updated_post_meta plugin-sync cascade, Yoast
601 // indexable rebuilds, and sitemap cache busts don't fire once per
602 // row; loops until exhausted (repaired rows stop matching LIKE).
603 if (class_exists('Metasync_Canonical_Sanitizer')) {
604 $own_keys = array('meta_canonical', '_metasync_canonical_url');
605 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
609 ));
610 if (empty($rows)) {
611 break;
612 }
613 foreach ($rows as $row) {
614 $repaired = '';
615 if (in_array($row->meta_key, $own_keys, true)) {
616 $repaired = Metasync_Canonical_Sanitizer::sanitize(maybe_unserialize($row->meta_value));
617 }
618 if ($repaired !== '') {
619 $wpdb->update($wpdb->postmeta, array('meta_value' => $repaired), array('meta_id' => (int) $row->meta_id));
620 } else {
621 $wpdb->delete($wpdb->postmeta, array('meta_id' => (int) $row->meta_id));
622 }
623 wp_cache_delete((int) $row->post_id, 'post_meta');
624 }
625 if (count($rows) < 500) {
626 break;
627 }
628 }
629 }
630
631 // 3. Yoast indexable cache: null corrupted canonical columns so the
632 // frontend and sitemaps stop serving the bad value immediately.
633 $indexable_table = $wpdb->prefix . 'yoast_indexable';
634 if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($indexable_table))) === $indexable_table) {
635 $wpdb->query($wpdb->prepare(
636 "UPDATE {$indexable_table} SET canonical = NULL WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical))) IN ({$vals_placeholders})",
637 $bad_values
638 ));
639 }
640
641 // 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 }
650 }
651
652 // 5. Yoast stores term canonicals in the wpseo_taxonomy_meta option,
653 // not termmeta. Strip only exact corruption literals.
654 if (class_exists('Metasync_Canonical_Sanitizer')) {
655 $tax_meta = get_option('wpseo_taxonomy_meta');
656 if (is_array($tax_meta)) {
657 $changed = false;
658 foreach ($tax_meta as $taxonomy => $terms) {
659 if (!is_array($terms)) {
660 continue;
661 }
662 foreach ($terms as $term_id => $fields) {
663 if (is_array($fields) && isset($fields['wpseo_canonical'])
664 && Metasync_Canonical_Sanitizer::is_corrupted($fields['wpseo_canonical'])) {
665 unset($tax_meta[$taxonomy][$term_id]['wpseo_canonical']);
666 $changed = true;
667 }
668 }
669 }
670 if ($changed) {
671 update_option('wpseo_taxonomy_meta', $tax_meta);
672 }
673 }
674 }
675 }
676
677
678 }
679