Aula: Cópia e Exclusão em Massa com MU-Plugin – Substituindo o Copy Delete Posts
Objetivo: Substituir o plugin copy-delete-posts por um MU-Plugin enxuto que permite copiar e excluir posts/páginas em massa diretamente da lista administrativa, com suporte a metadados, taxonomias e feedback visual.
Contexto: O plugin original oferece cópia rápida e exclusão em massa de posts/páginas, mas é pesado e muitas vezes desnecessário para necessidades básicas. Vamos criar uma solução própria com ações em massa personalizadas, mantendo o controle total sobre o processo.
Funcionalidades implementadas no nosso MU-Plugin
- Ação em massa “Copy” – duplica posts/páginas selecionados (como rascunhos)
- Ação em massa “Delete Permanently” – exclui permanentemente os posts selecionados
- Suporte para todos os tipos de post (posts, páginas, CPTs)
- Cópia completa de metadados (
post_meta) e taxonomias (categorias, tags) - Nomes de cópias com sufixo automático ” – Copy”
- Notificações visuais no admin após cada operação (quantidade processada)
- Verificação de permissões (
edit_posts) e nonce para segurança - Tratamento de erros (exclusão de posts protegidos)
Código do MU-Plugin
Crie a pasta wp-content/mu-plugins/neodoor-copy-delete-posts/ e dentro dela o arquivo neodoor-copy-delete-posts.php com o conteúdo abaixo:
<?php
/**
* Plugin Name: Neodoor Copy Delete Posts
* Description: Adiciona ações em massa "Copy" e "Delete Permanently" na lista de posts/páginas.
* Version: 1.0
* Author: Reinaldo (Neodoor.neo)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Neodoor_Copy_Delete_Posts {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_filter( 'bulk_actions-edit-post', [ $this, 'add_bulk_actions' ] );
add_filter( 'bulk_actions-edit-page', [ $this, 'add_bulk_actions' ] );
add_filter( 'handle_bulk_actions-edit-post', [ $this, 'handle_bulk_actions' ], 10, 3 );
add_filter( 'handle_bulk_actions-edit-page', [ $this, 'handle_bulk_actions' ], 10, 3 );
add_action( 'admin_notices', [ $this, 'show_admin_notices' ] );
// Para CPTs genéricos
add_filter( 'bulk_actions-edit-product', [ $this, 'add_bulk_actions' ] );
add_filter( 'handle_bulk_actions-edit-product', [ $this, 'handle_bulk_actions' ], 10, 3 );
}
/**
* Adiciona as ações em massa no dropdown
*/
public function add_bulk_actions( $bulk_actions ) {
$bulk_actions['neodoor_bulk_copy'] = __( 'Copy', 'neodoor-copy-delete' );
$bulk_actions['neodoor_bulk_delete'] = __( 'Delete Permanently', 'neodoor-copy-delete' );
return $bulk_actions;
}
/**
* Processa as ações em massa
*/
public function handle_bulk_actions( $redirect_to, $doaction, $post_ids ) {
// Verifica se é uma das nossas ações
if ( $doaction !== 'neodoor_bulk_copy' && $doaction !== 'neodoor_bulk_delete' ) {
return $redirect_to;
}
// Verifica permissão (usuário pode editar posts)
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( 'Você não tem permissão para realizar esta ação.' );
}
// Verifica nonce (proteção CSRF)
$referer = wp_get_referer();
if ( $referer && strpos( $referer, admin_url() ) === 0 ) {
check_admin_referer( 'bulk-posts' );
}
$processed = 0;
$errors = 0;
if ( $doaction === 'neodoor_bulk_copy' ) {
foreach ( $post_ids as $post_id ) {
$new_post_id = $this->duplicate_post( $post_id );
if ( $new_post_id ) {
$processed++;
} else {
$errors++;
}
}
} elseif ( $doaction === 'neodoor_bulk_delete' ) {
foreach ( $post_ids as $post_id ) {
// Verifica se o post pode ser excluído permanentemente
if ( wp_delete_post( $post_id, true ) ) {
$processed++;
} else {
$errors++;
}
}
}
// Adiciona argumentos na URL de redirecionamento para exibir notificações
$redirect_to = add_query_arg(
[
'neodoor_action' => $doaction,
'neodoor_processed' => $processed,
'neodoor_errors' => $errors,
],
$redirect_to
);
return $redirect_to;
}
/**
* Duplica um post e seus metadados/taxonomias
* @param int $post_id ID do post original
* @return int|false ID do novo post ou false em caso de erro
*/
private function duplicate_post( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
// Prepara o array para o novo post (como rascunho)
$new_post = [
'post_title' => $post->post_title . ' – Copy',
'post_content' => $post->post_content,
'post_excerpt' => $post->post_excerpt,
'post_status' => 'draft',
'post_type' => $post->post_type,
'post_author' => get_current_user_id(),
];
// Insere o novo post
$new_post_id = wp_insert_post( $new_post, true );
if ( is_wp_error( $new_post_id ) ) {
return false;
}
// Copia metadados (post_meta) – inclusive os com underscore
global $wpdb;
$meta_data = $wpdb->get_results( $wpdb->prepare(
"SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d",
$post_id
) );
foreach ( $meta_data as $meta ) {
add_post_meta( $new_post_id, $meta->meta_key, maybe_unserialize( $meta->meta_value ) );
}
// Copia taxonomias (categorias, tags, etc.)
$taxonomies = get_post_taxonomies( $post_id );
foreach ( $taxonomies as $taxonomy ) {
$terms = wp_get_object_terms( $post_id, $taxonomy, [ 'fields' => 'ids' ] );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
wp_set_object_terms( $new_post_id, $terms, $taxonomy );
}
}
return $new_post_id;
}
/**
* Exibe notificações após o processamento das ações em massa
*/
public function show_admin_notices() {
if ( empty( $_GET['neodoor_action'] ) ) {
return;
}
$action = sanitize_text_field( $_GET['neodoor_action'] );
$processed = isset( $_GET['neodoor_processed'] ) ? intval( $_GET['neodoor_processed'] ) : 0;
$errors = isset( $_GET['neodoor_errors'] ) ? intval( $_GET['neodoor_errors'] ) : 0;
$message = '';
if ( $action === 'neodoor_bulk_copy' ) {
if ( $processed === 1 ) {
$message = sprintf( '1 post copiado com sucesso (rascunho).', $processed );
} elseif ( $processed > 1 ) {
$message = sprintf( '%d posts copiados com sucesso.', $processed );
}
} elseif ( $action === 'neodoor_bulk_delete' ) {
if ( $processed === 1 ) {
$message = sprintf( '1 post excluído permanentemente.', $processed );
} elseif ( $processed > 1 ) {
$message = sprintf( '%d posts excluídos permanentemente.', $processed );
}
}
if ( $errors > 0 ) {
$message .= sprintf( ' %d erro(s) ignorado(s).', $errors );
}
if ( ! empty( $message ) ) {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
}
}
}
Neodoor_Copy_Delete_Posts::get_instance();
Suporte para Custom Post Types (CPTs)
O código acima já inclui suporte para produtos (WooCommerce) como exemplo. Para adicionar outros CPTs, basta duplicar as linhas dos filtros trocando o nome do post type. Exemplo:
add_filter( 'bulk_actions-edit-<SEU_CPT>', [ $this, 'add_bulk_actions' ] );
add_filter( 'handle_bulk_actions-edit-<SEU_CPT>', [ $this, 'handle_bulk_actions' ], 10, 3 );
Substitua <SEU_CPT> pelo slug do seu tipo de post personalizado.
Como testar na prática
- Após criar o MU-Plugin, acesse Posts → Todos os posts ou Páginas → Todas as páginas.
- Selecione um ou mais posts/páginas usando as caixas de seleção.
- No dropdown “Ações em massa”, escolha “Copy” ou “Delete Permanently”.
- Clique em “Aplicar”.
- Uma notificação verde aparecerá informando quantos itens foram processados.
Exemplo visual
+-----------------------------------------------------------+
| [ ] Título do Post 1 |
| [ ] Título do Post 2 |
| [ ] Título do Post 3 |
| |
| Ações em massa: [Copy ▼] [Aplicar] |
+-----------------------------------------------------------+
↓
+-----------------------------------------------------------+
| ✅ 3 posts copiados com sucesso. |
+-----------------------------------------------------------+
Funcionalidades em detalhe
- Cópia: O novo post é criado como rascunho e recebe o sufixo ” – Copy”. Todos os metadados (campos personalizados, dados do Elementor) e taxonomias (categorias, tags) são preservados.
- Exclusão permanente: Os posts selecionados são removidos definitivamente do banco de dados, sem passar pela lixeira. Atenção: esta operação é irreversível!
- Segurança: O plugin verifica se o usuário tem permissão para editar posts e utiliza nonce para proteção contra CSRF.
- Feedback: Após a operação, uma notificação amigável informa quantos itens foram processados com sucesso.
Personalizações possíveis
- Alterar o sufixo da cópia: Modifique a linha
'post_title' => $post->post_title . ' – Copy'no métododuplicate_post(). - Copiar como publicado: Troque
'post_status' => 'draft'por'publish'. - Adicionar confirmação JavaScript antes de excluir: Insira o código abaixo no MU-Plugin:
add_action( 'admin_footer-edit.php', function() {
?>
<script>
jQuery(function($) {
$('select[name="action"], select[name="action2"]').change(function() {
if ($(this).val() === 'neodoor_bulk_delete') {
return confirm('Tem certeza que deseja excluir permanentemente os posts selecionados? Esta ação não pode ser desfeita.');
}
});
});
</script>
<?php
} );
Comparação com o plugin original
Nosso MU-Plugin substitui as principais funções do copy-delete-posts, com as seguintes vantagens:
- Mais leve: Apenas 150 linhas de código, sem CSS/JS desnecessários.
- Transparente: O código é simples e fácil de modificar.
- Sem anúncios ou rastreadores: O plugin original exibe propagandas e coleta dados; nossa versão é 100% limpa.
- Atualizado: Compatível com as versões mais recentes do WordPress (6.x).
- Cópia completa: Preserva metadados com underscore (essenciais para Elementor e outros builders) e taxonomias.
Observações finais
- Backup recomendado: Antes de usar a exclusão em massa, faça um backup do banco de dados – especialmente em produção.
- Performance: Para copiar/excluir centenas de posts, o processo pode levar alguns segundos. O WordPress lida bem com lotes moderados.
- Exclusão permanente: Diferente da lixeira, os posts excluídos não podem ser recuperados. Use com cautela.
- Elementor: O código copia todos os metadados, incluindo os dados de layout do Elementor. O novo post será uma réplica fiel, pronta para edição.
Registro no caderno
Data:
Funcionalidade: Cópia e exclusão em massa de posts/páginas
Plugins substituídos: copy-delete-posts
Tempo de implementação: ~15 minutos (configuração e testes)
Observação: Para CPTs adicionais, basta estender os filtros conforme explicado.