Função para localizar e corrigir links quebrados no WordPress

No comments

29.08.2026|

No comments

Tiempo de lectura Lectura: 29 min, 25 s
Número de palabras Palabras: 5444
Número de visitas Visitas: 29
Icono de traducción

Links quebrados causam perda de autoridade (link juice) e consomem o orçamento de rastreamento (crawl budget), já que os bots dos mecanismos de busca perdem tempo indexando respostas HTTP 404 em vez de conteúdo de valor. Além disso, prejudicam a experiência do usuário ao interromper o fluxo de navegação e aumentam a taxa de rejeição. Ao corrigi-los com redirecionamentos 301 ou atualizando as URLs, otimiza-se o rastreamento do site, preserva-se o fluxo de equidade de links para as páginas de destino e consolida-se a arquitetura do site para evitar penalizações indiretas no posicionamento orgânico.

Ao fazer a verificação de rotina de links quebrados com o plugin de sempre, o Broken Link Checker, descobri que a varredura local não estava funcionando. Então, peguei uma função bem simples que tinha guardada e a aprimorei um pouco com a ajuda do Gemini para torná-la mais prática e executar a varredura e a correção de links quebrados.

Embora os links quebrados surjam como cogumelos, nesta jornada notei uma perda significativa de links do Twitter (X), que está perdendo contas aos montes. Já aparecem centenas de contas e/ou tuítes que deixaram de existir, mas vou deixar esse assunto para um artigo futuro sobre a morte da web como a conhecíamos.

Interface do recurso de verificação de links quebrados para o WordPress, com pontos explicativos para cada uma de suas partes

Para usar a função, basta adicionar o snippet ao arquivo functions.php do seu modelo ou tema filho, ou ao seu plugin de snippets; em Ferramentas/Inspeção de Links, você encontrará a ferramenta e suas opções, detalhadas a seguir.

1 - Tipos de estado. Aqui você pode escolher entre mais de 10 tipos de estado para uma varredura seletiva.

2 - O botão “Iniciar Varredura Contínua” executa uma varredura sem interrupções. Embora não devesse, isso pode ter algum impacto na velocidade de carregamento do site. Já o mantive funcionando por horas sem problemas.

3 - Digitalização em lote. Serão digitalizados 10 itens cada vez que você clicar neste botão.

4 - Exportar/Importar JSON. Como o plugin não grava nada no banco de dados, se você fechar o navegador, perderá a lista; e, para recuperar tudo o que não se lembrar de ter feito, precisará sempre salvar sua lista de varredura com a opção “Exportar”. Para recuperá-la, basta importá-la.

5 - Informações sobre o processo de digitalização.

6 - Filtragem por tipos. Depois de concluir a varredura, você pode filtrar a exibição da lista por estados.

7 - Postagem/Página. Aqui você encontrará um link para editar a postagem e outro para (Ver), para acessá-la.

8 - O âncora do link para que você o encontre mais rapidamente.

9 - A URL do link quebrado.

10 - Código de estado HTTP do link.

11 - Ação. O ícone de fechar apenas oculta essa linha da visualização da lista. Ele não edita, não apaga nem adiciona nada.

12 - URL de substituição. Campo onde se insere o link de substituição.

13 - Pesquisar no Archive.org. Ao clicar aqui, o Wayback Machine será aberto com a lista de datas disponíveis das cópias do seu URL (se houver) para que você escolha aquela que mais lhe interessar.

14 - Remover link. Essa opção remove o hiperlink, mantendo o texto do link na postagem ou página, caso você não tenha uma URL alternativa ou não queira adicioná-la.

Deixa eu esclarecer desde já que essa é uma função descartável; recomenda-se que você a remova assim que terminar a limpeza. Não acho que vá modificá-la novamente, mas, se isso acontecer, atualizarei o código aqui mesmo.

Embora no final tenha ficado um código muito longo para um snippet — que, em teoria, deveria ser algo curto ou um fragmento —, ele funciona muito bem. É claro que ainda dá para aprimorá-lo para evitar certos falsos positivos de 404 e melhorá-lo com a seleção múltipla de ações, mas se você não tiver muitos links e tempo para revisá-los manualmente, ele é totalmente funcional.

Para substituições em massa de links, você pode recorrer aos plugins clássicos dedicados, como o Better Search Replace e o Search Regex.

Atualização em 23 de agosto de 2026.

Melhorias:

1 - É adicionada uma caixa de seleção múltipla à esquerda e um botão“Descartar os selecionados” para remover linhas da lista em lotes.

2 - O tempo de resposta para os links do Archive.org e do Bluesky foi ajustado para 7 segundos, a fim de evitar que apareçam muitos resultados 403 ou “Timeout” com esses domínios.

3 - O processo de carregamento do arquivo JSON na opção “Importar JSON” foi otimizado

Atualização em 24 de agosto de 2026

Novo sistema de exclusão de domínios (Proteção de IP): Foi integrada uma matriz de exclusão robusta para ignorar grandes sites e redes sociais.

Pulo silencioso: A partir de agora, se o scanner detectar um link que aponte para o Google, YouTube, Twitter/X, Facebook, Instagram, LinkedIn, Archive.org, Amazon, Pinterest ou TikTok, ele o ignorará automaticamente, sem enviar nenhuma solicitação HTTP para esses sites. Portanto, você precisará verificar esses links com outra ferramenta ou manualmente.

