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 +416 -12 2.6.3trunk 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");
@@ -110,8 +120,41 @@
110 120 // Set default pattern_type for existing records
111 121 $wpdb->query("UPDATE {$tableNameRedirection} SET pattern_type = 'exact' WHERE pattern_type IS NULL OR pattern_type = ''");
112 122 }
113 123
124 + // One-time migration: auto-enable external redirects if the site already has any
125 + if (!get_option('metasync_external_redirects_migrated')) {
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 + }
145 + $external_count = $wpdb->get_var(
146 + $wpdb->prepare(
147 + "SELECT COUNT(*) FROM {$tableNameRedirection} WHERE url_redirect_to LIKE %s{$not_like}",
148 + ...$params
149 + )
150 + );
151 + if ($external_count > 0) {
152 + update_option('metasync_allow_external_redirects', 1, true);
153 + }
154 + update_option('metasync_external_redirects_migrated', 1, true);
155 + }
156 +
114 157 // Create HeartBeat Error Monitor Table
115 158 require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
116 159 $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
117 160
@@ -127,9 +170,31 @@
127 170
128 171 dbDelta($table_sql);
129 172 }
130 173
131 - // 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
132 197 require_once dirname(__FILE__, 2) . '/sync-history/class-metasync-sync-history-database.php';
133 198 $tableNameSyncHistory = esc_sql($wpdb->prefix . Metasync_Sync_History_Database::$table_name);
134 199
135 200 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s ", $tableNameSyncHistory)) != $tableNameSyncHistory) {
@@ -203,11 +268,8 @@
203 268 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name_robots)) != $table_name_robots) {
204 269 $robots_db->create_table();
205 270 }
206 271
207 - // Create Zapier Subscriptions Table
208 - require_once dirname(__FILE__, 2) . '/zapier/class-metasync-zapier-database.php';
209 - Metasync_Zapier_Database::create_table();
210 272 }
211 273
212 274 /**
213 275 * deactivation of migration.
@@ -228,13 +290,16 @@
228 290 /* drop wp_metasync_redirections table */
229 291 // $sql = "DROP TABLE IF EXISTS `$tableNameRedirection` ";
230 292 // $wpdb->query($sql);
231 293
232 - require_once dirname(__FILE__, 2) . '/heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
233 - $tableNameHeartBeatErrorMonitor = esc_sql($wpdb->prefix . Metasync_HeartBeat_Error_Monitor_Database::$table_name);
234 - /* drop wp_metasync_redirections table */
235 - $sql = "DROP TABLE IF EXISTS `$tableNameHeartBeatErrorMonitor` ";
236 - $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);
237 302 }
238 303
239 304 /**
240 305 * Run version-specific migrations
@@ -258,8 +323,13 @@
258 323 if ($force_run || version_compare($to_version, '2.5.9', '>=')) {
259 324 self::migrate_otto_excluded_urls_v2_5_9();
260 325 }
261 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 +
262 332 // Add more version-specific migrations here as needed
263 333 // if (version_compare($from_version, '1.1.0', '<')) {
264 334 // self::migrate_something_v1_1();
265 335 // }
@@ -345,9 +415,9 @@
345 415 $columns = $wpdb->get_col("DESCRIBE {$tableNameRedirection}");
346 416
347 417 // Add pattern_type column if it doesn't exist
348 418 if (!in_array('pattern_type', $columns)) {
349 - $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");
350 420 }
351 421
352 422 // Add regex_pattern column if it doesn't exist
353 423 if (!in_array('regex_pattern', $columns)) {
@@ -441,8 +511,9 @@
441 511 KEY pattern_type (pattern_type),
442 512 KEY created_at (created_at),
443 513 KEY is_permanent (is_permanent),
444 514 KEY auto_excluded (auto_excluded),
515 + KEY status_auto_excluded (status, auto_excluded),
445 516 KEY recheck_after (recheck_after),
446 517 UNIQUE KEY url_pattern_type_unique (url_pattern(191), pattern_type)
447 518 ) $collate;";
448 519
@@ -512,12 +583,345 @@
512 583 if (!in_array('url_pattern_type_unique', $index_names)) {
513 584 $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD UNIQUE KEY url_pattern_type_unique (url_pattern(191), pattern_type)");
514 585 }
515 586
587 + // Composite index for the cache-miss path in metasync_is_otto_url_manually_excluded()
588 + if (!in_array('status_auto_excluded', $index_names)) {
589 + $wpdb->query("ALTER TABLE {$tableNameOttoExcludedURLs} ADD KEY status_auto_excluded (status, auto_excluded)");
590 + }
591 +
516 592 // if ($missing_columns) {
517 593 // error_log('MetaSync: OTTO Excluded URLs table structure updated (v2.5.9)');
518 594 // }
519 595 }
596 + }
597 +
598 + /**
599 + * One-time cleanup of canonical meta corrupted to the literal
600 + * "Array" (and its esc_url'd forms "http://Array" / "https://Array").
601 + *
602 + * Deletions are exact-match only — a legitimate URL can never match.
603 + * Also repairs legacy rows still stored as (possibly nested) serialized
604 + * arrays, and clears the mirrored corruption from Yoast / RankMath /
605 + * AIOSEO storage that plugin-sync propagated, so cleaned sites are not
606 + * re-polluted by stale third-party caches. Idempotent by construction.
607 + */
608 + public static function cleanup_corrupted_canonicals()
609 + {
610 + global $wpdb;
611 +
612 + $meta_keys = array('meta_canonical', '_metasync_canonical_url', '_yoast_wpseo_canonical', 'rank_math_canonical_url');
613 + $bad_values = array('array', 'http://array', 'https://array');
614 +
615 + $keys_placeholders = implode(',', array_fill(0, count($meta_keys), '%s'));
616 + $vals_placeholders = implode(',', array_fill(0, count($bad_values), '%s'));
617 +
618 + // Normalized comparison: trailing slashes stripped in SQL so
619 + // "http://Array/" and "http://Array//" both match the literals.
620 + $norm_meta = "LOWER(TRIM(TRAILING '/' FROM TRIM(meta_value)))";
621 +
622 + // 1. Post meta + term meta: delete exact-match corrupted rows.
623 + // Batched and deleted by primary key so huge postmeta tables aren't
624 + // range-locked in one statement, with per-object meta-cache
625 + // invalidation — raw SQL alone would leave persistent object caches
626 + // (Redis/Memcached) serving the deleted value to Yoast/RankMath
627 + // readers indefinitely.
628 + $meta_targets = array(
629 + array($wpdb->postmeta, 'post_id', 'post_meta'),
630 + array($wpdb->termmeta, 'term_id', 'term_meta'),
631 + );
632 + foreach ($meta_targets as $target) {
633 + list($table, $object_col, $cache_group) = $target;
634 + for ($batch = 0; $batch < 50; $batch++) {
635 + $rows = $wpdb->get_results($wpdb->prepare(
636 + "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",
637 + array_merge($meta_keys, $bad_values)
638 + ));
639 + if (empty($rows)) {
640 + break;
641 + }
642 + $meta_ids = implode(',', array_map('intval', wp_list_pluck($rows, 'meta_id')));
643 + $wpdb->query("DELETE FROM {$table} WHERE meta_id IN ({$meta_ids})");
644 + foreach ($rows as $row) {
645 + wp_cache_delete((int) $row->object_id, $cache_group);
646 + }
647 + if (count($rows) < 500) {
648 + break;
649 + }
650 + }
651 + }
652 +
653 + // 2. Rows still stored as serialized arrays (the raw material the
654 + // "Array" casts came from): repair MetaSync's own keys to the first
655 + // usable URL inside; third-party keys are delete-only (never invent
656 + // a value inside another plugin's storage). Written with direct SQL
657 + // by meta_id so the updated_post_meta plugin-sync cascade, Yoast
658 + // indexable rebuilds, and sitemap cache busts don't fire once per
659 + // row; loops until exhausted (repaired rows stop matching LIKE).
660 + if (class_exists('Metasync_Canonical_Sanitizer')) {
661 + $own_keys = array('meta_canonical', '_metasync_canonical_url');
662 + for ($batch = 0; $batch < 50; $batch++) {
663 + $rows = $wpdb->get_results($wpdb->prepare(
664 + "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",
665 + $meta_keys
666 + ));
667 + if (empty($rows)) {
668 + break;
669 + }
670 + foreach ($rows as $row) {
671 + $repaired = '';
672 + if (in_array($row->meta_key, $own_keys, true)) {
673 + $repaired = Metasync_Canonical_Sanitizer::sanitize(maybe_unserialize($row->meta_value));
674 + }
675 + if ($repaired !== '') {
676 + $wpdb->update($wpdb->postmeta, array('meta_value' => $repaired), array('meta_id' => (int) $row->meta_id));
677 + } else {
678 + $wpdb->delete($wpdb->postmeta, array('meta_id' => (int) $row->meta_id));
679 + }
680 + wp_cache_delete((int) $row->post_id, 'post_meta');
681 + }
682 + if (count($rows) < 500) {
683 + break;
684 + }
685 + }
686 + }
687 +
688 + // 3. Yoast indexable cache: null corrupted canonical columns so the
689 + // frontend and sitemaps stop serving the bad value immediately.
690 + $indexable_table = $wpdb->prefix . 'yoast_indexable';
691 + if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($indexable_table))) === $indexable_table) {
692 + $wpdb->query($wpdb->prepare(
693 + "UPDATE {$indexable_table} SET canonical = NULL WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical))) IN ({$vals_placeholders})",
694 + $bad_values
695 + ));
696 + }
697 +
698 + // 4. AIOSEO custom tables: null corrupted canonical_url columns.
699 + foreach (array('aioseo_posts', 'aioseo_terms') as $aioseo_table) {
700 + $table = $wpdb->prefix . $aioseo_table;
701 + if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table))) === $table) {
702 + $wpdb->query($wpdb->prepare(
703 + "UPDATE {$table} SET canonical_url = NULL WHERE LOWER(TRIM(TRAILING '/' FROM TRIM(canonical_url))) IN ({$vals_placeholders})",
704 + $bad_values
705 + ));
706 + }
707 + }
708 +
709 + // 5. Yoast stores term canonicals in the wpseo_taxonomy_meta option,
710 + // not termmeta. Strip only exact corruption literals.
711 + if (class_exists('Metasync_Canonical_Sanitizer')) {
712 + $tax_meta = get_option('wpseo_taxonomy_meta');
713 + if (is_array($tax_meta)) {
714 + $changed = false;
715 + foreach ($tax_meta as $taxonomy => $terms) {
716 + if (!is_array($terms)) {
717 + continue;
718 + }
719 + foreach ($terms as $term_id => $fields) {
720 + if (is_array($fields) && isset($fields['wpseo_canonical'])
721 + && Metasync_Canonical_Sanitizer::is_corrupted($fields['wpseo_canonical'])) {
722 + unset($tax_meta[$taxonomy][$term_id]['wpseo_canonical']);
723 + $changed = true;
724 + }
725 + }
726 + }
727 + if ($changed) {
728 + update_option('wpseo_taxonomy_meta', $tax_meta);
729 + }
730 + }
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];
520 924 }
521 925
522 926
523 927 }