wordpress媒体库插件-批量关联附件:bulk-media-attacher

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;
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注