[MU] Copy Delete Posts

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

  1. Após criar o MU-Plugin, acesse Posts → Todos os posts ou Páginas → Todas as páginas.
  2. Selecione um ou mais posts/páginas usando as caixas de seleção.
  3. No dropdown “Ações em massa”, escolha “Copy” ou “Delete Permanently”.
  4. Clique em “Aplicar”.
  5. 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étodo duplicate_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.

Média: 3.1 (32 votos)