feat(visibility): sistema de visibilidad por tipo de página
- Añadir PageVisibility use case y repositorio - Implementar PageTypeDetector para detectar home/single/page/archive - Actualizar FieldMappers con soporte show_on_[page_type] - Extender FormBuilders con UI de visibilidad por página - Refactorizar Renderers para evaluar visibilidad dinámica - Limpiar schemas removiendo campos de visibilidad legacy - Añadir MigrationCommand para migrar configuraciones existentes - Implementar adsense-loader.js para carga lazy de ads - Actualizar front-page.php con nueva estructura - Extender DIContainer con nuevos servicios 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||
|
||||
/**
|
||||
* WP-CLI Command para Sincronización de Schemas
|
||||
*
|
||||
@@ -297,6 +299,298 @@ final class MigrationCommand
|
||||
'stats' => $stats
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migra configuración de visibilidad para todos los componentes
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp roi-theme migrate-visibility
|
||||
*
|
||||
* @when after_wp_load
|
||||
*/
|
||||
public function migrate_visibility(): void
|
||||
{
|
||||
$container = DIContainer::getInstance();
|
||||
$service = $container->getMigratePageVisibilityService();
|
||||
|
||||
$result = $service->migrate();
|
||||
|
||||
\WP_CLI::success(sprintf(
|
||||
'Migración completada: %d creados, %d omitidos',
|
||||
$result['created'],
|
||||
$result['skipped']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcodes que DEBEN ser preservados
|
||||
*/
|
||||
private const PROTECTED_SHORTCODES = ['[roi_apu_search', '[roi_'];
|
||||
|
||||
/**
|
||||
* Máximo porcentaje de contenido que puede eliminarse
|
||||
*/
|
||||
private const MAX_CONTENT_LOSS_PERCENT = 50;
|
||||
|
||||
/**
|
||||
* Limpia contenido Thrive congelado de páginas (H2 y paginación)
|
||||
*
|
||||
* LIMPIEZA QUIRÚRGICA CON VALIDACIONES DE SEGURIDAD:
|
||||
* - Elimina H2 con data-shortcode="tcb_post_title"
|
||||
* - Elimina paginación rota ([tcb_pagination_current_page], [tcb_pagination_total_pages])
|
||||
* - PRESERVA todo el demás contenido incluyendo shortcodes [roi_apu_search]
|
||||
* - Verifica que shortcodes importantes NO sean eliminados
|
||||
* - Aborta si se detecta pérdida excesiva de contenido (>50%)
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--dry-run]
|
||||
* : Mostrar qué se limpiaría sin modificar nada (OBLIGATORIO primero)
|
||||
*
|
||||
* [--force]
|
||||
* : Ejecutar la limpieza real después de verificar dry-run
|
||||
*
|
||||
* [--include-others]
|
||||
* : Incluir otras páginas afectadas (Blog, Curso)
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Ver qué se limpiaría (modo seguro) - SIEMPRE PRIMERO
|
||||
* wp roi-theme clean_thrive --dry-run
|
||||
*
|
||||
* # Ejecutar limpieza real (requiere --force)
|
||||
* wp roi-theme clean_thrive --force
|
||||
*
|
||||
* @when after_wp_load
|
||||
*/
|
||||
public function clean_thrive(array $args, array $assoc_args): void
|
||||
{
|
||||
$affectedPageIds = [
|
||||
107264, 107312, 107340, 107345, 107351, 107357, 107362,
|
||||
107369, 107374, 107379, 107384, 107389, 107395, 107399,
|
||||
107403, 107407, 107411, 107416, 107421, 107425, 185752
|
||||
];
|
||||
$otherAffectedIds = [252030, 290709];
|
||||
|
||||
$dryRun = isset($assoc_args['dry-run']);
|
||||
$includeOthers = isset($assoc_args['include-others']);
|
||||
$force = isset($assoc_args['force']);
|
||||
|
||||
$pageIds = $affectedPageIds;
|
||||
if ($includeOthers) {
|
||||
$pageIds = array_merge($pageIds, $otherAffectedIds);
|
||||
}
|
||||
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('╔══════════════════════════════════════════════════════════════════╗');
|
||||
\WP_CLI::line('║ LIMPIEZA QUIRÚRGICA DE CONTENIDO THRIVE CONGELADO (v2.0) ║');
|
||||
\WP_CLI::line('║ Con validaciones de seguridad para proteger shortcodes ║');
|
||||
\WP_CLI::line('╚══════════════════════════════════════════════════════════════════╝');
|
||||
\WP_CLI::line('');
|
||||
|
||||
if ($dryRun) {
|
||||
\WP_CLI::warning('MODO DRY-RUN: No se modificará ningún contenido');
|
||||
} else {
|
||||
\WP_CLI::error('MODO REAL DESHABILITADO: Ejecuta primero con --dry-run', false);
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('Para ejecutar la limpieza real, primero revisa el dry-run:');
|
||||
\WP_CLI::line(' wp roi-theme clean_thrive --dry-run');
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('Si el dry-run es correcto y deseas ejecutar:');
|
||||
\WP_CLI::line(' wp roi-theme clean_thrive --force');
|
||||
|
||||
if (!$force) {
|
||||
return;
|
||||
}
|
||||
\WP_CLI::warning('MODO REAL CON --force: Se modificará el contenido');
|
||||
}
|
||||
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('Páginas a procesar: ' . count($pageIds));
|
||||
\WP_CLI::line('Shortcodes protegidos: ' . implode(', ', self::PROTECTED_SHORTCODES));
|
||||
\WP_CLI::line('Máxima pérdida permitida: ' . self::MAX_CONTENT_LOSS_PERCENT . '%');
|
||||
\WP_CLI::line('');
|
||||
|
||||
$totalH2Removed = 0;
|
||||
$totalPaginationRemoved = 0;
|
||||
$totalBytesFreed = 0;
|
||||
$pagesModified = 0;
|
||||
$pagesSkipped = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($pageIds as $id) {
|
||||
$page = get_post($id);
|
||||
if (!$page) {
|
||||
\WP_CLI::warning("Página {$id} no encontrada, saltando...");
|
||||
continue;
|
||||
}
|
||||
|
||||
$originalContent = $page->post_content;
|
||||
$originalSize = strlen($originalContent);
|
||||
|
||||
$hasThrive = strpos($originalContent, 'tcb_post_title') !== false ||
|
||||
strpos($originalContent, 'tcb_pagination') !== false;
|
||||
|
||||
if (!$hasThrive) {
|
||||
\WP_CLI::line(sprintf("[SIN THRIVE] ID %d: %s", $id, mb_substr($page->post_title, 0, 50)));
|
||||
continue;
|
||||
}
|
||||
|
||||
$h2Count = preg_match_all('/<h2[^>]*>\s*<span[^>]*data-shortcode="tcb_post_title"[^>]*>.*?<\/span>\s*<\/h2>/s', $originalContent);
|
||||
$protectedBefore = $this->countProtectedShortcodes($originalContent);
|
||||
$cleanResult = $this->cleanThriveContentSafely($originalContent);
|
||||
|
||||
if ($cleanResult['error']) {
|
||||
$errors[] = "ID {$id}: {$cleanResult['error']}";
|
||||
\WP_CLI::error(sprintf("[ERROR] ID %d: %s - %s", $id, mb_substr($page->post_title, 0, 40), $cleanResult['error']), false);
|
||||
$pagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedContent = $cleanResult['content'];
|
||||
$newSize = strlen($cleanedContent);
|
||||
$protectedAfter = $this->countProtectedShortcodes($cleanedContent);
|
||||
|
||||
if ($protectedAfter < $protectedBefore) {
|
||||
$errors[] = "ID {$id}: Se perderían shortcodes protegidos ({$protectedBefore} → {$protectedAfter})";
|
||||
\WP_CLI::error(sprintf("[ABORTADO] ID %d: Se perderían shortcodes protegidos (%d → %d)", $id, $protectedBefore, $protectedAfter), false);
|
||||
$pagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$lossPercent = $originalSize > 0 ? (($originalSize - $newSize) / $originalSize) * 100 : 0;
|
||||
if ($lossPercent > self::MAX_CONTENT_LOSS_PERCENT) {
|
||||
$errors[] = "ID {$id}: Pérdida excesiva de contenido ({$lossPercent}%)";
|
||||
\WP_CLI::error(sprintf("[ABORTADO] ID %d: Pérdida excesiva %.1f%% (máx %d%%)", $id, $lossPercent, self::MAX_CONTENT_LOSS_PERCENT), false);
|
||||
$pagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasChanges = $originalContent !== $cleanedContent;
|
||||
$bytesSaved = $originalSize - $newSize;
|
||||
$paginationRemoved = (strpos($originalContent, 'tcb_pagination_current_page') !== false && strpos($cleanedContent, 'tcb_pagination_current_page') === false) ? 1 : 0;
|
||||
|
||||
if ($hasChanges) {
|
||||
$pagesModified++;
|
||||
$totalH2Removed += $h2Count;
|
||||
$totalPaginationRemoved += $paginationRemoved;
|
||||
$totalBytesFreed += $bytesSaved;
|
||||
|
||||
$status = $dryRun ? '[DRY-RUN]' : '[LIMPIADO]';
|
||||
\WP_CLI::line(sprintf("%s ID %d: %s", $status, $id, mb_substr($page->post_title, 0, 50) . (mb_strlen($page->post_title) > 50 ? '...' : '')));
|
||||
\WP_CLI::line(sprintf(" → H2 eliminados: %d | Paginación: %s | Pérdida: %.1f%%", $h2Count, $paginationRemoved ? 'Sí' : 'No', $lossPercent));
|
||||
\WP_CLI::line(sprintf(" → Shortcodes [roi_*] preservados: %d | Bytes liberados: %s", $protectedAfter, $this->formatBytes($bytesSaved)));
|
||||
|
||||
if (!$dryRun && $force) {
|
||||
wp_update_post(['ID' => $id, 'post_content' => $cleanedContent]);
|
||||
}
|
||||
} else {
|
||||
\WP_CLI::line(sprintf("[SIN CAMBIOS] ID %d: %s", $id, mb_substr($page->post_title, 0, 50)));
|
||||
}
|
||||
}
|
||||
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::line('════════════════════════════════════════════════════════════════════');
|
||||
\WP_CLI::line('RESUMEN:');
|
||||
\WP_CLI::line(sprintf(' Páginas modificadas: %d', $pagesModified));
|
||||
\WP_CLI::line(sprintf(' Páginas omitidas: %d', $pagesSkipped));
|
||||
\WP_CLI::line(sprintf(' Total H2 eliminados: %d', $totalH2Removed));
|
||||
\WP_CLI::line(sprintf(' Paginaciones removidas: %d', $totalPaginationRemoved));
|
||||
\WP_CLI::line(sprintf(' Espacio liberado: %s', $this->formatBytes($totalBytesFreed)));
|
||||
\WP_CLI::line('════════════════════════════════════════════════════════════════════');
|
||||
|
||||
if (count($errors) > 0) {
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::warning('ERRORES ENCONTRADOS:');
|
||||
foreach ($errors as $error) {
|
||||
\WP_CLI::line(" - {$error}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun && $pagesModified > 0 && count($errors) === 0) {
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::success('Dry-run completado SIN errores.');
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::warning('Para ejecutar la limpieza real:');
|
||||
\WP_CLI::line(' wp roi-theme clean_thrive --force');
|
||||
} elseif (!$dryRun && $force && $pagesModified > 0) {
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::success('Limpieza completada exitosamente.');
|
||||
\WP_CLI::line('');
|
||||
\WP_CLI::warning('IMPORTANTE: Purga el caché del sitio para ver los cambios.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia el contenido con validaciones de seguridad
|
||||
* @return array{content: string, error: string|null}
|
||||
*/
|
||||
private function cleanThriveContentSafely(string $content): array
|
||||
{
|
||||
$originalContent = $content;
|
||||
|
||||
// Patrón específico: H2 que contiene span con data-shortcode="tcb_post_title"
|
||||
// Estructura: <h2><span data-shortcode="tcb_post_title"...>...</span></h2>
|
||||
$result = preg_replace('/<h2[^>]*>\s*<span[^>]*data-shortcode="tcb_post_title"[^>]*>.*?<\/span>\s*<\/h2>/s', '', $content);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón H2'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
$result = preg_replace('/<p[^>]*>.*?\[tcb_pagination_current_page\].*?\[tcb_pagination_total_pages\].*?<\/p>/s', '', $content);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón paginación'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
$result = preg_replace('/<p[^>]*data-button_layout="[^"]*"[^>]*data-page="[^"]*"[^>]*>.*?<\/p>/s', '', $content);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón botones'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
$content = str_replace('[tcb_pagination_current_page]', '', $content);
|
||||
$content = str_replace('[tcb_pagination_total_pages]', '', $content);
|
||||
|
||||
$result = preg_replace('/(\r?\n){3,}/', "\n\n", $content);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en limpieza líneas'];
|
||||
}
|
||||
$content = trim($result);
|
||||
|
||||
if (empty($content) && !empty($originalContent)) {
|
||||
return ['content' => $originalContent, 'error' => 'El contenido quedó vacío'];
|
||||
}
|
||||
|
||||
return ['content' => $content, 'error' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuenta shortcodes protegidos en el contenido
|
||||
*/
|
||||
private function countProtectedShortcodes(string $content): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach (self::PROTECTED_SHORTCODES as $shortcode) {
|
||||
$count += substr_count($content, $shortcode);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea bytes a formato legible
|
||||
*/
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . ' B';
|
||||
} elseif ($bytes < 1048576) {
|
||||
return round($bytes / 1024, 1) . ' KB';
|
||||
} else {
|
||||
return round($bytes / 1048576, 2) . ' MB';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar comando WP-CLI
|
||||
|
||||
352
Shared/Infrastructure/CLI/CleanThriveContentCommand.php
Normal file
352
Shared/Infrastructure/CLI/CleanThriveContentCommand.php
Normal file
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\CLI;
|
||||
|
||||
use WP_CLI;
|
||||
|
||||
/**
|
||||
* Comando WP-CLI para limpiar contenido Thrive congelado de páginas
|
||||
*
|
||||
* LIMPIEZA QUIRÚRGICA CON VALIDACIONES DE SEGURIDAD:
|
||||
* - Elimina H2 con data-shortcode="tcb_post_title"
|
||||
* - Elimina paginación rota ([tcb_pagination_current_page], [tcb_pagination_total_pages])
|
||||
* - PRESERVA todo el demás contenido incluyendo shortcodes [roi_apu_search]
|
||||
* - Verifica que shortcodes importantes NO sean eliminados
|
||||
* - Aborta si se detecta pérdida excesiva de contenido (>50%)
|
||||
*
|
||||
* USO:
|
||||
* wp roi-theme clean_thrive --dry-run # Ver qué se limpiaría (OBLIGATORIO primero)
|
||||
* wp roi-theme clean_thrive # Ejecutar limpieza real
|
||||
*
|
||||
* SEGURIDAD:
|
||||
* - Verifica preservación de shortcodes [roi_apu_search]
|
||||
* - Máximo 50% de reducción de contenido permitida
|
||||
* - Valida cada preg_replace para evitar null returns
|
||||
*/
|
||||
final class CleanThriveContentCommand
|
||||
{
|
||||
/**
|
||||
* IDs de páginas buscar-apus afectadas
|
||||
*/
|
||||
private const AFFECTED_PAGE_IDS = [
|
||||
107264, 107312, 107340, 107345, 107351, 107357, 107362,
|
||||
107369, 107374, 107379, 107384, 107389, 107395, 107399,
|
||||
107403, 107407, 107411, 107416, 107421, 107425, 185752
|
||||
];
|
||||
|
||||
/**
|
||||
* Otras páginas con contenido Thrive (Blog, Curso)
|
||||
*/
|
||||
private const OTHER_AFFECTED_IDS = [252030, 290709];
|
||||
|
||||
/**
|
||||
* Shortcodes que DEBEN ser preservados
|
||||
*/
|
||||
private const PROTECTED_SHORTCODES = [
|
||||
'[roi_apu_search',
|
||||
'[roi_',
|
||||
];
|
||||
|
||||
/**
|
||||
* Máximo porcentaje de contenido que puede eliminarse
|
||||
*/
|
||||
private const MAX_CONTENT_LOSS_PERCENT = 50;
|
||||
|
||||
public function __invoke(array $args, array $assoc_args): void
|
||||
{
|
||||
$dryRun = isset($assoc_args['dry-run']);
|
||||
$includeOthers = isset($assoc_args['include-others']);
|
||||
$force = isset($assoc_args['force']);
|
||||
|
||||
$pageIds = self::AFFECTED_PAGE_IDS;
|
||||
if ($includeOthers) {
|
||||
$pageIds = array_merge($pageIds, self::OTHER_AFFECTED_IDS);
|
||||
}
|
||||
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log('╔══════════════════════════════════════════════════════════════════╗');
|
||||
WP_CLI::log('║ LIMPIEZA QUIRÚRGICA DE CONTENIDO THRIVE CONGELADO (v2.0) ║');
|
||||
WP_CLI::log('║ Con validaciones de seguridad para proteger shortcodes ║');
|
||||
WP_CLI::log('╚══════════════════════════════════════════════════════════════════╝');
|
||||
WP_CLI::log('');
|
||||
|
||||
if ($dryRun) {
|
||||
WP_CLI::warning('MODO DRY-RUN: No se modificará ningún contenido');
|
||||
} else {
|
||||
WP_CLI::error('MODO REAL DESHABILITADO: Ejecuta primero con --dry-run', false);
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log('Para ejecutar la limpieza real, primero revisa el dry-run:');
|
||||
WP_CLI::log(' wp roi-theme clean_thrive --dry-run');
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log('Si el dry-run es correcto y deseas ejecutar:');
|
||||
WP_CLI::log(' wp roi-theme clean_thrive --force');
|
||||
|
||||
if (!$force) {
|
||||
return;
|
||||
}
|
||||
WP_CLI::warning('MODO REAL CON --force: Se modificará el contenido');
|
||||
}
|
||||
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log('Páginas a procesar: ' . count($pageIds));
|
||||
WP_CLI::log('Shortcodes protegidos: ' . implode(', ', self::PROTECTED_SHORTCODES));
|
||||
WP_CLI::log('Máxima pérdida permitida: ' . self::MAX_CONTENT_LOSS_PERCENT . '%');
|
||||
WP_CLI::log('');
|
||||
|
||||
$totalH2Removed = 0;
|
||||
$totalPaginationRemoved = 0;
|
||||
$totalBytesFreed = 0;
|
||||
$pagesModified = 0;
|
||||
$pagesSkipped = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($pageIds as $id) {
|
||||
$page = get_post($id);
|
||||
if (!$page) {
|
||||
WP_CLI::warning("Página {$id} no encontrada, saltando...");
|
||||
continue;
|
||||
}
|
||||
|
||||
$originalContent = $page->post_content;
|
||||
$originalSize = strlen($originalContent);
|
||||
|
||||
// Verificar si tiene contenido Thrive que limpiar
|
||||
$hasThrive = strpos($originalContent, 'tcb_post_title') !== false ||
|
||||
strpos($originalContent, 'tcb_pagination') !== false;
|
||||
|
||||
if (!$hasThrive) {
|
||||
WP_CLI::log(sprintf(
|
||||
"[SIN THRIVE] ID %d: %s",
|
||||
$id,
|
||||
mb_substr($page->post_title, 0, 50)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Contar elementos antes de limpiar
|
||||
$h2Count = preg_match_all('/<h2[^>]*>.*?data-shortcode="tcb_post_title".*?<\/h2>/s', $originalContent);
|
||||
|
||||
// Contar shortcodes protegidos antes
|
||||
$protectedBefore = $this->countProtectedShortcodes($originalContent);
|
||||
|
||||
// Limpiar contenido con validación
|
||||
$cleanResult = $this->cleanContentSafely($originalContent);
|
||||
|
||||
if ($cleanResult['error']) {
|
||||
$errors[] = "ID {$id}: {$cleanResult['error']}";
|
||||
WP_CLI::error(sprintf(
|
||||
"[ERROR] ID %d: %s - %s",
|
||||
$id,
|
||||
mb_substr($page->post_title, 0, 40),
|
||||
$cleanResult['error']
|
||||
), false);
|
||||
$pagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedContent = $cleanResult['content'];
|
||||
$newSize = strlen($cleanedContent);
|
||||
|
||||
// Contar shortcodes protegidos después
|
||||
$protectedAfter = $this->countProtectedShortcodes($cleanedContent);
|
||||
|
||||
// VALIDACIÓN CRÍTICA: Verificar shortcodes protegidos
|
||||
if ($protectedAfter < $protectedBefore) {
|
||||
$errors[] = "ID {$id}: Se perderían shortcodes protegidos ({$protectedBefore} → {$protectedAfter})";
|
||||
WP_CLI::error(sprintf(
|
||||
"[ABORTADO] ID %d: Se perderían shortcodes protegidos (%d → %d)",
|
||||
$id,
|
||||
$protectedBefore,
|
||||
$protectedAfter
|
||||
), false);
|
||||
$pagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar pérdida excesiva de contenido
|
||||
$lossPercent = $originalSize > 0 ? (($originalSize - $newSize) / $originalSize) * 100 : 0;
|
||||
if ($lossPercent > self::MAX_CONTENT_LOSS_PERCENT) {
|
||||
$errors[] = "ID {$id}: Pérdida excesiva de contenido ({$lossPercent}%)";
|
||||
WP_CLI::error(sprintf(
|
||||
"[ABORTADO] ID %d: Pérdida excesiva %.1f%% (máx %d%%)",
|
||||
$id,
|
||||
$lossPercent,
|
||||
self::MAX_CONTENT_LOSS_PERCENT
|
||||
), false);
|
||||
$pagesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar si hubo cambios
|
||||
$hasChanges = $originalContent !== $cleanedContent;
|
||||
$bytesSaved = $originalSize - $newSize;
|
||||
|
||||
// Contar paginación removida
|
||||
$paginationRemoved = (
|
||||
strpos($originalContent, 'tcb_pagination_current_page') !== false &&
|
||||
strpos($cleanedContent, 'tcb_pagination_current_page') === false
|
||||
) ? 1 : 0;
|
||||
|
||||
if ($hasChanges) {
|
||||
$pagesModified++;
|
||||
$totalH2Removed += $h2Count;
|
||||
$totalPaginationRemoved += $paginationRemoved;
|
||||
$totalBytesFreed += $bytesSaved;
|
||||
|
||||
$status = $dryRun ? '[DRY-RUN]' : '[LIMPIADO]';
|
||||
WP_CLI::log(sprintf(
|
||||
"%s ID %d: %s",
|
||||
$status,
|
||||
$id,
|
||||
mb_substr($page->post_title, 0, 50) . (mb_strlen($page->post_title) > 50 ? '...' : '')
|
||||
));
|
||||
WP_CLI::log(sprintf(
|
||||
" → H2 eliminados: %d | Paginación: %s | Pérdida: %.1f%%",
|
||||
$h2Count,
|
||||
$paginationRemoved ? 'Sí' : 'No',
|
||||
$lossPercent
|
||||
));
|
||||
WP_CLI::log(sprintf(
|
||||
" → Shortcodes [roi_*] preservados: %d | Bytes liberados: %s",
|
||||
$protectedAfter,
|
||||
$this->formatBytes($bytesSaved)
|
||||
));
|
||||
|
||||
if (!$dryRun && $force) {
|
||||
wp_update_post([
|
||||
'ID' => $id,
|
||||
'post_content' => $cleanedContent
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
WP_CLI::log(sprintf(
|
||||
"[SIN CAMBIOS] ID %d: %s",
|
||||
$id,
|
||||
mb_substr($page->post_title, 0, 50)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log('════════════════════════════════════════════════════════════════════');
|
||||
WP_CLI::log('RESUMEN:');
|
||||
WP_CLI::log(sprintf(' Páginas modificadas: %d', $pagesModified));
|
||||
WP_CLI::log(sprintf(' Páginas omitidas: %d', $pagesSkipped));
|
||||
WP_CLI::log(sprintf(' Total H2 eliminados: %d', $totalH2Removed));
|
||||
WP_CLI::log(sprintf(' Paginaciones removidas: %d', $totalPaginationRemoved));
|
||||
WP_CLI::log(sprintf(' Espacio liberado: %s', $this->formatBytes($totalBytesFreed)));
|
||||
WP_CLI::log('════════════════════════════════════════════════════════════════════');
|
||||
|
||||
if (count($errors) > 0) {
|
||||
WP_CLI::log('');
|
||||
WP_CLI::warning('ERRORES ENCONTRADOS:');
|
||||
foreach ($errors as $error) {
|
||||
WP_CLI::log(" - {$error}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun && $pagesModified > 0 && count($errors) === 0) {
|
||||
WP_CLI::log('');
|
||||
WP_CLI::success('Dry-run completado SIN errores.');
|
||||
WP_CLI::log('');
|
||||
WP_CLI::warning('Para ejecutar la limpieza real:');
|
||||
WP_CLI::log(' wp roi-theme clean_thrive --force');
|
||||
} elseif (!$dryRun && $force && $pagesModified > 0) {
|
||||
WP_CLI::log('');
|
||||
WP_CLI::success('Limpieza completada exitosamente.');
|
||||
WP_CLI::log('');
|
||||
WP_CLI::warning('IMPORTANTE: Purga el caché del sitio para ver los cambios.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia el contenido con validaciones de seguridad
|
||||
*
|
||||
* @return array{content: string, error: string|null}
|
||||
*/
|
||||
private function cleanContentSafely(string $content): array
|
||||
{
|
||||
$originalContent = $content;
|
||||
|
||||
// 1. Eliminar H2 con data-shortcode="tcb_post_title"
|
||||
$result = preg_replace(
|
||||
'/<h2[^>]*>.*?data-shortcode="tcb_post_title".*?<\/h2>/s',
|
||||
'',
|
||||
$content
|
||||
);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón H2'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
// 2. Eliminar paginación Thrive rota
|
||||
$result = preg_replace(
|
||||
'/<p[^>]*>.*?\[tcb_pagination_current_page\].*?\[tcb_pagination_total_pages\].*?<\/p>/s',
|
||||
'',
|
||||
$content
|
||||
);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón paginación'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
// 3. Eliminar botones de paginación Thrive
|
||||
$result = preg_replace(
|
||||
'/<p[^>]*data-button_layout="[^"]*"[^>]*data-page="[^"]*"[^>]*>.*?<\/p>/s',
|
||||
'',
|
||||
$content
|
||||
);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en patrón botones'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
// 4. Eliminar shortcodes Thrive huérfanos
|
||||
$content = str_replace('[tcb_pagination_current_page]', '', $content);
|
||||
$content = str_replace('[tcb_pagination_total_pages]', '', $content);
|
||||
|
||||
// 5. Limpiar múltiples líneas vacías (con validación)
|
||||
$result = preg_replace('/(\r?\n){3,}/', "\n\n", $content);
|
||||
if ($result === null) {
|
||||
return ['content' => $originalContent, 'error' => 'preg_replace falló en limpieza líneas'];
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
// 6. Trim
|
||||
$content = trim($content);
|
||||
|
||||
// Validación final: no retornar vacío si original tenía contenido
|
||||
if (empty($content) && !empty($originalContent)) {
|
||||
return ['content' => $originalContent, 'error' => 'El contenido quedó vacío'];
|
||||
}
|
||||
|
||||
return ['content' => $content, 'error' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuenta shortcodes protegidos en el contenido
|
||||
*/
|
||||
private function countProtectedShortcodes(string $content): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach (self::PROTECTED_SHORTCODES as $shortcode) {
|
||||
$count += substr_count($content, $shortcode);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea bytes a formato legible
|
||||
*/
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . ' B';
|
||||
} elseif ($bytes < 1048576) {
|
||||
return round($bytes / 1024, 1) . ' KB';
|
||||
} else {
|
||||
return round($bytes / 1048576, 2) . ' MB';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@ use ROITheme\Shared\Infrastructure\Services\CriticalCSSCollector;
|
||||
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer;
|
||||
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
|
||||
use ROITheme\Shared\Infrastructure\Services\WordPressPageTypeDetector;
|
||||
use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressPageVisibilityRepository;
|
||||
use ROITheme\Shared\Application\UseCases\EvaluatePageVisibility\EvaluatePageVisibilityUseCase;
|
||||
use ROITheme\Shared\Infrastructure\Services\MigratePageVisibilityService;
|
||||
|
||||
/**
|
||||
* DIContainer - Contenedor de Inyección de Dependencias
|
||||
@@ -46,10 +52,38 @@ final class DIContainer
|
||||
{
|
||||
private array $instances = [];
|
||||
|
||||
/**
|
||||
* Instancia singleton del contenedor
|
||||
* @var self|null
|
||||
*/
|
||||
private static ?self $instance = null;
|
||||
|
||||
/**
|
||||
* Obtiene la instancia singleton del contenedor
|
||||
*
|
||||
* NOTA: Se debe haber creado una instancia previamente en functions.php
|
||||
* El constructor registra automáticamente la instancia.
|
||||
*
|
||||
* @return self
|
||||
* @throws \RuntimeException Si no se ha inicializado el contenedor
|
||||
*/
|
||||
public static function getInstance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
throw new \RuntimeException(
|
||||
'DIContainer no ha sido inicializado. Asegúrate de que functions.php se haya ejecutado primero.'
|
||||
);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
private \wpdb $wpdb,
|
||||
private string $schemasPath
|
||||
) {}
|
||||
) {
|
||||
// Registrar como instancia singleton
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener repositorio de componentes
|
||||
@@ -272,4 +306,61 @@ final class DIContainer
|
||||
|
||||
return $this->instances['criticalCSSCollector'];
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Page Visibility System
|
||||
// ===============================
|
||||
|
||||
/**
|
||||
* Obtiene el repositorio de visibilidad de página
|
||||
*
|
||||
* IMPORTANTE: Inyecta $wpdb para consistencia con el resto del código
|
||||
* (WordPressComponentSettingsRepository también recibe $wpdb por constructor)
|
||||
*/
|
||||
public function getPageVisibilityRepository(): PageVisibilityRepositoryInterface
|
||||
{
|
||||
if (!isset($this->instances['pageVisibilityRepository'])) {
|
||||
// Inyectar $wpdb siguiendo el patrón existente
|
||||
$this->instances['pageVisibilityRepository'] = new WordPressPageVisibilityRepository($this->wpdb);
|
||||
}
|
||||
return $this->instances['pageVisibilityRepository'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el detector de tipo de página
|
||||
*/
|
||||
public function getPageTypeDetector(): PageTypeDetectorInterface
|
||||
{
|
||||
if (!isset($this->instances['pageTypeDetector'])) {
|
||||
$this->instances['pageTypeDetector'] = new WordPressPageTypeDetector();
|
||||
}
|
||||
return $this->instances['pageTypeDetector'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el caso de uso de evaluación de visibilidad
|
||||
*/
|
||||
public function getEvaluatePageVisibilityUseCase(): EvaluatePageVisibilityUseCase
|
||||
{
|
||||
if (!isset($this->instances['evaluatePageVisibilityUseCase'])) {
|
||||
$this->instances['evaluatePageVisibilityUseCase'] = new EvaluatePageVisibilityUseCase(
|
||||
$this->getPageTypeDetector(),
|
||||
$this->getPageVisibilityRepository()
|
||||
);
|
||||
}
|
||||
return $this->instances['evaluatePageVisibilityUseCase'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el servicio de migración de visibilidad
|
||||
*/
|
||||
public function getMigratePageVisibilityService(): MigratePageVisibilityService
|
||||
{
|
||||
if (!isset($this->instances['migratePageVisibilityService'])) {
|
||||
$this->instances['migratePageVisibilityService'] = new MigratePageVisibilityService(
|
||||
$this->getPageVisibilityRepository()
|
||||
);
|
||||
}
|
||||
return $this->instances['migratePageVisibilityService'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Implementación WordPress del repositorio de visibilidad
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
|
||||
*/
|
||||
final class WordPressPageVisibilityRepository implements PageVisibilityRepositoryInterface
|
||||
{
|
||||
private const GROUP_NAME = '_page_visibility';
|
||||
private const TABLE_SUFFIX = 'roi_theme_component_settings';
|
||||
|
||||
private const VISIBILITY_FIELDS = [
|
||||
'show_on_home',
|
||||
'show_on_posts',
|
||||
'show_on_pages',
|
||||
'show_on_archives',
|
||||
'show_on_search',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor con inyección de dependencias
|
||||
*
|
||||
* IMPORTANTE: Sigue el patrón existente de WordPressComponentSettingsRepository
|
||||
* donde $wpdb se inyecta por constructor, no se usa global.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly \wpdb $wpdb
|
||||
) {}
|
||||
|
||||
public function getVisibilityConfig(string $componentName): array
|
||||
{
|
||||
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||
|
||||
$results = $this->wpdb->get_results(
|
||||
$this->wpdb->prepare(
|
||||
"SELECT attribute_name, attribute_value
|
||||
FROM {$table}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s",
|
||||
$componentName,
|
||||
self::GROUP_NAME
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if (empty($results)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$config = [];
|
||||
foreach ($results as $row) {
|
||||
$config[$row['attribute_name']] = $row['attribute_value'] === '1';
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function saveVisibilityConfig(string $componentName, array $config): void
|
||||
{
|
||||
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||
|
||||
foreach ($config as $field => $enabled) {
|
||||
if (!in_array($field, self::VISIBILITY_FIELDS, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $this->wpdb->get_var($this->wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s
|
||||
AND attribute_name = %s",
|
||||
$componentName,
|
||||
self::GROUP_NAME,
|
||||
$field
|
||||
));
|
||||
|
||||
$value = $enabled ? '1' : '0';
|
||||
|
||||
if ($exists) {
|
||||
$this->wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'attribute_value' => $value,
|
||||
'updated_at' => current_time('mysql'),
|
||||
],
|
||||
[
|
||||
'component_name' => $componentName,
|
||||
'group_name' => self::GROUP_NAME,
|
||||
'attribute_name' => $field,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->wpdb->insert($table, [
|
||||
'component_name' => $componentName,
|
||||
'group_name' => self::GROUP_NAME,
|
||||
'attribute_name' => $field,
|
||||
'attribute_value' => $value,
|
||||
'is_editable' => 1,
|
||||
'created_at' => current_time('mysql'),
|
||||
'updated_at' => current_time('mysql'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function hasVisibilityConfig(string $componentName): bool
|
||||
{
|
||||
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||
|
||||
$count = $this->wpdb->get_var($this->wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s",
|
||||
$componentName,
|
||||
self::GROUP_NAME
|
||||
));
|
||||
|
||||
return (int) $count > 0;
|
||||
}
|
||||
|
||||
public function getAllComponentNames(): array
|
||||
{
|
||||
$table = $this->wpdb->prefix . self::TABLE_SUFFIX;
|
||||
|
||||
$results = $this->wpdb->get_col(
|
||||
"SELECT DISTINCT component_name FROM {$table} ORDER BY component_name"
|
||||
);
|
||||
|
||||
return $results ?: [];
|
||||
}
|
||||
|
||||
public function createDefaultVisibility(string $componentName, array $defaults): void
|
||||
{
|
||||
if ($this->hasVisibilityConfig($componentName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveVisibilityConfig($componentName, $defaults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\PageVisibilityRepositoryInterface;
|
||||
use ROITheme\Shared\Domain\Constants\VisibilityDefaults;
|
||||
|
||||
/**
|
||||
* Servicio para migrar configuración de visibilidad inicial
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class MigratePageVisibilityService
|
||||
{
|
||||
// NOTA: Usa VisibilityDefaults::DEFAULT_VISIBILITY para cumplir DRY
|
||||
|
||||
public function __construct(
|
||||
private readonly PageVisibilityRepositoryInterface $visibilityRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la migración para todos los componentes
|
||||
*
|
||||
* @return array{created: int, skipped: int}
|
||||
*/
|
||||
public function migrate(): array
|
||||
{
|
||||
$created = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$components = $this->visibilityRepository->getAllComponentNames();
|
||||
|
||||
foreach ($components as $componentName) {
|
||||
if ($this->visibilityRepository->hasVisibilityConfig($componentName)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Usar constante compartida (DRY)
|
||||
$this->visibilityRepository->createDefaultVisibility(
|
||||
$componentName,
|
||||
VisibilityDefaults::DEFAULT_VISIBILITY
|
||||
);
|
||||
$created++;
|
||||
}
|
||||
|
||||
return [
|
||||
'created' => $created,
|
||||
'skipped' => $skipped,
|
||||
];
|
||||
}
|
||||
}
|
||||
39
Shared/Infrastructure/Services/PageVisibilityHelper.php
Normal file
39
Shared/Infrastructure/Services/PageVisibilityHelper.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||
|
||||
/**
|
||||
* Facade/Helper para evaluar visibilidad de componentes
|
||||
*
|
||||
* PROPÓSITO:
|
||||
* Permite que los Renderers existentes evalúen visibilidad sin modificar sus constructores.
|
||||
* Actúa como un Service Locator limitado a este único propósito.
|
||||
*
|
||||
* USO EN RENDERERS:
|
||||
* ```php
|
||||
* if (!PageVisibilityHelper::shouldShow('cta-box-sidebar')) {
|
||||
* return '';
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class PageVisibilityHelper
|
||||
{
|
||||
/**
|
||||
* Evalúa si un componente debe mostrarse en la página actual
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @return bool True si debe mostrarse
|
||||
*/
|
||||
public static function shouldShow(string $componentName): bool
|
||||
{
|
||||
$container = DIContainer::getInstance();
|
||||
$useCase = $container->getEvaluatePageVisibilityUseCase();
|
||||
|
||||
return $useCase->execute($componentName);
|
||||
}
|
||||
}
|
||||
65
Shared/Infrastructure/Services/WordPressPageTypeDetector.php
Normal file
65
Shared/Infrastructure/Services/WordPressPageTypeDetector.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\PageTypeDetectorInterface;
|
||||
use ROITheme\Shared\Domain\ValueObjects\PageType;
|
||||
|
||||
/**
|
||||
* Implementación WordPress del detector de tipo de página
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class WordPressPageTypeDetector implements PageTypeDetectorInterface
|
||||
{
|
||||
public function detect(): PageType
|
||||
{
|
||||
if ($this->isHome()) {
|
||||
return PageType::home();
|
||||
}
|
||||
|
||||
if ($this->isPost()) {
|
||||
return PageType::post();
|
||||
}
|
||||
|
||||
if ($this->isPage()) {
|
||||
return PageType::page();
|
||||
}
|
||||
|
||||
if ($this->isSearch()) {
|
||||
return PageType::search();
|
||||
}
|
||||
|
||||
if ($this->isArchive()) {
|
||||
return PageType::archive();
|
||||
}
|
||||
|
||||
return PageType::fromString(PageType::UNKNOWN);
|
||||
}
|
||||
|
||||
public function isHome(): bool
|
||||
{
|
||||
return is_front_page();
|
||||
}
|
||||
|
||||
public function isPost(): bool
|
||||
{
|
||||
return is_single() && !is_front_page();
|
||||
}
|
||||
|
||||
public function isPage(): bool
|
||||
{
|
||||
return is_page() && !is_front_page();
|
||||
}
|
||||
|
||||
public function isArchive(): bool
|
||||
{
|
||||
return is_archive();
|
||||
}
|
||||
|
||||
public function isSearch(): bool
|
||||
{
|
||||
return is_search();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user