Zero falsos positivos e sem bloqueios: essa medida evita que o endereço IP do seu servidor seja temporariamente incluído em listas negras devido ao excesso de solicitações (limitação de taxa, como os bloqueios 429 nas APIs do Google). Além disso, elimina os resultados de “links quebrados falsos” (erros 403) causados pelos agressivos firewalls anti-bots dessas plataformas, tornando a varredura geral muito mais rápida e eficiente.


// Actualizado 24 de agosto 2026 Inspector de Enlaces Rotos (Con Reemplazo Directo AJAX, Desvinculación de Enlace, Filtro de Vista, Exportar/Importar JSON)

if (!defined('ABSPATH')) exit;

// 1. Menú en Herramientas
add_action('admin_menu', function() {
    add_management_page('Inspector Enlaces', 'Inspector Enlaces', 'manage_options', 'inspector-enlaces-rotos', 'ier_render_admin_page');
});

// 2. Registro AJAX
add_action('wp_ajax_ier_scan_single_post', 'ier_scan_single_post_callback');
add_action('wp_ajax_ier_replace_link', 'ier_replace_link_callback');
add_action('wp_ajax_ier_unlink_link', 'ier_unlink_link_callback');

// 3. Interfaz HTML + JavaScript
function ier_render_admin_page() {
    global $wpdb;
    $ajax_url = admin_url('admin-ajax.php');
    $total_posts = (int) $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type IN ('post', 'page') AND post_status = 'publish'");
    
    echo '<style>.ier-hidden-row { display: none !important; }</style>';
    echo '<div class="wrap">';
    echo '<h1>Inspector de Enlaces Rotos</h1>';
    echo '<p>Escaneo ultra-ligero con reemplazo directo, desvinculación, filtro por estados, exportación/importación JSON y protección anti-bloqueos (excluye RRSS y Google).</p>';
    
    echo '<div style="background: #fff; padding: 12px 15px; border: 1px solid #ccc; max-width: 800px; border-radius: 4px; margin-bottom: 15px;">';
    echo '<strong style="display: block; margin-bottom: 8px;">Tipos de estado a escanear:</strong>';
    echo '<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;" id="ier-status-filters">';
    echo '<label style="font-weight: bold; margin-right: 10px;"><input type="checkbox" id="ier-check-all" checked> [ Todos ]</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="404" checked> 404</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="404 Interno" checked> 404 Interno</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="Timeout / Error Red" checked> Timeout / Error Red</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="403" checked> 403</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="400" checked> 400</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="500" checked> 500</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="502" checked> 502</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="503" checked> 503</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="520" checked> 520</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="Otros / Desconocido" checked> Otros</label>';
    echo '</div>';
    echo '</div>';

    echo '<div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">';
    echo '<button id="ier-start-btn" type="button" class="button button-primary">Iniciar Escaneo Continuo</button>';
    echo '<button id="ier-next-10-btn" type="button" class="button button-secondary">Escanear 10 Entradas</button>';
    echo '<button id="ier-stop-btn" type="button" class="button button-secondary" style="display:none;">Pausar</button>';
    echo '<span style="border-left: 1px solid #ccc; height: 24px; margin: 0 5px;"></span>';
    echo '<button id="ier-export-btn" type="button" class="button button-secondary">Exportar JSON</button>';
    echo '<button id="ier-import-trigger-btn" type="button" class="button button-secondary">Importar JSON</button>';
    echo '<input type="file" id="ier-import-file" accept=".json" style="display: none;">';
    echo '</div>';
    
    echo '<div style="margin-top: 15px; background: #fff; padding: 12px; border: 1px solid #ccc; max-width: 600px; border-radius: 4px;">';
    echo '<strong>Estado:</strong> <span id="ier-status-text">Listo para comenzar</span><br>';
    echo '<strong>Progresión:</strong> Procesadas <span id="ier-processed-count">0</span> de <strong>' . $total_posts . '</strong><br>';
    echo '<strong>Última entrada analizada:</strong> <em id="ier-current-title" style="color: #2271b1;">-</em><br>';
    echo '<strong>Enlaces rotos localizados:</strong> <span id="ier-count-broken" style="color: #d63638; font-weight: bold;">0</span>';
    echo '</div>';

    echo '<div style="margin-top: 20px; display: flex; align-items: center; gap: 10px; background: #f6f7f7; padding: 10px; border: 1px solid #c3c4c7; border-radius: 4px;">';
    echo '<strong>Filtrar vista por tipo:</strong> ';
    echo '<select id="ier-view-filter-select" style="max-width: 250px;"><option value="ALL">Mostrar todos los estados</option></select> ';
    echo '<span id="ier-view-filter-count" style="color: #646970; font-style: italic;"></span>';
    echo '<span style="border-left: 1px solid #ccc; height: 20px; margin: 0 5px;"></span>';
    echo '<button id="ier-bulk-dismiss-btn" type="button" class="button button-secondary">Descartar seleccionados</button>';
    echo '</div>';

    echo '<table class="widefat fixed striped" style="margin-top: 10px;">';
    echo '<thead><tr>';
    echo '<th style="width: 3%; text-align: center;"><input type="checkbox" id="ier-bulk-select-all" title="Seleccionar todos los visibles"></th>';
    echo '<th style="width: 17%;">Post / Página</th>';
    echo '<th style="width: 16%;">Texto / Anchor</th>';
    echo '<th style="width: 23%;">URL Rota</th>';
    echo '<th style="width: 12%;">Estado</th>';
    echo '<th style="width: 24%;">Sustituir por (Wayback/Nuevo)</th>';
    echo '<th style="width: 5%; text-align: center;">Acción</th>';
    echo '</tr></thead>';
    echo '<tbody id="ier-results-body"><tr class="ier-empty-row"><td colspan="7">Haz clic en un botón de arriba para comenzar o importa un JSON guardado.</td></tr></tbody>';
    echo '</table>';
    echo '</div>';

    ?>
    <script>
    jQuery(document).ready(function($) {
        const totalPosts = <?php echo $total_posts; ?>;
        const ajaxUrl = '<?php echo esc_url($ajax_url); ?>';
        let lastId = 0;
        let processedCount = 0;
        let scanning = false;
        let totalBroken = 0;
        let targetBatchCount = 0;
        let currentBatchProgress = 0;

        $('#ier-check-all').on('change', function() {
            $('.ier-status-cb').prop('checked', $(this).is(':checked'));
        });

        $('.ier-status-cb').on('change', function() {
            if (!$(this).is(':checked')) {
                $('#ier-check-all').prop('checked', false);
            } else {
                if ($('.ier-status-cb:not(:checked)').length === 0) {
                    $('#ier-check-all').prop('checked', true);
                }
            }
        });

        function getSelectedStatuses() {
            const selected = [];
            $('.ier-status-cb:checked').each(function() {
                selected.push($(this).val());
            });
            return selected;
        }

        $('#ier-bulk-select-all').on('change', function() {
            $('.ier-bulk-cb:visible').prop('checked', $(this).is(':checked'));
        });

        $('#ier-bulk-dismiss-btn').on('click', function(e) {
            e.preventDefault();
            const $checked = $('.ier-bulk-cb:checked:visible');
            if ($checked.length === 0) {
                alert('No hay enlaces seleccionados en la vista actual.');
                return;
            }
            if (!confirm('¿Seguro que quieres descartar ' + $checked.length + ' enlaces de la lista?')) {
                return;
            }

            $checked.each(function() {
                $(this).closest('tr').remove();
                if (totalBroken > 0) totalBroken--;
            });

            $('#ier-count-broken').text(totalBroken);
            $('#ier-bulk-select-all').prop('checked', false);
            updateViewFilterDropdown();
            
            if ($('#ier-results-body tr:not(.ier-empty-row)').length === 0) {
                $('#ier-results-body').html('<tr class="ier-empty-row"><td colspan="7">No hay enlaces rotos pendientes en el listado.</td></tr>');
            }
        });

        function updateViewFilterDropdown() {
            const currentSelected = $('#ier-view-filter-select').val() || 'ALL';
            const statusSet = new Set();

            $('#ier-results-body tr:not(.ier-empty-row)').each(function() {
                const status = $(this).attr('data-status');
                if (status) statusSet.add(status);
            });

            const $select = $('#ier-view-filter-select');
            $select.empty();
            $select.append('<option value="ALL">Mostrar todos los estados</option>');

            Array.from(statusSet).sort().forEach(function(status) {
                $select.append('<option value="' + status + '">' + status + '</option>');
            });

            if (statusSet.has(currentSelected)) {
                $select.val(currentSelected);
            } else {
                $select.val('ALL');
            }

            applyViewFilter();
        }

        function applyViewFilter() {
            const selectedStatus = $('#ier-view-filter-select').val();
            let visibleCount = 0;
            const $rows = $('#ier-results-body tr:not(.ier-empty-row)');

            if (selectedStatus === 'ALL') {
                $rows.removeClass('ier-hidden-row');
                visibleCount = $rows.length;
                $('#ier-view-filter-count').text('');
            } else {
                $rows.each(function() {
                    const $tr = $(this);
                    if ($tr.attr('data-status') === selectedStatus) {
                        $tr.removeClass('ier-hidden-row');
                        visibleCount++;
                    } else {
                        $tr.addClass('ier-hidden-row');
                    }
                });
                $('#ier-view-filter-count').text('(' + visibleCount + ' de ' + totalBroken + ' visibles)');
            }
            
            $('#ier-bulk-select-all').prop('checked', false);
        }

        $('#ier-view-filter-select').on('change', function() {
            applyViewFilter();
        });

        $(document).on('click', '.ier-replace-btn', function(e) {
            e.preventDefault();
            const $btn = $(this);
            const $tr = $btn.closest('tr');
            
            let postId = parseInt($tr.attr('data-post-id'), 10) || 0;
            if (!postId) {
                const editUrl = $tr.find('.ier-edit-link').attr('href') || '';
                const match = editUrl.match(/post=(\d+)/);
                if (match) {
                    postId = parseInt(match[1], 10);
                }
            }

            const oldUrl = $tr.attr('data-old-url');
            const $input = $tr.find('.ier-new-url-input');
            const newUrl = $input.val().trim();

            if (!postId) {
                alert('Error: ID del Post no válido o ausente.');
                return;
            }
            if (!oldUrl) {
                alert('Error: URL original no encontrada en el elemento.');
                return;
            }
            if (!newUrl) {
                alert('Introduce una nueva URL para reemplazar.');
                return;
            }

            $btn.prop('disabled', true).text('Guardando...');
            $input.prop('disabled', true);

            $.ajax({
                url: ajaxUrl,
                type: 'POST',
                data: { action: 'ier_replace_link', post_id: postId, old_url: oldUrl, new_url: newUrl },
                success: function(response) {
                    if (response.success) {
                        $tr.attr('data-old-url', newUrl);
                        $input.prop('disabled', false);
                        $tr.css({'background-color': '#d4edda'});
                        $btn.text('¡Guardado!').css({'background': '#28a745', 'border-color': '#28a745', 'color': '#fff'}).prop('disabled', false);
                        setTimeout(function() {
                            $btn.text('Guardar').css({'background': '', 'border-color': '', 'color': ''});
                            $tr.css({'background-color': ''});
                        }, 2000);
                    } else {
                        alert('Error al reemplazar: ' + (response.data || 'No se encontró la URL en la entrada.'));
                        $btn.prop('disabled', false).text('Guardar');
                        $input.prop('disabled', false);
                    }
                },
                error: function() {
                    alert('Error en la solicitud AJAX.');
                    $btn.prop('disabled', false).text('Guardar');
                    $input.prop('disabled', false);
                }
            });
        });

        $(document).on('click', '.ier-unlink-btn', function(e) {
            e.preventDefault();
            const $btn = $(this);
            const $tr = $btn.closest('tr');

            let postId = parseInt($tr.attr('data-post-id'), 10) || 0;
            if (!postId) {
                const editUrl = $tr.find('.ier-edit-link').attr('href') || '';
                const match = editUrl.match(/post=(\d+)/);
                if (match) {
                    postId = parseInt(match[1], 10);
                }
            }

            const urlToUnlink = $tr.attr('data-old-url');

            if (!postId || !urlToUnlink) {
                alert('Error: Datos del post o URL no encontrados.');
                return;
            }

            if (!confirm('¿Quieres eliminar la etiqueta de enlace mantención el texto ancla intacto?')) {
                return;
            }

            $btn.prop('disabled', true).text('Quitando...');

            $.ajax({
                url: ajaxUrl,
                type: 'POST',
                data: { action: 'ier_unlink_link', post_id: postId, url: urlToUnlink },
                success: function(response) {
                    if (response.success) {
                        $tr.css({ 'background-color': '#f8d7da', 'transition': 'background-color 0.4s ease' });
                        $btn.text('Enlace quitado').css({'background': '#d63638', 'border-color': '#d63638', 'color': '#fff'});
                        setTimeout(function() {
                            $tr.fadeOut(300, function() {
                                $(this).remove();
                                if (totalBroken > 0) {
                                    totalBroken--;
                                    $('#ier-count-broken').text(totalBroken);
                                }
                                updateViewFilterDropdown();
                                if ($('#ier-results-body tr:not(.ier-empty-row)').length === 0) {
                                    $('#ier-results-body').html('<tr class="ier-empty-row"><td colspan="7">No hay enlaces rotos pendientes en el listado.</td></tr>');
                                }
                            });
                        }, 1000);
                    } else {
                        alert('Error al desvincular: ' + (response.data || 'No se pudo eliminar el enlace.'));
                        $btn.prop('disabled', false).text('Quitar enlace');
                    }
                },
                error: function() {
                    alert('Error en la solicitud AJAX.');
                    $btn.prop('disabled', false).text('Quitar enlace');
                }
            });
        });

        $('#ier-start-btn').on('click', function(e) {
            e.preventDefault();
            if (getSelectedStatuses().length === 0) {
                alert('Selecciona al menos un tipo de estado para escanear.');
                return;
            }
            scanning = true;
            targetBatchCount = totalPosts;
            currentBatchProgress = 0;
            toggleButtons(true);
            if (processedCount === 0) $('#ier-results-body').empty();
            processNext();
        });

        $('#ier-next-10-btn').on('click', function(e) {
            e.preventDefault();
            if (getSelectedStatuses().length === 0) {
                alert('Selecciona al menos un tipo de estado para escanear.');
                return;
            }
            scanning = true;
            targetBatchCount = 10;
            currentBatchProgress = 0;
            toggleButtons(true);
            if (processedCount === 0) $('#ier-results-body').empty();
            processNext();
        });

        $('#ier-stop-btn').on('click', function(e) {
            e.preventDefault();
            scanning = false;
            toggleButtons(false);
            $('#ier-status-text').text('Pausado por el usuario.');
        });

        $(document).on('click', '.ier-dismiss-row', function(e) {
            e.preventDefault();
            const $tr = $(this).closest('tr');
            $tr.fadeOut(150, function() {
                $(this).remove();
                if (totalBroken > 0) {
                    totalBroken--;
                    $('#ier-count-broken').text(totalBroken);
                }
                updateViewFilterDropdown();
                if ($('#ier-results-body tr:not(.ier-empty-row)').length === 0) {
                    $('#ier-results-body').html('<tr class="ier-empty-row"><td colspan="7">No hay enlaces rotos pendientes en el listado.</td></tr>');
                }
            });
        });

        $('#ier-export-btn').on('click', function(e) {
            e.preventDefault();
            const rows = [];

            $('#ier-results-body tr:not(.ier-empty-row)').each(function() {
                const $tr = $(this);
                if ($tr.find('td').length < 7) return;

                const $postLink = $tr.find('.ier-edit-link');
                const postTitle = $postLink.text();
                const postEditUrl = $postLink.attr('href') || '';
                const postViewUrl = $tr.find('.ier-view-link').attr('href') || '';
                
                let postId = parseInt($tr.attr('data-post-id'), 10) || 0;
                if (!postId && postEditUrl) {
                    const match = postEditUrl.match(/post=(\d+)/);
                    if (match) postId = parseInt(match[1], 10);
                }

                const anchorHtml = $tr.find('td:nth-child(3)').html();
                const url = $tr.attr('data-old-url') || $tr.find('td:nth-child(4) a').text();
                const statusHtml = $tr.attr('data-status') || $tr.find('td:nth-child(5) strong').text();

                rows.push({
                    post_id: postId,
                    post_title: postTitle,
                    post_edit_url: postEditUrl,
                    post_view_url: postViewUrl,
                    anchor: anchorHtml,
                    url: url,
                    status: statusHtml
                });
            });

            if (rows.length === 0) {
                alert('No hay enlaces rotos en la lista para exportar.');
                return;
            }

            const exportData = {
                created_at: new Date().toISOString(),
                last_id: lastId,
                processed_count: processedCount,
                total_broken: rows.length,
                broken_links: rows
            };

            const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportData, null, 2));
            const downloadAnchor = document.createElement('a');
            const dateSuffix = new Date().toISOString().slice(0, 10);
            
            downloadAnchor.setAttribute("href", dataStr);
            downloadAnchor.setAttribute("download", "enlaces-rotos-" + dateSuffix + ".json");
            document.body.appendChild(downloadAnchor);
            downloadAnchor.click();
            downloadAnchor.remove();
        });

        $('#ier-import-trigger-btn').on('click', function(e) {
            e.preventDefault();
            $('#ier-import-file').click();
        });

        $('#ier-import-file').on('change', function(e) {
            const file = e.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(evt) {
                try {
                    const data = JSON.parse(evt.target.result);
                    if (!data.broken_links || !Array.isArray(data.broken_links)) {
                        alert('El archivo JSON no tiene un formato válido.');
                        return;
                    }

                    $('#ier-results-body').empty();
                    totalBroken = data.broken_links.length;
                    $('#ier-count-broken').text(totalBroken);

                    if (typeof data.last_id !== 'undefined') lastId = data.last_id;
                    if (typeof data.processed_count !== 'undefined') {
                        processedCount = data.processed_count;
                        $('#ier-processed-count').text(processedCount);
                    }

                    let allRowsHtml = '';
                    data.broken_links.forEach(function(item) {
                        allRowsHtml += buildRowHtml(item);
                    });
                    
                    $('#ier-results-body').html(allRowsHtml);

                    updateViewFilterDropdown();
                    $('#ier-status-text').text('Datos importados correctamente desde JSON.');
                    $('#ier-import-file').val('');
                } catch (err) {
                    alert('Error al leer el archivo JSON: ' + err.message);
                }
            };
            reader.readAsText(file);
        });

        function escapeAttr(str) {
            return String(str || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
        }

        function buildRowHtml(item) {
            const cleanStatus = typeof item.status === 'string' ? item.status.replace(/<[^>]*>?/gm, '') : item.status;
            
            let postId = parseInt(item.post_id, 10) || 0;
            if (!postId && item.post_edit_url) {
                const match = item.post_edit_url.match(/post=(\d+)/);
                if (match) {
                    postId = parseInt(match[1], 10);
                }
            }

            let rawUrl = item.url;
            try {
                rawUrl = decodeURIComponent(item.url);
            } catch (e) {
                rawUrl = item.url;
            }

            const archiveSearchUrl = 'https://web.archive.org/web/*/' + rawUrl;
            const viewUrl = (item.post_view_url && item.post_view_url !== '#') ? item.post_view_url : '<?php echo esc_js(home_url('/?p=')); ?>' + postId;

            return '<tr data-status="' + escapeAttr(cleanStatus) + '" data-post-id="' + postId + '" data-old-url="' + escapeAttr(item.url) + '">' +
                '<td style="text-align: center;"><input type="checkbox" class="ier-bulk-cb"></td>' +
                '<td>' +
                    '<a href="' + escapeAttr(item.post_edit_url) + '" target="_blank" class="ier-edit-link">' + item.post_title + '</a> ' +
                    '<a href="' + escapeAttr(viewUrl) + '" target="_blank" class="ier-view-link" style="font-size: 11px; text-decoration: none; color: #2271b1;" title="Ver entrada pública">(Ver)</a>' +
                '</td>' +
                '<td>' + item.anchor + '</td>' +
                '<td><a href="' + escapeAttr(item.url) + '" target="_blank" style="color:#d63638;">' + item.url + '</a></td>' +
                '<td><strong>' + item.status + '</strong></td>' +
                '<td>' +
                    '<div style="display: flex; gap: 4px;">' +
                        '<input type="url" class="ier-new-url-input" placeholder="https://..." style="width: 100%; font-size: 11px; padding: 2px 4px;">' +
                        '<button type="button" class="button button-small ier-replace-btn" title="Reemplazar URL en la BD">Guardar</button>' +
                    '</div>' +
                    '<div style="margin-top: 4px; display: flex; justify-content: space-between; align-items: center;">' +
                        '<a href="' + escapeAttr(archiveSearchUrl) + '" target="_blank" style="font-size: 10px; color: #2271b1; text-decoration: none;">🔍 Buscar en Archive.org</a>' +
                        '<button type="button" class="button-link ier-unlink-btn" style="font-size: 10px; color: #d63638; text-decoration: underline;" title="Quitar enlace y dejar solo texto">Quitar enlace</button>' +
                    '</div>' +
                '</td>' +
                '<td style="text-align: center;"><button type="button" class="button-link ier-dismiss-row" title="Descartar de la lista" style="color: #a7aaad; text-decoration: none; font-size: 18px; font-weight: bold; line-height: 1;">&times;</button></td>' +
            '</tr>';
        }

        function appendRow(item) {
            $('.ier-empty-row').remove();
            $('#ier-results-body').append(buildRowHtml(item));
        }

        function processNext() {
            if (!scanning || currentBatchProgress >= targetBatchCount || processedCount >= totalPosts) {
                scanning = false;
                toggleButtons(false);
                $('#ier-status-text').text(processedCount >= totalPosts ? 'Escaneo completado.' : 'Lote procesado correctamente.');
                return;
            }

            $('#ier-status-text').text('Consultando ID superior a ' + lastId + '...');

            $.ajax({
                url: ajaxUrl,
                type: 'POST',
                data: { action: 'ier_scan_single_post', last_id: lastId, allowed_statuses: getSelectedStatuses() },
                success: function(response) {
                    if (response.success) {
                        const data = response.data;

                        if (data.finished) {
                            scanning = false;
                            toggleButtons(false);
                            $('#ier-status-text').text('Fin de la base de datos.');
                            return;
                        }

                        lastId = data.post_id;
                        processedCount++;
                        currentBatchProgress++;

                        $('#ier-processed-count').text(processedCount);
                        $('#ier-current-title').text(data.post_title + ' (ID ' + data.post_id + ')');

                        if (data.broken_links && data.broken_links.length > 0) {
                            totalBroken += data.broken_links.length;
                            $('#ier-count-broken').text(totalBroken);

                            data.broken_links.forEach(function(item) {
                                appendRow(item); 
                            });

                            updateViewFilterDropdown();
                        }

                        if (scanning) {
                            setTimeout(processNext, 20);
                        }
                    } else {
                        $('#ier-status-text').text('Error del servidor: ' + (response.data || 'Respuesta vacía'));
                        scanning = false;
                        toggleButtons(false);
                    }
                },
                error: function(xhr, status, error) {
                    console.warn('Fallo en ID > ' + lastId + '. Forzando salto al siguiente ID...', status, error);
                    $('#ier-status-text').text('Timeout en ID > ' + lastId + '. Saltando entrada problemática...');
                    
                    lastId++;
                    processedCount++;
                    currentBatchProgress++;
                    
                    if (scanning) {
                        setTimeout(processNext, 1000);
                    }
                }
            });
        }

        function toggleButtons(isScanning) {
            if (isScanning) {
                $('#ier-start-btn, #ier-next-10-btn, #ier-export-btn, #ier-import-trigger-btn').hide();
                $('.ier-status-cb, #ier-check-all').prop('disabled', true);
                $('#ier-stop-btn').show();
            } else {
                $('#ier-stop-btn').hide();
                $('.ier-status-cb, #ier-check-all').prop('disabled', false);
                $('#ier-start-btn, #ier-next-10-btn, #ier-export-btn, #ier-import-trigger-btn').show();
                $('#ier-start-btn').text('Continuar Escaneo');
            }
        }
    });
    </script>
    <?php
}

