__('Matching run', 'wpbr-bulk-replace'), 'replace_file' => __('Replace file', 'wpbr-bulk-replace'), 'new_attachment' => __('New attachment', 'wpbr-bulk-replace'), 'add_missing' => __('Add missing file', 'wpbr-bulk-replace'), 'rollback' => __('Roll back', 'wpbr-bulk-replace'), 'verify' => __('Mark verified', 'wpbr-bulk-replace'), 'delete_backup' => __('Delete backup', 'wpbr-bulk-replace'), 'combine' => __('Combine files', 'wpbr-bulk-replace'), 'import' => __('Reviewer import', 'wpbr-bulk-replace'), 'undo_import' => __('Undo import', 'wpbr-bulk-replace'), 'upload' => __('Upload', 'wpbr-bulk-replace'), 'scan' => __('Pick up files', 'wpbr-bulk-replace'), ); if (isset($labels[$action])) return $labels[$action]; // Unknown action: readable rather than absent. return ucfirst(str_replace('_', ' ', (string) $action)); } /** * Display label for a logged result. */ function wpbr_log_result_label($result) { $labels = array( 'success' => __('Succeeded', 'wpbr-bulk-replace'), 'skipped' => __('Skipped', 'wpbr-bulk-replace'), 'failed' => __('Failed', 'wpbr-bulk-replace'), 'info' => __('Info', 'wpbr-bulk-replace'), ); return isset($labels[$result]) ? $labels[$result] : ucfirst(str_replace('_', ' ', (string) $result)); } function wpbr_render_log_page() { if (!current_user_can('manage_options')) return; global $wpdb; if (isset($_POST['wpbr_action']) && $_POST['wpbr_action'] === 'clear_log' && check_admin_referer('wpbr_clear_log')) { $scope = isset($_POST['scope']) ? sanitize_text_field(wp_unslash($_POST['scope'])) : 'all'; if ($scope === 'all') { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; a write, and nothing here is cached, so there is nothing to invalidate. $wpdb->query("DELETE FROM {$wpdb->prefix}wpbr_log"); echo "

" . esc_html__('Log cleared.', 'wpbr-bulk-replace') . "

"; } else { $days = intval($scope); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; a write, and nothing here is cached, so there is nothing to invalidate. $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}wpbr_log WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)", $days)); echo "

" . esc_html(sprintf( /* translators: %s is a number of days. */ _n('Entries older than %s day removed.', 'Entries older than %s days removed.', $days, 'wpbr-bulk-replace'), number_format_i18n($days) )) . "

"; } } $f_batch = isset($_GET['batch']) ? intval($_GET['batch']) : 0; $f_action = isset($_GET['act']) ? sanitize_text_field(wp_unslash($_GET['act'])) : ''; $f_result = isset($_GET['res']) ? sanitize_text_field(wp_unslash($_GET['res'])) : ''; $f_search = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $per_page = 200; $paged = max(1, isset($_GET['paged']) ? intval($_GET['paged']) : 1); // One fixed statement with every filter bound. Each filter reads "unset, or // equal to", so an empty one matches every row and the SQL text never // changes with what the person typed: there is nothing to assemble, and so // nothing to get wrong. $like = '%' . $wpdb->esc_like($f_search) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; the screen shows live state, and a cached copy would lag the write the person just made. $total = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}wpbr_log WHERE (%d = 0 OR batch_id = %d) AND (%s = '' OR action = %s) AND (%s = '' OR result = %s) AND (%s = '' OR filename LIKE %s OR message LIKE %s)", $f_batch, $f_batch, $f_action, $f_action, $f_result, $f_result, $f_search, $like, $like )); $offset = ($paged - 1) * $per_page; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; the screen shows live state, and a cached copy would lag the write the person just made. $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wpbr_log WHERE (%d = 0 OR batch_id = %d) AND (%s = '' OR action = %s) AND (%s = '' OR result = %s) AND (%s = '' OR filename LIKE %s OR message LIKE %s) ORDER BY id DESC LIMIT %d OFFSET %d", $f_batch, $f_batch, $f_action, $f_action, $f_result, $f_result, $f_search, $like, $like, $per_page, $offset )); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; the screen shows live state, and a cached copy would lag the write the person just made. $batches = $wpdb->get_results("SELECT id, label FROM {$wpdb->prefix}wpbr_batches ORDER BY id DESC"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; the screen shows live state, and a cached copy would lag the write the person just made. $actions = $wpdb->get_col("SELECT DISTINCT action FROM {$wpdb->prefix}wpbr_log ORDER BY action"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned table with no WordPress API; the screen shows live state, and a cached copy would lag the write the person just made. $counts = $wpdb->get_results("SELECT result, COUNT(*) AS n FROM {$wpdb->prefix}wpbr_log GROUP BY result", OBJECT_K); echo "
"; wpbr_admin_header_print(__('Activity Log', 'wpbr-bulk-replace')); echo "

