get_var($wpdb->prepare('SHOW TABLES LIKE %s', $batches_table)); if ($found !== $batches_table) { return; // Fresh install: nothing to adopt. } update_option('wpbr_adopted_notice', array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; an activation-time look at what already exists, which must be current. 'batches' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wpbr_batches"), 'items' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wpbr_batch_items"), )); } add_action('admin_init', 'wpbr_dismiss_adoption_notice'); function wpbr_dismiss_adoption_notice() { if (empty($_GET['wpbr_dismiss_adopted'])) return; if (!current_user_can('manage_options')) return; if (!check_admin_referer('wpbr_dismiss_adopted')) return; delete_option('wpbr_adopted_notice'); wp_safe_redirect(remove_query_arg(array('wpbr_dismiss_adopted', '_wpnonce'))); exit; } /** * Catch installs updated in place rather than reactivated. * * Priority 1 so it precedes wpbr_maybe_upgrade_db(), which would otherwise * create an empty set of wpbr_ tables alongside the legacy data and leave the * site looking as though every batch had vanished. */ add_action('admin_init', 'wpbr_maybe_migrate_legacy', 1); function wpbr_maybe_migrate_legacy() { if (get_option('wpbr_schema_version') === WPBR_SCHEMA_VERSION) return; $result = wpbr_run_migration(); if ($result['status'] === 'migrated') { delete_option('wpbr_migration_failure'); update_option('wpbr_migration_notice', $result); } elseif ($result['status'] !== 'skipped') { // incomplete, failed, rolled_back and critical each mean something // different to whoever has to act on them, so the status travels with // the message rather than being flattened to "failed". update_option('wpbr_migration_failure', array( 'status' => $result['status'], 'detail' => $result['detail'], )); } } add_action('admin_init', 'wpbr_dismiss_migration_notice'); function wpbr_dismiss_migration_notice() { if (empty($_GET['wpbr_dismiss_migration'])) return; if (!current_user_can('manage_options')) return; if (!check_admin_referer('wpbr_dismiss_migration')) return; delete_option('wpbr_migration_notice'); wp_safe_redirect(remove_query_arg(array('wpbr_dismiss_migration', '_wpnonce'))); exit; } add_action('admin_init', 'wpbr_maybe_upgrade_db'); function wpbr_maybe_upgrade_db() { if (get_option('wpbr_db_version') !== WPBR_DB_VERSION) { wpbr_create_tables(); wpbr_ensure_folders(); } // Directory hardening must not depend on the schema version changing - // an install updating from a build that predates wpbr_protect_dir() would // otherwise leave its backups folder web-readable indefinitely. if (get_option('wpbr_dirs_protected') !== WPBR_PROTECT_VERSION) { wpbr_protect_dir(wpbr_staging_dir()); wpbr_protect_dir(wpbr_backups_dir()); update_option('wpbr_dirs_protected', WPBR_PROTECT_VERSION); } } /** * Whether the admin screen being drawn is one of this plugin's. * * Asked of WordPress's own `$plugin_page`, which admin.php sets from the page * the request named, rather than by reading the address a second time: the * question is "which screen is this", and core has already answered it. Every * screen here has a slug beginning "wpbr-". */ function wpbr_is_plugin_screen() { global $plugin_page; return is_string($plugin_page) && strpos($plugin_page, 'wpbr-') === 0; } add_action('admin_footer', 'wpbr_notice_behaviour'); function wpbr_notice_behaviour() { if (!wpbr_is_plugin_screen()) return; wpbr_inline_open('style'); ?> .wpbr-notice-fading { transition: opacity .6s ease; } .wrap .notice.is-dismissible { padding-right: 38px; } (function(){ var AUTO_HIDE_MS = 10000; function enhance(notice) { if (notice.dataset.wpbrEnhanced) return; if (notice.classList.contains('wpbr-no-dismiss')) return; notice.dataset.wpbrEnhanced = '1'; if (!notice.classList.contains('is-dismissible')) { notice.classList.add('is-dismissible'); var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'notice-dismiss'; btn.innerHTML = ''; btn.addEventListener('click', function(){ notice.parentNode && notice.parentNode.removeChild(notice); }); notice.appendChild(btn); } if (notice.classList.contains('notice-error') || notice.classList.contains('wpbr-persist')) return; setTimeout(function(){ notice.classList.add('wpbr-notice-fading'); notice.style.opacity = '0'; setTimeout(function(){ notice.parentNode && notice.parentNode.removeChild(notice); }, 650); }, AUTO_HIDE_MS); } document.querySelectorAll('.wrap .notice, .wrap .updated, .wrap .error').forEach(enhance); // Search box above any meta-field checkbox list document.addEventListener('input', function(e){ if (!e.target.classList.contains('wpbr-field-search')) return; var box = e.target; var list = box.nextElementSibling; if (!list) return; var q = (box.value || '').trim().toLowerCase(); var shown = 0, total = 0; list.querySelectorAll('.wpbr-field-opt').forEach(function(opt){ total++; var hit = (q === '') || (opt.dataset.field || '').indexOf(q) !== -1; // never hide a field that's currently ticked var cb = opt.querySelector('input[type=checkbox]'); if (cb && cb.checked) hit = true; opt.style.display = hit ? '' : 'none'; if (hit) shown++; }); box.title = (q === '') ? '' : (shown + ' of ' + total + ' fields'); }); })();
' . wp_kses_post(sprintf(
/* translators: 1: "Staging" or "Backups", 2: a PHP constant name, 3: the value it was set to. */
__('%1$s folder constant ignored. %2$s is set to %3$s, which is not an absolute path, so the folder configured in Settings is being used instead.', 'wpbr-bulk-replace'),
esc_html($label), esc_html($status['constant']), esc_html($status['raw'])
)) . '
%2$s points at %3$s, which cannot be written to. Nothing can be staged until that is fixed.', 'wpbr-bulk-replace'),
esc_html($label), esc_html($status['constant']), esc_html($status['path'])
)
: sprintf(
/* translators: 1: "Backups", 2: a PHP constant name, 3: a filesystem path. */
__('%1$s folder is not writable. %2$s points at %3$s, which cannot be written to. Nothing can be backed up until that is fixed.', 'wpbr-bulk-replace'),
esc_html($label), esc_html($status['constant']), esc_html($status['path'])
);
echo '' . wp_kses_post($unwritable) . '
' . wp_kses_post(sprintf(
/* translators: 1: "Staging" or "Backups", 2: the configured path, 3: a PHP constant name. */
__('%1$s folder setting ignored. %2$s is outside wp-content, so the default is being used instead. To put it outside wp-content deliberately, set %3$s in wp-config.php; otherwise update it under Bulk Replace → Settings.', 'wpbr-bulk-replace'),
esc_html($label),
esc_html($opts[$key]),
'WPBR_' . esc_html(strtoupper($which)) . '_DIR'
)) . '
' . wp_kses_post(sprintf( /* translators: 1: a comma-separated list of add-on names, 2: the version of the extension contract this release honours. */ _n( 'An add-on needs a newer version of WPBR Bulk Replace. %1$s was written against a later extension API than this release provides (this one supports version %2$s), so it has not been loaded. Updating WPBR Bulk Replace should fix it. Nothing else is affected.', 'Some add-ons need a newer version of WPBR Bulk Replace. %1$s were written against a later extension API than this release provides (this one supports version %2$s), so they have not been loaded. Updating WPBR Bulk Replace should fix it. Nothing else is affected.', count($refused), 'wpbr-bulk-replace' ), esc_html(implode(', ', $names)), esc_html(number_format_i18n(WPBR_EXTENSION_API)) )) . '
' . esc_html($headline) . ' ' . esc_html($failure['detail']) . ' ' . wp_kses_post($schema) . '
' . wp_kses_post(sprintf( /* translators: 1: number of database tables, 2: number of rows, 3: number of settings. */ __('Data migrated to WPBR Bulk Replace. %1$s tables carrying %2$s rows, and %3$s settings, were renamed from their legacy names. Row counts were verified after the move.', 'wpbr-bulk-replace'), esc_html(number_format_i18n($migrated['tables'])), esc_html(number_format_i18n($migrated['rows'])), esc_html(number_format_i18n($migrated['options'])) )) . ' ' . esc_html__('Dismiss', 'wpbr-bulk-replace') . '
' . wp_kses_post(sprintf( /* translators: 1: number of batches, 2: number of file records. */ __('Existing data adopted. WPBR Bulk Replace picked up %1$s batches and %2$s file records from your previous Bulk Attachment Manager install, including history, backups and settings. Nothing was re-created or lost.', 'wpbr-bulk-replace'), esc_html(number_format_i18n($adopted['batches'])), esc_html(number_format_i18n($adopted['items'])) )) . ' ' . esc_html__('Dismiss', 'wpbr-bulk-replace') . '
' . esc_html__('Backup files are readable over the web.', 'wpbr-bulk-replace') . ' '
. esc_html($exposure['detail']) . ' '
. wp_kses_post(sprintf(
/* translators: %s is a folder name. */
__('Backups are copies of live documents, so anyone who guesses a filename can download them. The plugin writes .htaccess and web.config rules, but those are ignored by nginx — ask whoever runs the server to deny access to %s, or move the folder outside uploads using the Backups folder setting.', 'wpbr-bulk-replace'),
esc_html(basename(wpbr_backups_dir()))
)) . '
%s falls under a document root or an alias. If it does, it needs denying; if it does not, nothing further is needed.', 'wpbr-bulk-replace'),
esc_html(wpbr_backups_dir())
)
: sprintf(
/* translators: %s is a folder name. */
__('This often means loopback requests are blocked, or the site sits behind HTTP auth or a firewall — common on staging. Check by hand: open a file from %s in a private window. If it downloads, the folder needs denying in the server config.', 'wpbr-bulk-replace'),
esc_html(basename(wpbr_backups_dir()))
);
echo '' . esc_html__('Could not verify that backups are private.', 'wpbr-bulk-replace') . ' ' . esc_html($exposure['detail']) . ' ' . wp_kses_post($advice) . '
' . wp_kses_post(__('Give the batch a label and click “Start Batch” — you land in that batch’s wizard on the Upload step. See the Help page in the left menu for the full walkthrough.', 'wpbr-bulk-replace')) . '
', )); } function wpbr_add_detail_help_tab() { $screen = get_current_screen(); if (!$screen) return; $screen->add_help_tab(array( 'id' => 'wpbr-detail-steps', 'title' => __('The four steps', 'wpbr-bulk-replace'), 'content' => '' . wp_kses_post(__('Upload — drag files in, or pick up files placed in the staging folder by FTP.', 'wpbr-bulk-replace')) . '
' . '' . wp_kses_post(__('Match — choose the post type and the meta field holding the file URL, then Run Matching. Safe to re-run: rows you set by hand are protected.', 'wpbr-bulk-replace')) . '
' . '' . wp_kses_post(__('Review & Resolve — correct matches, filter by confidence, status or the matched post’s own fields, and send a CSV to a reviewer if you need a second opinion.', 'wpbr-bulk-replace')) . '
' . '' . wp_kses_post(__('Backup & Replace — tick the rows you want and process them. Do one first to check it, then the rest. Progress shows in the toolbar and rows update as they finish. Everything processed can be rolled back from the same screen.', 'wpbr-bulk-replace')) . '
' . '' . esc_html__('Verifying and deleting backups happen on that same screen once rows are processed.', 'wpbr-bulk-replace') . '
', )); $screen->add_help_tab(array( 'id' => 'wpbr-detail-gotchas', 'title' => __('Things to watch', 'wpbr-bulk-replace'), 'content' => '' . wp_kses_post(__('Conflicts block replacing. Two files matched to the same post would overwrite each other, so the Backup & Replace step stops until you fix them.', 'wpbr-bulk-replace')) . '
' . '' . wp_kses_post(__('“No existing file” needs confirming. When a post has nothing in the configured field, replacing becomes creating — a separate approval per row, or in bulk.', 'wpbr-bulk-replace')) . '
' . '' . wp_kses_post(__('Imports can be undone. Every reviewer CSV import is listed with an Undo button, so a bad import is reversible later, not just immediately.', 'wpbr-bulk-replace')) . '
' . '' . wp_kses_post(__('Backups are kept until you say otherwise. Nothing is deleted on a timer; cleanup is always a deliberate click — but note that deleting a backup also removes the ability to roll that file back.', 'wpbr-bulk-replace')) . '
', )); }