// 4. Servidor: Analizar entrada individual
function ier_scan_single_post_callback() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Sin permisos');
    }

    if (function_exists('wp_ob_end_flush_all')) {
        @wp_ob_end_flush_all();
    }

    @set_time_limit(20);

    global $wpdb;
    $last_id = isset($_POST['last_id']) ? intval($_POST['last_id']) : 0;
    $allowed_statuses = isset($_POST['allowed_statuses']) && is_array($_POST['allowed_statuses']) ? $_POST['allowed_statuses'] : [];

    $post = $wpdb->get_row($wpdb->prepare(
        "SELECT ID, post_title, post_content FROM {$wpdb->posts} WHERE post_type IN ('post', 'page') AND post_status = 'publish' AND ID > %d ORDER BY ID ASC LIMIT 1",
        $last_id
    ));

    if (!$post) {
        wp_send_json_success(['finished' => true]);
    }

    $title = $post->post_title;
    $content = $post->post_content;
    $broken_links = [];
    $post_view_url = get_permalink($post->ID);

    if (!empty($content)) {
        $regex = '/<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is';

        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
            $site_url = site_url();
            $site_host = parse_url($site_url, PHP_URL_HOST);
            $start_time = microtime(true);

            foreach ($matches as $match) {
                if ((microtime(true) - $start_time) > 8.0) {
                    break;
                }

                $url = trim($match[1]);
                $anchor = wp_strip_all_tags($match[2]);

                if (empty($url) || strpos($url, '#') === 0 || strpos($url, 'mailto:') === 0 || strpos($url, 'tel:') === 0 || strpos($url, 'javascript:') === 0) {
                    continue;
                }

                $url_host = parse_url($url, PHP_URL_HOST);

                // 1. COMPROBACIÓN NATIVA DE ENLACES INTERNOS
                if (empty($url_host) || $url_host === $site_host) {
                    $target_post_id = url_to_postid($url);
                    if (!$target_post_id && !ier_is_internal_url_valid($url)) {
                        $status_key = '404 Interno';
                        if (ier_is_status_allowed($status_key, $allowed_statuses)) {
                            $broken_links[] = [
                                'post_id'       => $post->ID,
                                'post_title'    => $title,
                                'post_edit_url' => get_edit_post_link($post->ID, ''),
                                'post_view_url' => $post_view_url,
                                'anchor'        => !empty($anchor) ? $anchor : '<em>[Sin texto / Imagen]</em>',
                                'url'           => $url,
                                'status'        => $status_key
                            ];
                        }
                    }
                    continue;
                }

                // 2. EXCLUSIÓN DE DOMINIOS GIGANTES (Protección de IP y reducción de falsos positivos)
                $excluded_domains = [
                    'google.com', 'google.es', 'googleapis.com',
                    'youtube.com', 'youtu.be',
                    'twitter.com', 'x.com',
                    'facebook.com', 'fb.com', 'instagram.com', 'linkedin.com',
                    'archive.org', 'web.archive.org',
                    'pinterest.com', 'tiktok.com', 
                    'amazon.com', 'amazon.es', 'amzn.to'
                ];
                
                $is_excluded = false;
                foreach ($excluded_domains as $ex_domain) {
                    if ($url_host === $ex_domain || substr($url_host, -strlen('.' . $ex_domain)) === '.' . $ex_domain) {
                        $is_excluded = true;
                        break;
                    }
                }

                if ($is_excluded) {
                    continue; // Saltamos este enlace silenciosamente sin hacer petición HTTP
                }

                // 3. AJUSTES DINÁMICOS PARA BLUESKY (Archive ya está excluido arriba)
                $timeout_limit = 2; 
                if (strpos($url_host, 'bsky.app') !== false) {
                    $timeout_limit = 7; 
                }

                // 4. COMPROBACIÓN RÁPIDA VÍA HEAD
                $args = [
                    'timeout'     => $timeout_limit,
                    'redirection' => 2,
                    'sslverify'   => false,
                    'user-agent'  => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
                    'headers'     => [
                        'Accept'          => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
                        'Accept-Language' => 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3',
                    ]
                ];

                $response = wp_remote_head($url, $args);
                $status_code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response);

                if (is_wp_error($response) || $status_code === 405 || $status_code === 403) {
                    $response = wp_remote_get($url, $args);
                    $status_code = is_wp_error($response) ? 'Timeout / Error Red' : wp_remote_retrieve_response_code($response);
                }

                if (intval($status_code) === 498) {
                    continue;
                }
                
                // Excepción específica para falsos positivos de Bluesky (bloqueo antibots 403)
                if (strpos($url_host, 'bsky.app') !== false && in_array($status_code, [403, 'Timeout / Error Red'], true)) {
                    continue;
                }

                if (is_wp_error($response) || (is_numeric($status_code) && $status_code >= 400) || $status_code === 'Timeout / Error Red') {
                    $status_str = (string) $status_code;
                    
                    if (ier_is_status_allowed($status_str, $allowed_statuses)) {
                        $broken_links[] = [
                            'post_id'       => $post->ID,
                            'post_title'    => $title,
                            'post_edit_url' => get_edit_post_link($post->ID, ''),
                            'post_view_url' => $post_view_url,
                            'anchor'        => !empty($anchor) ? $anchor : '<em>[Sin texto / Imagen]</em>',
                            'url'           => $url,
                            'status'        => $status_str
                        ];
                    }
                }
            }
        }
    }

    wp_send_json_success([
        'post_id'      => $post->ID,
        'post_title'   => $title,
        'broken_links' => $broken_links,
        'finished'     => false
    ]);
}