" . esc_html__('Every file the tool has processed, rolled back, verified or cleaned up, plus matching runs and reviewer imports. Kept indefinitely until you clear it, so a run can be checked long after the on-screen log has gone.', 'wpbr-bulk-replace') . "

"; $n_fail = isset($counts['failed']) ? $counts['failed']->n : 0; $n_skip = isset($counts['skipped']) ? $counts['skipped']->n : 0; $n_ok = isset($counts['success']) ? $counts['success']->n : 0; // The counts are a summary of the table below, so they are announced as one. echo "

"; // The same three counts, as the shared status badge: a glyph and the word, so colour is never the only signal. wpbr_status_badge_print('ok', sprintf( /* translators: %s is a count of log entries. */ _n('%s succeeded', '%s succeeded', $n_ok, 'wpbr-bulk-replace'), number_format_i18n($n_ok) )); echo " "; wpbr_status_badge_print('warn', sprintf( /* translators: %s is a count of log entries. */ _n('%s skipped', '%s skipped', $n_skip, 'wpbr-bulk-replace'), number_format_i18n($n_skip) )); echo " "; wpbr_status_badge_print('error', sprintf( /* translators: %s is a count of log entries. */ _n('%s failed', '%s failed', $n_fail, 'wpbr-bulk-replace'), number_format_i18n($n_fail) )); echo "

"; wpbr_inline_open('style'); echo ".wpbr-log-pill{display:inline-block;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:500;margin-right:6px;}"; echo ".wpbr-log-row-failed td{background:#fdf4f4 !important;}"; wpbr_inline_close(); // Filters. Each select had no accessible name at all, so a screen reader // announced three unlabelled combo boxes in a row. echo "
"; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; if ($f_batch || $f_action || $f_result || $f_search) { echo "" . esc_html__('Clear filters', 'wpbr-bulk-replace') . ""; } echo "" . esc_html(sprintf( /* translators: %s is a count of log entries. */ _n('%s entry', '%s entries', $total, 'wpbr-bulk-replace'), number_format_i18n($total) )) . ""; echo "
"; if (!$rows) { echo "

" . esc_html__('Nothing logged yet.', 'wpbr-bulk-replace') . "

"; return; } echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; $datetime_format = wpbr_datetime_format(); foreach ($rows as $r) { $row_class = ($r->result === 'failed') ? 'wpbr-log-row-failed' : ''; echo ""; echo ""; echo ""; $kinds = array('success' => 'ok', 'skipped' => 'warn', 'failed' => 'error', 'info' => 'info'); echo ""; echo ""; echo ""; echo ""; echo ""; } echo "
" . esc_html__('When', 'wpbr-bulk-replace') . "" . esc_html__('Action', 'wpbr-bulk-replace') . "" . esc_html__('Result', 'wpbr-bulk-replace') . "" . esc_html__('File', 'wpbr-bulk-replace') . "" . esc_html__('Details', 'wpbr-bulk-replace') . "" . esc_html__('By', 'wpbr-bulk-replace') . "
".esc_html(mysql2date($datetime_format, $r->created_at))."".esc_html(wpbr_log_action_label($r->action)).""; wpbr_status_badge_print(isset($kinds[$r->result]) ? $kinds[$r->result] : 'idle', wpbr_log_result_label($r->result)); echo "".esc_html($r->filename)."".esc_html($r->message).""; if (!empty($r->actor) && wpbr_actor_label($r->actor) !== '') { echo "created_by))."\">".esc_html(wpbr_actor_label($r->actor)).""; } else { echo $r->created_by ? "created_by))."\">".esc_html(wpbr_user_label($r->created_by))."" : '—'; } echo "
"; $pages = (int) ceil($total / $per_page); if ($pages > 1) { echo ""; } echo "
"; wp_nonce_field('wpbr_clear_log'); echo ""; echo ""; echo ""; // Was a confirm() from an inline onclick: untranslatable, and it stated the // consequence only after the click. echo ""; echo "" . esc_html__('This only affects the log. Your files, batches and backups are not touched.', 'wpbr-bulk-replace') . ""; echo "
"; echo ""; }