PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
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 2.6.4 2.6.3 All 139 releases
← All changes | database/class-db-migrations.php +262 -13 2.6.22trunk 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 {
@@ -59,9 +69,9 @@
59 69 url_redirect_to TEXT NOT NULL,
60 70 http_code SMALLINT(4) unsigned NOT NULL DEFAULT 301,
61 71 hits_count BIGINT(20) unsigned NOT NULL DEFAULT '0',
62 72 status VARCHAR(25) NOT NULL DEFAULT 'active',
63 - pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex') NOT NULL DEFAULT 'exact',
73 + pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact',
64 74 regex_pattern TEXT NULL,
65 75 description TEXT NULL,
66 76 created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
67 77 updated_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
@@ -78,9 +88,9 @@
78 88 // Check if new columns exist and add them if they don't
79 89 $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
80 90
81 91 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");
92 + $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact' AFTER status");
83 93 }
84 94
85 95 if (!in_array('regex_pattern', $columns)) {
86 96 $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN regex_pattern TEXT NULL AFTER pattern_type");
@@ -112,14 +122,31 @@
112 122 }
113 123
114 124 // One-time migration: auto-enable external redirects if the site already has any
115 125 if (!get_option('metasync_external_redirects_migrated')) {
116 - $home_prefix = $wpdb->esc_like(trailingslashit(home_url()));
126 + // Compare against every home-URL variant (http/https, www/non-www),
127 + // not just the exact home_url() string — otherwise same-site
128 + // destinations such as 'https://example.com/about' on a
129 + // www-prefixed site count as external and silently flip the setting.
130 + $home_host = wp_parse_url(home_url(), PHP_URL_HOST);
131 + $home_host = is_string($home_host) ? strtolower(preg_replace('/^www\./i', '', $home_host)) : '';
132 + $params = ['http%'];
133 + if ($home_host !== '') {
134 + $not_like = '';
135 + foreach (['http://', 'https://'] as $scheme) {
136 + foreach ([$home_host, 'www.' . $home_host] as $host) {
137 + $not_like .= ' AND url_redirect_to NOT LIKE %s';
138 + $params[] = $wpdb->esc_like($scheme . $host) . '%';
139 + }
140 + }
141 + } else {
142 + $not_like = ' AND url_redirect_to NOT LIKE %s';
143 + $params[] = $wpdb->esc_like(trailingslashit(home_url())) . '%';
144 + }
117 145 $external_count = $wpdb->get_var(
118 146 $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 . '%'
147 + "SELECT COUNT(*) FROM {$tableNameRedirection} WHERE url_redirect_to LIKE %s{$not_like}",
148 + ...$params
122 149 )
123 150 );
124 151 if ($external_count > 0) {
125 152 update_option('metasync_allow_external_redirects', 1, true);
@@ -143,9 +170,31 @@
143 170
144 171 dbDelta($table_sql);
145 172 }
146 173
147 - // Create Sync History Table
174 + // Create Headless Refresh History Table
175 + require_once dirname(__FILE__, 2) . '/headless-refresh-history/class-metasync-headless-refresh-history-database.php';
176 + $tableNameHeadlessRefreshHistory = esc_sql($wpdb->prefix . Metasync_Headless_Refresh_History::$table_name);
177 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameHeadlessRefreshHistory)) != $tableNameHeadlessRefreshHistory) {
178 + $table_sql = "CREATE TABLE {$tableNameHeadlessRefreshHistory} (
179 + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
180 + object_type VARCHAR(20) NOT NULL DEFAULT '',
181 + object_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
182 + url_hash CHAR(64) NOT NULL DEFAULT '',
183 + outcome VARCHAR(32) NOT NULL DEFAULT '',
184 + reason VARCHAR(64) NOT NULL DEFAULT '',
185 + duration_ms INT UNSIGNED NOT NULL DEFAULT 0,
186 + object_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
187 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
188 + PRIMARY KEY (id),
189 + KEY idx_created_at (created_at),
190 + KEY idx_object (object_type, object_id, created_at),
191 + KEY idx_outcome (outcome, created_at)
192 + ) $collate;";
193 + dbDelta($table_sql);
194 + }
195 +
196 + // Create Sync History Table
148 197 require_once dirname(__FILE__, 2) . '/sync-history/class-metasync-sync-history-database.php';
149 198 $tableNameSyncHistory = esc_sql($wpdb->prefix . Metasync_Sync_History_Database::$table_name);
150 199
151 200 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameSyncHistory)) != $tableNameSyncHistory) {
@@ -241,13 +290,16 @@
241 290 /* drop wp_metasync_redirections table */
242 291 // $sql = "DROP TABLE IF EXISTS `$tableNameRedirection` ";
243 292 // $wpdb->query($sql);
244 293
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);
294 + // Data tables are preserved on deactivation so a deactivate/reactivate cycle
295 + // does not silently destroy logged data (e.g. heartbeat error history).
296 + // Removal belongs to uninstall, not deactivation.
297 + // require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
298 + // $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
299 + /* drop wp_metasync_heartbeat_error_logs table */
300 + // $sql = "DROP TABLE IF EXISTS `$tableNameHeartBeatErrorMonitor` ";
301 + // $wpdb->query($sql);
250 302 }
251 303
252 304 /**
253 305 * Run version-specific migrations
@@ -271,8 +323,13 @@
271 323 if ($force_run || version_compare($to_version, '2.5.9', '>=')) {
272 324 self::migrate_otto_excluded_urls_v2_5_9();
273 325 }
274 326
327 + // Migration for versions 2.5.20+ - Remove insecure wp-config.php backup copies from the web root
328 + if ($force_run || version_compare($to_version, '2.5.20', '>=')) {
329 + self::migrate_remove_wpconfig_backups_v2_5_20();
330 + }
331 +
275 332 // Add more version-specific migrations here as needed
276 333 // if (version_compare($from_version, '1.1.0', '<')) {
277 334 // self::migrate_something_v1_1();
278 335 // }
@@ -358,9 +415,9 @@
358 415 $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
359 416
360 417 // Add pattern_type column if it doesn't exist
361 418 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");
419 + $wpdb->query("ALTER TABLE {$tableNameRedirection} ADD COLUMN pattern_type ENUM('exact', 'contain', 'start', 'end', 'regex', 'wildcard') NOT NULL DEFAULT 'exact' AFTER status");
363 420 }
364 421
365 422 // Add regex_pattern column if it doesn't exist
366 423 if (!in_array('regex_pattern', $columns)) {
@@ -671,8 +728,200 @@
671 728 update_option('wpseo_taxonomy_meta', $tax_meta);
672 729 }
673 730 }
674 731 }
732 + }
733 +
734 + /**
735 + * One-time repair of Local Business logo values corrupted to
736 + * "http://<attachment-id>" (and the https:// / trailing-slash variants).
737 + *
738 + * The legacy save path ran the logo through
739 + * sanitize_url() (= esc_url_raw()), which prepends a scheme to any value
740 + * that has none — so a stored attachment ID "45589" became "http://45589",
741 + * the admin preview rendered a broken image, and the front-end JSON-LD
742 + * published "logo": "http://45589" as structured data.
743 + *
744 + * The corrupted shape is unambiguous (a scheme followed by digits only — no
745 + * dot, no path, so it can never be a resolvable host) and encodes the
746 + * original ID exactly, so restoring the digits is lossless and needs no
747 + * rollback path. Idempotent by construction: repaired values no longer
748 + * match, so a second run writes nothing.
749 + *
750 + * Uses the same shared helper as the admin preview and the schema output
751 + * (metasync_repair_scheme_prefixed_media_id()), so the three can never
752 + * disagree about what "corrupted" means.
753 + *
754 + * @see metasync_repair_scheme_prefixed_media_id()
755 + */
756 + public static function repair_corrupted_local_seo_logo()
757 + {
758 + $options = get_option('metasync_options');
759 +
760 + if (!is_array($options) || !isset($options['localseo']['local_seo_logo'])) {
761 + return;
762 + }
763 +
764 + $stored = $options['localseo']['local_seo_logo'];
765 + $repaired = metasync_repair_scheme_prefixed_media_id($stored);
766 +
767 + if ($repaired === $stored) {
768 + return; // already clean: URL, plain attachment ID, or empty
769 + }
770 +
771 + $options['localseo']['local_seo_logo'] = $repaired;
772 + update_option('metasync_options', $options, true);
773 + }
774 +
775 +
776 + /**
777 + * Remove insecure wp-config.php backup copies left in the web root by prior versions.
778 + *
779 + * Older versions wrote a full copy of wp-config.php (containing DB credentials and
780 + * auth salts) to `wp-config.php.metasync-backup-<time()>` in the same directory as
781 + * wp-config.php before each debug-mode write. This cleanup deletes any such leftover
782 + * copies, plus any `.metasync-tmp-*` file orphaned by an interrupted atomic save.
783 + *
784 + * Claimed via an option so it runs once rather than on every later version bump, and
785 + * left unclaimed if anything could not be deleted so a later upgrade retries.
786 + *
787 + * @return void
788 + */
789 + private static function migrate_remove_wpconfig_backups_v2_5_20()
790 + {
791 + if (get_option(self::WPCONFIG_BACKUP_CLEANUP_OPTION)) {
792 + return;
793 + }
794 +
795 + // This runs from `init` at priority 1, so a wp_dlct_config_file_manager_path filter
796 + // another plugin registers at the default priority is not added yet and the filtered
797 + // value can still be the default. Sweep every candidate directory rather than
798 + // trusting one resolution, and only claim the cleanup once wp-config.php was
799 + // actually found in one of them - otherwise a later upgrade retries.
800 + $candidates = [
801 + ABSPATH . 'wp-config.php',
802 + dirname(ABSPATH) . '/wp-config.php',
803 + apply_filters('wp_dlct_config_file_manager_path', ABSPATH . 'wp-config.php'),
804 + ];
805 +
806 + $sweptDirs = [];
807 + $located = false;
808 + $failed = 0;
809 + $scan_failed = 0;
810 +
811 + foreach ($candidates as $config_file) {
812 + if (!is_string($config_file) || '' === $config_file) {
813 + continue;
814 + }
815 +
816 + $config_dir = dirname($config_file);
817 + if (isset($sweptDirs[$config_dir])) {
818 + continue;
819 + }
820 + $sweptDirs[$config_dir] = true;
821 +
822 + if (@file_exists($config_file)) {
823 + $located = true;
824 + }
825 +
826 + $result = self::remove_wpconfig_backups($config_dir);
827 + $failed += $result['failed'];
828 + $scan_failed += $result['scan_failed'];
829 + }
830 +
831 + if ($scan_failed) {
832 + error_log(sprintf(
833 + 'MetaSync: could not inspect %d wp-config.php backup directory scan(s); cleanup will be retried.',
834 + $scan_failed
835 + ));
836 + return;
837 + }
838 +
839 + if ($failed) {
840 + // A copy left behind is exactly the exposure this cleanup exists to remove,
841 + // so surface it rather than failing silently.
842 + error_log(sprintf(
843 + 'MetaSync: could not delete %d leftover wp-config.php copy/copies in %s - remove them manually.',
844 + $failed,
845 + implode(', ', array_keys($sweptDirs))
846 + ));
847 + return;
848 + }
849 +
850 + if (!$located) {
851 + // wp-config.php was not in any directory we swept, so it is relocated somewhere
852 + // we could not resolve. Leave the flag unset so a later upgrade tries again.
853 + error_log('MetaSync: wp-config.php was not found while cleaning up legacy backup copies in '
854 + . implode(', ', array_keys($sweptDirs)) . ' - cleanup will be retried.');
855 + return;
856 + }
857 +
858 + update_option(self::WPCONFIG_BACKUP_CLEANUP_OPTION, 1, false);
859 + }
860 +
861 + /**
862 + * Delete leftover full copies of wp-config.php from a directory.
863 + *
864 + * Covers the legacy `wp-config.php.metasync-backup-*` copies and `.metasync-tmp-*`
865 + * files orphaned by an interrupted atomic save. Temp files are only removed once
866 + * they are older than WPCONFIG_TMP_MAX_AGE, so a save running concurrently in
867 + * another request never has its temp file pulled out from under it.
868 + *
869 + * @param string $config_dir Directory that may contain leftover copies.
870 + *
871 + * @return array{removed:int,failed:int,scan_failed:int} Cleanup and scan-failure counts.
872 + */
873 + public static function remove_wpconfig_backups($config_dir)
874 + {
875 + $dir = rtrim($config_dir, '/');
876 + $removed = 0;
877 + $failed = 0;
878 + $scan_failed = 0;
879 +
880 + $backups = glob($dir . '/wp-config.php.metasync-backup-*');
881 + if (false === $backups) {
882 + $scan_failed++;
883 + $backups = [];
884 + }
885 +
886 + foreach ($backups as $backup) {
887 + if (!is_file($backup)) {
888 + continue;
889 + }
890 +
891 + if (@unlink($backup)) {
892 + $removed++;
893 + } else {
894 + $failed++;
895 + }
896 + }
897 +
898 + $temps = glob($dir . '/.metasync-tmp-*');
899 + if (false === $temps) {
900 + $scan_failed++;
901 + $temps = [];
902 + }
903 +
904 + foreach ($temps as $temp) {
905 + if (!is_file($temp)) {
906 + continue;
907 + }
908 +
909 + // abs() so a future mtime (clock skew, NFS) still ages out instead of being
910 + // skipped forever.
911 + $mtime = @filemtime($temp);
912 + if (false === $mtime || abs(time() - $mtime) < self::WPCONFIG_TMP_MAX_AGE) {
913 + continue;
914 + }
915 +
916 + if (@unlink($temp)) {
917 + $removed++;
918 + } else {
919 + $failed++;
920 + }
921 + }
922 +
923 + return ['removed' => $removed, 'failed' => $failed, 'scan_failed' => $scan_failed];
675 924 }
676 925
677 926
678 927 }