// 5. Servidor: Reemplazar URL directamente en el contenido del post
function ier_replace_link_callback() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Sin permisos');
    }

    global $wpdb;
    $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
    
    $old_url_raw = isset($_POST['old_url']) ? wp_unslash(trim($_POST['old_url'])) : '';
    $new_url_raw = isset($_POST['new_url']) ? wp_unslash(trim($_POST['new_url'])) : '';

    if (!$post_id || empty($old_url_raw) || empty($new_url_raw)) {
        wp_send_json_error('Parámetros incompletos.');
    }

    $content = $wpdb->get_var($wpdb->prepare("SELECT post_content FROM {$wpdb->posts} WHERE ID = %d", $post_id));

    if (!$content) {
        wp_send_json_error('No se encontró el contenido de la entrada.');
    }

    $old_url_decoded = html_entity_decode($old_url_raw, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $old_url_entities = htmlentities($old_url_decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $old_url_amp = str_replace('&', '&amp;', $old_url_decoded);
    $old_url_sanitized = esc_url_raw($old_url_raw);

    $target_old_url = false;

    if (strpos($content, $old_url_raw) !== false) {
        $target_old_url = $old_url_raw;
    } elseif (strpos($content, $old_url_decoded) !== false) {
        $target_old_url = $old_url_decoded;
    } elseif (strpos($content, $old_url_amp) !== false) {
        $target_old_url = $old_url_amp;
    } elseif (strpos($content, $old_url_entities) !== false) {
        $target_old_url = $old_url_entities;
    } elseif (strpos($content, $old_url_sanitized) !== false) {
        $target_old_url = $old_url_sanitized;
    }

    if ($target_old_url !== false) {
        $new_url = esc_url_raw($new_url_raw);
        $updated_content = str_replace($target_old_url, $new_url, $content);
        
        $result = $wpdb->update(
            $wpdb->posts,
            ['post_content' => $updated_content],
            ['ID' => $post_id],
            ['%s'],
            ['%d']
        );

        if ($result !== false) {
            clean_post_cache($post_id);
            wp_send_json_success();
        } else {
            wp_send_json_error('No se pudo actualizar la base de datos.');
        }
    } else {
        wp_send_json_error('La URL original no coincide exactamente con el texto en BD.');
    }
}

// 6. Servidor: Eliminar enlace conservando el texto ancla (Unlink)
function ier_unlink_link_callback() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Sin permisos');
    }

    global $wpdb;
    $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
    $url_raw = isset($_POST['url']) ? wp_unslash(trim($_POST['url'])) : '';

    if (!$post_id || empty($url_raw)) {
        wp_send_json_error('Parámetros incompletos.');
    }

    $content = $wpdb->get_var($wpdb->prepare("SELECT post_content FROM {$wpdb->posts} WHERE ID = %d", $post_id));

    if (!$content) {
        wp_send_json_error('No se encontró la entrada.');
    }

    $url_decoded = html_entity_decode($url_raw, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $url_sanitized = esc_url_raw($url_raw);
    
    $urls_to_check = array_unique([$url_raw, $url_decoded, $url_sanitized]);
    $replaced = false;
    $updated_content = $content;

    foreach ($urls_to_check as $target_url) {
        $quoted_url = preg_quote($target_url, '/');
        $pattern = '/<a\s+[^>]*href=["\']' . $quoted_url . '["\'][^>]*>(.*?)<\/a>/is';

        if (preg_match($pattern, $updated_content)) {
            $updated_content = preg_replace($pattern, '$1', $updated_content);
            $replaced = true;
            break;
        }
    }

    if ($replaced) {
        $result = $wpdb->update(
            $wpdb->posts,
            ['post_content' => $updated_content],
            ['ID' => $post_id],
            ['%s'],
            ['%d']
        );

        if ($result !== false) {
            clean_post_cache($post_id);
            wp_send_json_success();
        } else {
            wp_send_json_error('No se pudo actualizar la base de datos.');
        }
    } else {
        wp_send_json_error('No se pudo encontrar el enlace exacto en el contenido del post.');
    }
}

