*/ public function scan(): array { $html = $this->fetch_home(); $third = array(); $forms = 0; $inline = 0; $blocked = 0; $error = ''; if ( is_wp_error( $html ) ) { // Guardo el motivo: sin él, un diagnóstico que no pudo leer el sitio // informaría igual y parecería haber mirado algo. $error = (string) $html->get_error_message(); $html = ''; } if ( is_string( $html ) && '' !== $html ) { $third = Script_Blocker::detect( $html ); $forms = (int) preg_match_all( '#]*\bsrc=)#i', $html ); $blocked = (int) preg_match_all( '#' . CUMPLECONSENT_CAT_ATTR . '#i', $html ); } $data = array( 'ts' => current_time( 'mysql', true ), 'url' => home_url( '/' ), 'fetched' => is_string( $html ) && '' !== $html, 'fetch_error' => $error, 'third_parties' => $third, 'forms' => $forms, 'inline_scripts' => $inline, 'blocked_markers' => $blocked, 'cache_plugin' => $this->cache_plugin(), 'conflict' => $this->other_cmp(), 'cron_ok' => (bool) wp_next_scheduled( CUMPLECONSENT_CRON_PURGE ), ); Settings::save( array( 'diag_last' => (string) $data['ts'], 'diag_data' => $data, ) ); return $data; } /** Descarga el HTML de la portada. @return string|\WP_Error */ private function fetch_home() { $response = wp_remote_get( add_query_arg( 'cumpleconsent_diag', time(), home_url( '/' ) ), array( 'timeout' => 12, 'redirection' => 3, 'user-agent' => 'CumpleConsent/' . CUMPLECONSENT_VERSION . '; ' . home_url( '/' ), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); if ( $code < 200 || $code >= 400 ) { return new \WP_Error( 'cumpleconsent_http', sprintf( 'HTTP %d', $code ) ); } return (string) wp_remote_retrieve_body( $response ); } /** Detecta un plugin de caché activo (para recordar la purga). */ private function cache_plugin(): string { $candidates = array( 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', 'wp-super-cache/wp-cache.php' => 'WP Super Cache', 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', 'nginx-helper/nginx-helper.php' => 'Nginx Helper', 'cache-enabler/cache-enabler.php' => 'Cache Enabler', ); $plugins = (array) get_option( 'active_plugins', array() ); foreach ( $candidates as $file => $name ) { if ( in_array( $file, $plugins, true ) ) { return $name; } } return ''; } /** Detecta otro gestor de consentimiento activo (posible conflicto). */ private function other_cmp(): string { $candidates = array( 'cookie-law-info/cookie-law-info.php' => 'CookieYes', 'complianz-gdpr/complianz-gpdr.php' => 'Complianz', 'cookie-notice/cookie-notice.php' => 'Cookie Notice', 'borlabs-cookie/borlabs-cookie.php' => 'Borlabs', 'cookiebot/cookiebot.php' => 'Cookiebot', 'iubenda-cookie-law-solution/iubenda_cookie_law_solution.php' => 'Iubenda', 'chile-consiente/chile-consiente.php' => 'Chile Consiente', ); $plugins = (array) get_option( 'active_plugins', array() ); foreach ( $candidates as $file => $name ) { if ( in_array( $file, $plugins, true ) ) { return $name; } } return ''; } /** * Hallazgos priorizados. * * @param array|null $data Resultado de scan() (o el guardado). * @return array */ public function findings( $data = null ): array { if ( null === $data ) { $data = Settings::get( 'diag_data', array() ); } if ( ! is_array( $data ) || ! $data ) { return array(); } // Un diagnóstico guardado por una versión anterior (o una ejecución que // falló a medias) puede no traer todas las claves: se normaliza antes de leer. $data = array_merge( array( 'ts' => '', 'url' => home_url( '/' ), 'fetched' => false, 'fetch_error' => '', 'third_parties' => array(), 'forms' => 0, 'inline_scripts' => 0, 'blocked_markers' => 0, 'cache_plugin' => '', 'conflict' => '', 'cron_ok' => (bool) wp_next_scheduled( CUMPLECONSENT_CRON_PURGE ), ), $data ); $s = Settings::all(); $out = array(); $policy = Policy::score(); $add = static function ( string $severity, string $title, string $detail, string $article, string $action = '', string $url = '' ) use ( &$out ) { $out[] = array( 'severity' => $severity, 'title' => $title, 'detail' => $detail, 'article' => $article, 'action' => $action, 'url' => $url, ); }; // Si no se pudo leer la portada, hay que decirlo primero: el resto del // informe se apoya en ese HTML, y sin esta línea el diagnóstico parecería // haber mirado el sitio cuando en realidad no pudo descargarlo. if ( empty( $data['fetched'] ) ) { $add( 'atencion', __( 'No se pudo leer la portada del sitio', 'cumpleconsent' ), sprintf( /* translators: 1: dirección del sitio, 2: motivo técnico del fallo. */ __( 'El diagnóstico descarga la portada para ver qué cargan los terceros y si el bloqueo actúa. El servidor no pudo leer %1$s (%2$s). Suele ser el cortafuegos del hosting o un bloqueo de las peticiones que salen del propio servidor; mientras tanto, este informe no puede decir qué hay en la página.', 'cumpleconsent' ), home_url( '/' ), '' !== (string) $data['fetch_error'] ? (string) $data['fetch_error'] : __( 'sin detalle', 'cumpleconsent' ) ), __( 'Diagnóstico', 'cumpleconsent' ), __( 'Permite al servidor pedirse su propia portada y vuelve a ejecutar el diagnóstico.', 'cumpleconsent' ) ); } // Aviso operativo. if ( empty( $s['enabled'] ) ) { $add( 'critico', __( 'El aviso de privacidad no está activo', 'cumpleconsent' ), __( 'Sin aviso no hay consentimiento informado ni prueba: los terceros que cargan cookies lo hacen sin base de licitud.', 'cumpleconsent' ), 'art. 12 y 14 quáter', __( 'Completa la identidad del responsable y activa el aviso.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-ajustes' ) ); } if ( '' === trim( (string) $s['owner_name'] ) || '' === (string) $s['owner_email'] ) { $add( 'critico', __( 'Falta identificar al responsable y su canal de contacto', 'cumpleconsent' ), __( 'El aviso y la política deben individualizar al responsable del tratamiento y ofrecer un medio para notificarle solicitudes (correo, formulario o domicilio).', 'cumpleconsent' ), 'art. 14 ter b) y c)', __( 'Agrega razón social, RUT y correo de contacto.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-ajustes' ) ); } // Política. if ( '' === Policy::url() ) { $add( 'critico', __( 'No hay política de privacidad publicada ni enlazada', 'cumpleconsent' ), __( 'La ley obliga a mantener permanentemente a disposición del público la política con doce contenidos mínimos.', 'cumpleconsent' ), 'art. 14 ter', __( 'Genera la política con el botón de la pestaña Documentos y enlázala.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-documentos' ) ); } elseif ( $policy['score'] < 100 ) { $add( $policy['score'] >= 75 ? 'atencion' : 'critico', sprintf( /* translators: %d: porcentaje */ __( 'La política cubre el %d%% de los contenidos del art. 14 ter', 'cumpleconsent' ), $policy['score'] ), sprintf( /* translators: %d: literales faltantes */ __( 'Hay %d contenidos sin señales detectables. El verificador busca contenidos, no evalúa suficiencia jurídica: revísalos igual.', 'cumpleconsent' ), $policy['missing'] + $policy['partial'] ), 'art. 14 ter a)–l)', __( 'Abre la pestaña Documentos y revisa el detalle por literal.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-documentos' ) ); } // Patrones oscuros. if ( 'secondary' === (string) $s['reject_style'] ) { $add( 'atencion', __( '“Rechazar” está visualmente subordinado a “Aceptar”', 'cumpleconsent' ), __( 'El consentimiento debe ser libre. Un rechazo escondido o de menor peso es un patrón oscuro y debilita la prueba.', 'cumpleconsent' ), 'art. 12', __( 'Deja “rechazar” con el mismo peso visual.', 'cumpleconsent' ) ); } if ( ! empty( $s['cookie_wall'] ) ) { $add( 'critico', __( 'El “muro de cookies” está activo: el consentimiento se presume no libre', 'cumpleconsent' ), __( 'Condicionar el acceso al sitio a aceptar cookies permite presumir que el consentimiento no fue otorgado libremente; la ley admite esa presunción y la prueba queda en tu contra.', 'cumpleconsent' ), 'art. 12 inciso 6', __( 'Desactiva el bloqueo de contenido y ofrece el acceso sin aceptar.', 'cumpleconsent' ) ); } // Registro. if ( empty( $s['log_enabled'] ) ) { $add( 'critico', __( 'El registro de consentimiento está desactivado', 'cumpleconsent' ), __( 'Corresponde al responsable probar que contó con el consentimiento. Sin registro, la prueba depende de la memoria.', 'cumpleconsent' ), 'art. 12 inciso 7', __( 'Activa el registro y define el plazo de conservación.', 'cumpleconsent' ) ); } if ( empty( $data['cron_ok'] ) ) { $add( 'atencion', __( 'La tarea de purga automática no está programada', 'cumpleconsent' ), __( 'Si el plazo declarado en la política no se ejecuta, el plazo es una promesa sin respaldo. Desactiva y vuelve a activar el plugin para reprogramar la tarea.', 'cumpleconsent' ), 'art. 14 ter i)', __( 'Revisa las tareas programadas del sitio.', 'cumpleconsent' ), admin_url( 'site-health.php' ) ); } // Derechos. if ( empty( $s['rights_enabled'] ) ) { $add( 'atencion', __( 'No hay canal habilitado para ejercer los derechos', 'cumpleconsent' ), __( 'La ley exige informar cómo ejercer los derechos y dar un medio para notificar las solicitudes; los plazos de 30 días corridos y 2 días hábiles corren igual.', 'cumpleconsent' ), 'art. 11 y 14 ter f)', __( 'Activa el formulario de derechos y publica su página.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-ajustes' ) ); } // Terceros y transferencias. if ( ! empty( $data['third_parties'] ) ) { $names = array(); foreach ( array_slice( (array) $data['third_parties'], 0, 8, true ) as $host => $info ) { $names[] = $host; } $add( 'ok', __( 'Terceros detectados en la portada', 'cumpleconsent' ), implode( ', ', $names ), 'art. 14 ter d) y h)', __( 'Verifica que cada uno esté declarado en la política y en la tabla de transferencias.', 'cumpleconsent' ) ); } $undeclared = Transfers::undeclared(); if ( $undeclared ) { $list = array(); foreach ( $undeclared as $row ) { $list[] = $row['host']; } $add( 'atencion', __( 'Hay terceros que tratan datos fuera de Chile sin declarar', 'cumpleconsent' ), implode( ', ', array_slice( $list, 0, 8 ) ), 'art. 14 ter h) y art. 27', __( 'Agrega estos proveedores a la tabla de transferencias con su país y su garantía.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-ajustes' ) ); } if ( Transfers::has_unknown() ) { $add( 'critico', __( 'Hay transferencias sin garantía declarada', 'cumpleconsent' ), __( 'Una transferencia sin nivel adecuado ni garantías solo es lícita en los casos específicos de la ley. No declares en la política algo que no tienes suscrito.', 'cumpleconsent' ), 'art. 27', __( 'Corrige la garantía o elimina la fila hasta tenerla.', 'cumpleconsent' ) ); } // Bloqueo efectivo. Solo aplica a la estrategia estricta, que es la que deja // marcas en el HTML: en equilibrado el sitio puede delegar en Google Consent // Mode y no hay marcas por diseño, así que avisar de eso sería un falso positivo. $estrategia_estricta = 'balanced' !== (string) Settings::get( 'load_mode', 'strict' ); if ( $estrategia_estricta && ! empty( $data['third_parties'] ) && 0 === (int) $data['blocked_markers'] ) { $add( 'critico', __( 'Los terceros detectados no aparecen bloqueados en el HTML servido', 'cumpleconsent' ), __( 'Con la estrategia estricta, cada script de tercero queda inerte con su marca de finalidad. Si no hay ninguna marca, el bloqueo no está actuando (otro CMP, caché muy agresiva o inyección posterior por JavaScript).', 'cumpleconsent' ), 'art. 14 quáter', __( 'Revisa la estrategia de carga y los conflictos con otros plugins.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-ajustes' ) ); } if ( ! empty( $data['conflict'] ) ) { $add( 'atencion', sprintf( /* translators: %s: nombre del otro gestor */ __( 'Hay otro gestor de consentimiento activo: %s', 'cumpleconsent' ), (string) $data['conflict'] ), __( 'Dos avisos compitiendo confunden al visitante y rompen la prueba del consentimiento: quedan dos registros distintos de la misma decisión.', 'cumpleconsent' ), '—', __( 'Deja activo un solo gestor y desactiva el otro.', 'cumpleconsent' ) ); } // Formularios y consentimiento en formularios. if ( (int) $data['forms'] > 0 ) { $add( 'ok', sprintf( /* translators: %d: cantidad de formularios */ _n( '%d formulario detectado en la portada', '%d formularios detectados en la portada', (int) $data['forms'], 'cumpleconsent' ), (int) $data['forms'] ), __( 'Los formularios también recogen datos personales: la base de licitud, la finalidad y el plazo deben estar informados antes de enviar, con casilla desmarcada.', 'cumpleconsent' ), 'art. 12 y 14 ter', __( 'Usa la casilla de consentimiento del plugin o declara la finalidad en el formulario.', 'cumpleconsent' ) ); } // Registro de actividades. if ( empty( $s['ops'] ) ) { $add( 'atencion', __( 'El registro de actividades de tratamiento no está revisado', 'cumpleconsent' ), __( 'El reglamento exige caracterizar las operaciones de tratamiento; el plugin puede generarlo desde la configuración del sitio y tú debes revisarlo y ajustarlo.', 'cumpleconsent' ), 'reglamento art. 3 d)', __( 'Revisa y guarda el registro en la pestaña Documentos.', 'cumpleconsent' ), admin_url( 'admin.php?page=cumpleconsent-documentos' ) ); } // HTTPS. if ( ! is_ssl() ) { $add( 'atencion', __( 'El sitio no está sirviendo por HTTPS', 'cumpleconsent' ), __( 'Las medidas de seguridad deben ser adecuadas al riesgo; transmitir datos personales sin cifrado no lo es.', 'cumpleconsent' ), 'art. 14 quinquies', __( 'Activa un certificado y redirige todo el tráfico a HTTPS.', 'cumpleconsent' ) ); } // Caché. if ( '' !== (string) $data['cache_plugin'] ) { $add( 'ok', sprintf( /* translators: %s: plugin de caché */ __( 'Caché de página activa: %s', 'cumpleconsent' ), (string) $data['cache_plugin'] ), __( 'El bloqueo no depende de la cookie del servidor, así que la caché no expone rastreadores. Solo recuerda purgar la caché cuando cambies el aviso o subas su versión.', 'cumpleconsent' ), '—', '' ); } return $out; } /** Resumen de severidades. @return array */ public function summary( array $findings ): array { $out = array( 'critico' => 0, 'atencion' => 0, 'ok' => 0, ); foreach ( $findings as $row ) { $sev = (string) $row['severity']; if ( isset( $out[ $sev ] ) ) { ++$out[ $sev ]; } } return $out; } }