wordpress通过导出的xml上传恢复数据时候,如果手动上传附件,附件和文章的对应关系数据库是不存在的,可以通过这个插件重新关联。
一、基本功能:
1.扫描并注册文件到媒体库
扫描 /wp-content/uploads/ 目录并注册未识别的文件到媒体库;
2.扫描并将附件关联到文章
扫描媒体库中未关联的附件并批量关联到文章
3.扫描并管理缩略图
扫描 /wp-content/uploads/ 目录中属于缩略图的文件并进行管理扫描缩略图文件
4.日志文件位置
/var/www/html/wp-content/uploads/bma_log.txt
二、插件代码
将以下代码保存为bulk-media-attacher.php,放到wp-content/plugins/bulk-media-attacher文件夹即可。
<?php
/*
Plugin Name: Bulk Media Attacher
Description: 批量更新媒体库附件的归属关系、注册未识别的文件并管理缩略图
Version: 2.3
Author: Grok
*/
// 防止直接访问插件文件
if (!defined('ABSPATH')) {
exit;
}
// 将菜单添加到“媒体库”下
add_action('admin_menu', 'bma_add_admin_menu');
function bma_add_admin_menu() {
add_submenu_page(
'upload.php',
'Bulk Media Attacher',
'批量关联附件',
'manage_options',
'bulk-media-attacher',
'bma_admin_page'
);
}
// 获取日志文件路径
function bma_get_log_file() {
$upload_dir = wp_upload_dir();
$log_file = trailingslashit($upload_dir['basedir']) . 'bma_log.txt';
return is_writable($upload_dir['basedir']) ? $log_file : false;
}
// 写入日志
function bma_write_log($message) {
$log_file = bma_get_log_file();
if ($log_file) {
$time = date('[Y-m-d H:i:s]');
$message = $time . ' ' . $message . PHP_EOL;
@file_put_contents($log_file, $message, FILE_APPEND);
}
}
// 主管理页面
function bma_admin_page() {
$per_page = get_option('posts_per_page', 20);
$transient_key_uploads = 'bma_scan_uploads_result';
$transient_key_attachments = 'bma_scan_attachments_result';
$transient_key_thumbnails = 'bma_scan_thumbnails_result';
// 检查是否为分页请求
$is_pagination = isset($_GET['scan_type']) && isset($_GET['paged']) && isset($_GET['_wpnonce']);
if ($is_pagination) {
$scan_type = sanitize_text_field($_GET['scan_type']);
$paged = intval($_GET['paged']);
$nonce = $_GET['_wpnonce'];
// 验证 nonce
if (!wp_verify_nonce($nonce, 'bma_pagination_nonce')) {
wp_die('Nonce 验证失败,请刷新页面重试');
}
// 根据 scan_type 处理分页
switch ($scan_type) {
case 'uploads':
bma_scan_uploads_preview($per_page, false);
break;
case 'attachments':
bma_scan_attachments_preview($per_page, false);
break;
case 'thumbnails':
bma_scan_thumbnails_preview($per_page, false);
break;
default:
echo '<div class="notice notice-error"><p>无效的扫描类型</p></div>';
}
return;
}
// 处理扫描请求 (POST)
if (isset($_POST['bma_scan_uploads'])) {
delete_transient($transient_key_uploads);
bma_scan_uploads_preview($per_page, true);
} elseif (isset($_POST['bma_scan_attachments'])) {
delete_transient($transient_key_attachments);
bma_scan_attachments_preview($per_page, true);
} elseif (isset($_POST['bma_scan_thumbnails'])) {
delete_transient($transient_key_thumbnails);
bma_scan_thumbnails_preview($per_page, true);
}
// 处理操作请求
elseif (isset($_POST['bma_process_uploads'])) {
bma_process_uploads();
} elseif (isset($_POST['bma_process_attachments'])) {
bma_process_attachments();
} elseif (isset($_POST['bma_delete_thumbnails'])) {
bma_delete_thumbnails();
}
// 显示初始界面
else {
?>
<div class="wrap">
<h1>批量关联附件</h1>
<div class="bma-section">
<h2>扫描并注册文件</h2>
<p>扫描 /wp-content/uploads/ 目录并注册未识别的文件到媒体库</p>
<form method="post" class="bma-form">
<?php wp_nonce_field('bma_nonce_action', 'bma_nonce'); ?>
<button type="submit" name="bma_scan_uploads" class="button bma-button">扫描并注册到媒体库</button>
</form>
</div>
<div class="bma-section">
<h2>扫描并将附件关联到文章</h2>
<p>扫描媒体库中未关联的附件并批量关联到文章</p>
<form method="post" class="bma-form">
<?php wp_nonce_field('bma_nonce_action', 'bma_nonce'); ?>
<button type="submit" name="bma_scan_attachments" class="button bma-button">扫描并将附件关联到文章</button>
</form>
</div>
<div class="bma-section">
<h2>扫描并管理缩略图</h2>
<p>扫描 /wp-content/uploads/ 目录中属于缩略图的文件并进行管理</p>
<form method="post" class="bma-form">
<?php wp_nonce_field('bma_nonce_action', 'bma_nonce'); ?>
<button type="submit" name="bma_scan_thumbnails" class="button bma-button">扫描缩略图文件</button>
</form>
</div>
<h3>日志文件位置</h3>
<p><?php echo bma_get_log_file() ?: '日志文件不可写,请检查上传目录权限'; ?></p>
</div>
<style>
.bma-section { margin-bottom: 20px; }
.bma-form { display: inline-block; }
.bma-button {
background-color: #0073aa;
color: white;
padding: 8px 16px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.bma-button:hover { background-color: #005d87; }
.widefat th { text-align: left; }
.pagination { margin-top: 10px; }
.pagination .page-numbers {
padding: 5px 10px;
margin: 0 2px;
border: 1px solid #ddd;
text-decoration: none;
}
.pagination .current {
background-color: #0073aa;
color: white;
border-color: #0073aa;
}
</style>
<?php
}
}
// 扫描并预览待注册文件
function bma_scan_uploads_preview($per_page, $do_scan = false) {
if (!current_user_can('manage_options')) {
wp_die('无权限访问');
}
$transient_key = 'bma_scan_uploads_result';
$files_to_register = get_transient($transient_key);
if ($do_scan || !$files_to_register) {
bma_write_log('开始扫描上传目录文件以预览');
$upload_dir = wp_upload_dir();
$base_dir = $upload_dir['basedir'];
$base_url = $upload_dir['baseurl'];
$existing_attachments = get_posts(array(
'post_type' => 'attachment',
'posts_per_page' => -1,
'post_status' => 'inherit',
));
$existing_filenames = array();
foreach ($existing_attachments as $attachment) {
$url = wp_get_attachment_url($attachment->ID);
if ($url) {
$relative_path = str_replace($base_url, '', $url);
$filename = basename($relative_path);
$existing_filenames[strtolower($filename)] = true;
}
}
$files_to_register = array();
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base_dir));
foreach ($iterator as $file) {
if ($file->isFile()) {
$filepath = $file->getPathname();
$relative_path = str_replace($base_dir, '', $filepath);
$filename = basename($filepath);
if (preg_match('/-\d+x\d+\.(jpg|jpeg|png|gif)$/i', $filename)) {
continue;
}
if (isset($existing_filenames[strtolower($filename)])) {
continue;
}
$filetype = wp_check_filetype($filepath);
if ($filetype['type']) {
$files_to_register[$filepath] = array(
'url' => $base_url . $relative_path,
'filename' => $filename,
'mime_type' => $filetype['type']
);
}
}
}
set_transient($transient_key, $files_to_register, 3600);
}
if (empty($files_to_register)) {
echo '<div class="notice notice-warning"><p>未找到需要注册的新文件</p></div>';
return;
}
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$total_items = count($files_to_register);
$offset = ($current_page - 1) * $per_page;
$paged_files = array_slice($files_to_register, $offset, $per_page, true);
?>
<div class="wrap">
<h1>批量关联附件 - 扫描结果</h1>
<form method="post">
<input type="hidden" name="scan_type" value="uploads">
<h2>待注册文件列表(共 <?php echo $total_items; ?> 个文件)</h2>
<table class="widefat">
<thead>
<tr>
<th><input type="checkbox" id="bma_select_all_uploads"></th>
<th>文件路径</th>
<th>文件名</th>
<th>MIME 类型</th>
</tr>
</thead>
<tbody>
<?php foreach ($paged_files as $filepath => $data) : ?>
<tr>
<td><input type="checkbox" name="bma_files[]" value="<?php echo esc_attr($filepath); ?>"></td>
<td><?php echo esc_html($filepath); ?></td>
<td><?php echo esc_html($data['filename']); ?></td>
<td><?php echo esc_html($data['mime_type']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php echo bma_pagination($total_items, $per_page, $current_page, 'uploads'); ?>
<?php wp_nonce_field('bma_nonce_action', 'bma_nonce'); ?>
<p>
<button type="submit" name="bma_process_uploads" class="button bma-button" value="partial">部分导入媒体库</button>
<button type="submit" name="bma_process_uploads" class="button bma-button" value="all">全部导入媒体库</button>
</p>
<script>
document.getElementById('bma_select_all_uploads').addEventListener('change', function() {
document.querySelectorAll('input[name="bma_files[]"]').forEach(checkbox => checkbox.checked = this.checked);
});
</script>
</form>
</div>
<?php
}
// 处理注册文件
function bma_process_uploads() {
if (!current_user_can('manage_options') || !check_admin_referer('bma_nonce_action', 'bma_nonce')) {
wp_die('无权限访问');
}
bma_write_log('开始处理上传目录文件注册');
$selected_files = isset($_POST['bma_files']) && is_array($_POST['bma_files']) ? $_POST['bma_files'] : array();
$process_all = $_POST['bma_process_uploads'] === 'all';
$files_to_register = get_transient('bma_scan_uploads_result');
if ($files_to_register) {
$registered = 0;
$errors = 0;
foreach ($files_to_register as $filepath => $data) {
if ($process_all || in_array($filepath, $selected_files)) {
$attachment = array(
'guid' => $data['url'],
'post_mime_type' => $data['mime_type'],
'post_title' => $data['filename'],
'post_content' => '',
'post_status' => 'inherit'
);
$attach_id = wp_insert_attachment($attachment, $filepath);
if (is_wp_error($attach_id)) {
bma_write_log("注册文件 {$data['filename']} 失败: " . $attach_id->get_error_message());
$errors++;
continue;
}
require_once(ABSPATH . 'wp-admin/includes/image.php');
$attach_data = wp_generate_attachment_metadata($attach_id, $filepath);
wp_update_attachment_metadata($attach_id, $attach_data);
$registered++;
bma_write_log("成功注册文件 {$data['filename']},附件 ID: {$attach_id}");
}
}
$summary = "注册完成!共注册 {$registered} 个新附件,发现 {$errors} 个错误";
bma_write_log($summary);
echo '<div class="updated"><p>' . $summary . '<br>请检查日志文件:' . (bma_get_log_file() ?: '日志不可用') . '</p></div>';
}
}
// 扫描并预览附件关联
function bma_scan_attachments_preview($per_page, $do_scan = false) {
if (!current_user_can('manage_options')) {
wp_die('无权限访问');
}
$transient_key = 'bma_scan_attachments_result';
$relationships = get_transient($transient_key);
if ($do_scan || !$relationships) {
bma_write_log('开始扫描附件关联以预览');
$attachments = get_posts(array(
'post_type' => 'attachment',
'posts_per_page' => -1,
'post_status' => 'inherit',
'post_parent' => 0
));
$relationships = array();
foreach ($attachments as $attachment) {
$file_url = wp_get_attachment_url($attachment->ID);
if (!$file_url) continue;
$filename = pathinfo($file_url, PATHINFO_FILENAME);
$post_args = array(
'post_type' => 'post',
'posts_per_page' => 1,
'post_status' => 'publish',
's' => $filename
);
$related_posts = get_posts($post_args);
if (!empty($related_posts)) {
$relationships[$attachment->ID] = array(
'filename' => $filename,
'post_id' => $related_posts[0]->ID,
'post_title' => get_the_title($related_posts[0]->ID)
);
}
}
set_transient($transient_key, $relationships, 3600);
}
if (empty($relationships)) {
echo '<div class="notice notice-warning"><p>未找到可以关联的附件</p></div>';
return;
}
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$total_items = count($relationships);
$offset = ($current_page - 1) * $per_page;
$paged_relationships = array_slice($relationships, $offset, $per_page, true);
?>
<div class="wrap">
<h1>批量关联附件 - 附件关联预览</h1>
<form method="post">
<input type="hidden" name="scan_type" value="attachments">
<h2>附件关联预览(共 <?php echo $total_items; ?> 个附件)</h2>
<table class="widefat">
<thead>
<tr>
<th><input type="checkbox" id="bma_select_all_attachments"></th>
<th>附件 ID</th>
<th>文件名</th>
<th>关联文章 ID</th>
<th>文章标题</th>
</tr>
</thead>
<tbody>
<?php foreach ($paged_relationships as $attachment_id => $data) : ?>
<tr>
<td><input type="checkbox" name="bma_attachments[]" value="<?php echo esc_attr($attachment_id); ?>"></td>
<td><?php echo esc_html($attachment_id); ?></td>
<td><?php echo esc_html($data['filename']); ?></td>
<td><?php echo esc_html($data['post_id']); ?></td>
<td><?php echo esc_html($data['post_title']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php echo bma_pagination($total_items, $per_page, $current_page, 'attachments'); ?>
<?php wp_nonce_field('bma_nonce_action', 'bma_nonce'); ?>
<p>
<button type="submit" name="bma_process_attachments" class="button bma-button" value="partial">部分附件到文章</button>
<button type="submit" name="bma_process_attachments" class="button bma-button" value="all">全部附件到文章</button>
</p>
<script>
document.getElementById('bma_select_all_attachments').addEventListener('change', function() {
document.querySelectorAll('input[name="bma_attachments[]"]').forEach(checkbox => checkbox.checked = this.checked);
});
</script>
</form>
</div>
<?php
}
// 处理附件关联
function bma_process_attachments() {
if (!current_user_can('manage_options') || !check_admin_referer('bma_nonce_action', 'bma_nonce')) {
wp_die('无权限访问');
}
bma_write_log('开始处理附件关联');
$selected_attachments = isset($_POST['bma_attachments']) && is_array($_POST['bma_attachments']) ? $_POST['bma_attachments'] : array();
$process_all = $_POST['bma_process_attachments'] === 'all';
$relationships = get_transient('bma_scan_attachments_result');
if ($relationships) {
$updated = 0;
$errors = 0;
foreach ($relationships as $attachment_id => $data) {
if ($process_all || in_array($attachment_id, $selected_attachments)) {
$result = wp_update_post(array(
'ID' => $attachment_id,
'post_parent' => $data['post_id']
), true);
if (is_wp_error($result)) {
bma_write_log("附件 ID {$attachment_id} 更新失败: " . $result->get_error_message());
$errors++;
} else {
$updated++;
bma_write_log("成功为附件 ID {$attachment_id} 设置文章 ID {$data['post_id']}");
}
}
}
$summary = "处理完成!成功更新 {$updated} 个附件,发现 {$errors} 个错误";
bma_write_log($summary);
echo '<div class="updated"><p>' . $summary . '<br>请检查日志文件:' . (bma_get_log_file() ?: '日志不可用') . '</p></div>';
}
}
// 扫描并预览缩略图
function bma_scan_thumbnails_preview($per_page, $do_scan = false) {
if (!current_user_can('manage_options')) {
wp_die('无权限访问');
}
$transient_key = 'bma_scan_thumbnails_result';
$thumbnails = get_transient($transient_key);
if ($do_scan || !$thumbnails) {
bma_write_log('开始扫描缩略图文件以预览');
$upload_dir = wp_upload_dir();
$base_dir = $upload_dir['basedir'];
$base_url = $upload_dir['baseurl'];
$thumbnails = array();
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base_dir));
foreach ($iterator as $file) {
if ($file->isFile()) {
$filepath = $file->getPathname();
$filename = basename($filepath);
if (preg_match('/-\d+x\d+\.(jpg|jpeg|png|gif)$/i', $filename)) {
// 推导原始图像文件名
$original_filename = preg_replace('/-\d+x\d+\.(jpg|jpeg|png|gif)$/i', '.$1', $filename);
$original_url = str_replace($filename, $original_filename, $base_url . str_replace($base_dir, '', $filepath));
// 查找原始图像的附件 ID
$attachment_id = attachment_url_to_postid($original_url);
if ($attachment_id) {
$thumbnails[$filepath] = array(
'filename' => $filename,
'url' => $base_url . str_replace($base_dir, '', $filepath),
'original_attachment_id' => $attachment_id,
'original_title' => get_the_title($attachment_id)
);
} else {
bma_write_log("无法找到缩略图 {$filename} 的原始图像附件");
}
}
}
}
set_transient($transient_key, $thumbnails, 3600);
}
if (empty($thumbnails)) {
echo '<div class="notice notice-warning"><p>未找到缩略图文件</p></div>';
return;
}
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$total_items = count($thumbnails);
$offset = ($current_page - 1) * $per_page;
$paged_thumbnails = array_slice($thumbnails, $offset, $per_page, true);
?>
<div class="wrap">
<h1>批量关联附件 - 缩略图扫描结果</h1>
<form method="post">
<input type="hidden" name="scan_type" value="thumbnails">
<h2>缩略图文件列表(共 <?php echo $total_items; ?> 个文件)</h2>
<table class="widefat">
<thead>
<tr>
<th><input type="checkbox" id="bma_select_all_thumbnails"></th>
<th>文件路径</th>
<th>文件名</th>
<th>原始图像附件 ID</th>
<th>原始图像标题</th>
</tr>
</thead>
<tbody>
<?php foreach ($paged_thumbnails as $filepath => $data) : ?>
<tr>
<td><input type="checkbox" name="bma_thumbnails[]" value="<?php echo esc_attr($filepath); ?>"></td>
<td><?php echo esc_html($filepath); ?></td>
<td><?php echo esc_html($data['filename']); ?></td>
<td><?php echo esc_html($data['original_attachment_id']); ?></td>
<td><?php echo esc_html($data['original_title']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php echo bma_pagination($total_items, $per_page, $current_page, 'thumbnails'); ?>
<?php wp_nonce_field('bma_nonce_action', 'bma_nonce'); ?>
<p>
<button type="submit" name="bma_delete_thumbnails" class="button bma-button" value="partial_files">部分删除缩略图文件</button>
<button type="submit" name="bma_delete_thumbnails" class="button bma-button" value="all_files">全部删除缩略图文件</button>
<button type="submit" name="bma_delete_thumbnails" class="button bma-button" value="partial_metadata">部分删除缩略图注册信息</button>
<button type="submit" name="bma_delete_thumbnails" class="button bma-button" value="all_metadata">全部删除缩略图注册信息</button>
</p>
<script>
document.getElementById('bma_select_all_thumbnails').addEventListener('change', function() {
document.querySelectorAll('input[name="bma_thumbnails[]"]').forEach(checkbox => checkbox.checked = this.checked);
});
</script>
</form>
</div>
<?php
}
// 处理删除缩略图
function bma_delete_thumbnails() {
if (!current_user_can('manage_options') || !check_admin_referer('bma_nonce_action', 'bma_nonce')) {
wp_die('无权限访问');
}
bma_write_log('开始处理缩略图删除');
$selected_thumbnails = isset($_POST['bma_thumbnails']) && is_array($_POST['bma_thumbnails']) ? $_POST['bma_thumbnails'] : array();
$action = $_POST['bma_delete_thumbnails'];
$process_all = in_array($action, array('all_files', 'all_metadata'));
$delete_files = in_array($action, array('partial_files', 'all_files'));
$delete_metadata = in_array($action, array('partial_metadata', 'all_metadata'));
$thumbnails = get_transient('bma_scan_thumbnails_result');
if ($thumbnails) {
$deleted_files = 0;
$deleted_metadata = 0;
$errors = 0;
foreach ($thumbnails as $filepath => $data) {
if ($process_all || in_array($filepath, $selected_thumbnails)) {
// 删除缩略图文件
if ($delete_files && file_exists($filepath)) {
if (unlink($filepath)) {
bma_write_log("成功删除缩略图文件: {$filepath}");
$deleted_files++;
} else {
bma_write_log("删除缩略图文件失败: {$filepath}");
$errors++;
}
}
// 删除媒体库注册信息
if ($delete_metadata && isset($data['original_attachment_id'])) {
$attachment_id = $data['original_attachment_id'];
$metadata = wp_get_attachment_metadata($attachment_id);
if ($metadata && isset($metadata['sizes'])) {
$thumbnail_filename = $data['filename'];
foreach ($metadata['sizes'] as $size => $size_data) {
if ($size_data['file'] === $thumbnail_filename) {
unset($metadata['sizes'][$size]);
wp_update_attachment_metadata($attachment_id, $metadata);
bma_write_log("成功删除附件 ID {$attachment_id} 的缩略图元数据: {$size}");
$deleted_metadata++;
break;
}
}
}
}
}
}
$summary = "删除完成!共删除 {$deleted_files} 个缩略图文件,{$deleted_metadata} 个元数据记录,发现 {$errors} 个错误";
bma_write_log($summary);
echo '<div class="updated"><p>' . $summary . '<br>请检查日志文件:' . (bma_get_log_file() ?: '日志不可用') . '</p></div>';
}
}
// 分页函数
function bma_pagination($total_items, $per_page, $current_page, $scan_type) {
$total_pages = ceil($total_items / $per_page);
if ($total_pages <= 1) return '';
$base_url = admin_url('upload.php?page=bulk-media-attacher&scan_type=' . $scan_type);
$nonce = wp_create_nonce('bma_pagination_nonce');
$output = '<div class="pagination">';
$output .= paginate_links(array(
'base' => add_query_arg(array('paged' => '%#%', '_wpnonce' => $nonce), $base_url),
'format' => '',
'total' => $total_pages,
'current' => $current_page,
'prev_text' => __('« 上一页'),
'next_text' => __('下一页 »'),
));
$output .= '</div>';
return $output;
}
// 添加插件设置链接
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'bma_add_settings_link');
function bma_add_settings_link($links) {
$settings_link = '<a href="' . admin_url('upload.php?page=bulk-media-attacher') . '">' . __('设置') . '</a>';
array_push($links, $settings_link);
return $links;
}