// Funciones auxiliares
function ier_clean_raw_url($url) {
    $url = wp_unslash(trim($url));
    if (empty($url)) return '';
    return esc_url_raw($url);
}

function ier_is_status_allowed($status, $allowed_statuses) {
    if (empty($allowed_statuses)) return true;
    
    $status_str = (string) $status;
    
    if (in_array($status_str, $allowed_statuses, true)) {
        return true;
    }
    
    if (in_array('Otros / Desconocido', $allowed_statuses, true)) {
        $known_statuses = ['404', '404 Interno', 'Timeout / Error Red', '403', '400', '500', '502', '503', '520'];
        if (!in_array($status_str, $known_statuses, true)) {
            return true;
        }
    }
    
    return false;
}

function ier_is_internal_url_valid($url) {
    $path = parse_url($url, PHP_URL_PATH);
    if (empty($path) || $path === '/') return true;

    $site_host = parse_url(site_url(), PHP_URL_HOST);
    $url_host  = parse_url($url, PHP_URL_HOST);
    
    if (empty($url_host) || $url_host === $site_host) {
        $local_path = ABSPATH . ltrim($path, '/');
        if (file_exists($local_path) || is_dir($local_path)) {
            return true;
        }
    }

    $clean_path = ltrim($path, '/');
    $page_or_post = get_page_by_path($clean_path, OBJECT, ['post', 'page', 'attachment']);
    if (!$page_or_post && (str_ends_with($clean_path, '.htm') || str_ends_with($clean_path, '.html'))) {
        $path_without_ext = preg_replace('/\.html?$/i', '', $clean_path);
        $page_or_post = get_page_by_path($path_without_ext, OBJECT, ['post', 'page', 'attachment']);
    }
    if ($page_or_post) return true;

    $term_path = trim($path, '/');
    $segments  = explode('/', $term_path);
    if (count($segments) >= 2) {
        $taxonomy_slug = $segments[0];
        $term_slug     = end($segments);
        
        $taxonomies = [
            'tag'       => 'post_tag',
            'category'  => 'category',
            'etiqueta'  => 'post_tag',
            'categoria' => 'category'
        ];

        $target_tax = isset($taxonomies[$taxonomy_slug]) ? $taxonomies[$taxonomy_slug] : $taxonomy_slug;

        if (taxonomy_exists($target_tax) && term_exists($term_slug, $target_tax)) {
            return true;
        }
    }

    $query_vars = [];
    if (class_exists('WP')) {
        $wp = new WP();
        $query_vars = $wp->parse_request($url);
    }
    
    if (!empty($query_vars)) {
        return true;
    }

    $upload_dir = wp_upload_dir();
    $base_url   = parse_url($upload_dir['baseurl'], PHP_URL_PATH);
    if ($base_url && strpos($path, $base_url) === 0) {
        $relative_file = str_replace($base_url, '', $path);
        $file_path     = $upload_dir['basedir'] . $relative_file;
        if (file_exists($file_path)) return true;
    }

    return false;
}

Deixe um comentário

Tem algo a dizer?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting