%s

', esc_attr( (string) $m['tipo'] ), esc_html( (string) $m['texto'] ) ); } } /** * Textos de resultado según lo que dejó cada acción en la dirección. * * Método puro, para poder probarlo sin navegador. * * @param array $q Parámetros de la petición (normalmente $_GET). * @return array */ public static function action_messages( array $q ): array { $out = array(); if ( isset( $q['scanned'] ) ) { $out[] = array( 'tipo' => 'notice-success', 'texto' => __( 'Diagnóstico ejecutado. Tienes el resultado en la tarjeta “Diagnóstico del sitio”.', 'cumpleconsent' ), ); } if ( isset( $q['mail'] ) ) { $destino = isset( $q['mail_to'] ) ? sanitize_text_field( (string) $q['mail_to'] ) : ''; $motivo = isset( $q['mail_err'] ) ? sanitize_text_field( (string) $q['mail_err'] ) : ''; if ( '1' === (string) $q['mail'] ) { $out[] = array( 'tipo' => 'notice-success', /* translators: %s: correo de destino. */ 'texto' => sprintf( __( 'Correo de prueba entregado al servidor para %s. Si no llega en unos minutos, revisa la carpeta de correo no deseado del buzón del canal.', 'cumpleconsent' ), $destino ), ); } else { $out[] = array( 'tipo' => 'notice-error', 'texto' => '' !== $motivo /* translators: %s: motivo informado por el servidor de correo. */ ? sprintf( __( 'El servidor rechazó el correo de prueba: %s', 'cumpleconsent' ), $motivo ) : __( 'El servidor rechazó el correo de prueba. Revisa la configuración de correo del sitio.', 'cumpleconsent' ), ); } } if ( isset( $q['purged'] ) ) { $n = absint( $q['purged'] ); $out[] = array( 'tipo' => 'notice-success', /* translators: %d: cantidad de entradas eliminadas del registro. */ 'texto' => sprintf( _n( 'Registro depurado: %d entrada eliminada.', 'Registro depurado: %d entradas eliminadas.', $n, 'cumpleconsent' ), $n ), ); } if ( isset( $q['policy'] ) ) { $id = absint( $q['policy'] ); $mode = isset( $q['policy_mode'] ) ? sanitize_key( (string) $q['policy_mode'] ) : ''; if ( $id > 0 && 'borrador' === $mode ) { $out[] = array( 'tipo' => 'notice-warning', 'texto' => sprintf( /* translators: %d: identificador de la página que quedó como borrador. */ __( 'La página con tu política ya tenía contenido propio, así que no la toqué: el texto generado quedó como borrador aparte (página #%d). Compara los dos y decide cuál publicas.', 'cumpleconsent' ), $id ), ); } elseif ( $id > 0 ) { $out[] = array( 'tipo' => 'notice-success', 'texto' => ! empty( $q['published'] ) /* translators: %d: identificador de la página que contiene la política. */ ? sprintf( __( 'Política generada y publicada (página #%d). Revisa el texto en el sitio.', 'cumpleconsent' ), $id ) /* translators: %d: identificador de la página que contiene la política. */ /* translators: %d: identificador de la página que contiene la política. */ : sprintf( __( 'Política generada o actualizada (página #%d). Revísala antes de publicarla.', 'cumpleconsent' ), $id ), ); } else { $out[] = array( 'tipo' => 'notice-error', 'texto' => __( 'No se pudo generar la política: revisa que tu usuario pueda crear páginas y vuelve a intentarlo.', 'cumpleconsent' ), ); } } if ( isset( $q['responded'] ) ) { $out[] = array( 'tipo' => ! empty( $q['responded'] ) ? 'notice-success' : 'notice-error', 'texto' => ! empty( $q['responded'] ) ? __( 'Respuesta guardada en la solicitud.', 'cumpleconsent' ) : __( 'No se pudo guardar la respuesta: escribe el texto antes de enviarla.', 'cumpleconsent' ), ); } if ( isset( $q['extended'] ) ) { $out[] = array( 'tipo' => 'notice-success', 'texto' => __( 'Se amplió el plazo de la solicitud con la prórroga del artículo 11.', 'cumpleconsent' ), ); } return $out; } /** * Al guardar ajustes: avisa en el panel si se subió la versión del aviso. * * La purga de cachés no está aquí: la hace Settings::after_update, que cubre * por igual el formulario del panel y la escritura por API. Y no se exige que * el guardado venga del formulario, para que también funcione por API. * * @param mixed $old Valor anterior. * @param mixed $new Valor nuevo. */ public function on_settings_saved( $old, $new ): void { if ( ! is_array( $new ) ) { return; } if ( ! empty( $new['policy_version'] ) && ( empty( $old['policy_version'] ) || $old['policy_version'] !== $new['policy_version'] ) ) { set_transient( 'cumpleconsent_version_bumped', 1, 60 ); } } public function assets( $hook ): void { if ( false === strpos( (string) $hook, 'cumpleconsent' ) ) { return; } wp_enqueue_style( 'cumpleconsent-admin', CUMPLECONSENT_URL . 'assets/css/admin.css', array(), CUMPLECONSENT_VERSION ); } public function menu(): void { add_menu_page( __( 'CumpleConsent', 'cumpleconsent' ), __( 'CumpleConsent', 'cumpleconsent' ), CUMPLECONSENT_CAP, 'cumpleconsent', array( $this, 'screen_panel' ), 'dashicons-shield-alt', 58 ); add_submenu_page( 'cumpleconsent', __( 'Panel', 'cumpleconsent' ), __( 'Panel', 'cumpleconsent' ), CUMPLECONSENT_CAP, 'cumpleconsent', array( $this, 'screen_panel' ) ); add_submenu_page( 'cumpleconsent', __( 'Ajustes', 'cumpleconsent' ), __( 'Ajustes', 'cumpleconsent' ), CUMPLECONSENT_CAP, 'cumpleconsent-ajustes', array( $this, 'screen_settings' ) ); add_submenu_page( 'cumpleconsent', __( 'Solicitudes de derechos', 'cumpleconsent' ), __( 'Solicitudes', 'cumpleconsent' ), CUMPLECONSENT_CAP, 'cumpleconsent-derechos', array( $this, 'screen_rights' ) ); add_submenu_page( 'cumpleconsent', __( 'Registro de consentimiento', 'cumpleconsent' ), __( 'Registro', 'cumpleconsent' ), CUMPLECONSENT_CAP, 'cumpleconsent-registro', array( $this, 'screen_log' ) ); add_submenu_page( 'cumpleconsent', __( 'Documentos y verificación', 'cumpleconsent' ), __( 'Documentos', 'cumpleconsent' ), CUMPLECONSENT_CAP, 'cumpleconsent-documentos', array( $this, 'screen_documents' ) ); } // ── Utilidades de pantalla ─────────────────────────────────────────────── private function admin_url( string $page, array $args = array() ): string { return add_query_arg( array_merge( array( 'page' => $page ), $args ), admin_url( 'admin.php' ) ); } private function action_url( string $do, array $args = array() ): string { return wp_nonce_url( add_query_arg( array_merge( array( 'action' => self::ACTION, 'do' => $do, ), $args ), admin_url( 'admin-post.php' ) ), self::ACTION ); } private function require_admin(): void { if ( ! current_user_can( CUMPLECONSENT_CAP ) ) { wp_die( esc_html__( 'No tienes permisos para esta acción.', 'cumpleconsent' ) ); } check_admin_referer( self::ACTION ); } private function badge( string $severity ): string { $map = array( 'critico' => array( __( 'Crítico', 'cumpleconsent' ), 'ccn-badge--crit' ), 'atencion' => array( __( 'Atención', 'cumpleconsent' ), 'ccn-badge--warn' ), 'ok' => array( __( 'Correcto', 'cumpleconsent' ), 'ccn-badge--ok' ), ); $b = $map[ $severity ] ?? array( $severity, '' ); return '' . esc_html( $b[0] ) . ''; } /** Menú de pestañas del plugin. */ private function tabs( string $current ): void { $tabs = array( 'cumpleconsent' => __( 'Panel', 'cumpleconsent' ), 'cumpleconsent-ajustes' => __( 'Ajustes', 'cumpleconsent' ), 'cumpleconsent-derechos' => __( 'Solicitudes', 'cumpleconsent' ), 'cumpleconsent-registro' => __( 'Registro', 'cumpleconsent' ), 'cumpleconsent-documentos' => __( 'Documentos', 'cumpleconsent' ), ); echo '

CumpleConsent · ' . esc_html__( 'Ley 21.719 y su reglamento', 'cumpleconsent' ) . '

'; echo ''; } // ── Panel ──────────────────────────────────────────────────────────────── public function screen_panel(): void { $this->require_view(); $plugin = Plugin::instance(); $findings = $plugin->diagnostics->findings(); $summary = $plugin->diagnostics->summary( $findings ); $stats = $plugin->log->stats(); $alerts = $plugin->rights->alerts(); $policy = Policy::score(); $ready = Settings::is_ready(); $this->tabs( 'cumpleconsent' ); echo '
'; if ( ! $ready ) { echo '

' . esc_html__( 'Tres pasos y el sitio queda informando y respetando la decisión del visitante', 'cumpleconsent' ) . '

'; echo '
    '; echo '
  1. ' . esc_html__( 'Completa la identidad del responsable y activa el aviso (Ajustes → Identidad y aviso).', 'cumpleconsent' ) . '
  2. '; echo '
  3. ' . esc_html__( 'Genera la política de privacidad y revisa la verificación del art. 14 ter (Documentos).', 'cumpleconsent' ) . '
  4. '; echo '
  5. ' . esc_html__( 'Declara tus proveedores y habilita el canal de derechos (Ajustes).', 'cumpleconsent' ) . '
  6. '; echo '
'; printf( '

%s

', esc_url( $this->admin_url( 'cumpleconsent-ajustes' ) ), esc_html__( 'Empezar', 'cumpleconsent' ) ); } // Tarjetas de estado. echo '
'; $this->stat_card( __( 'Aviso', 'cumpleconsent' ), $ready ? __( 'Activo', 'cumpleconsent' ) : __( 'Inactivo', 'cumpleconsent' ), $ready ? 'ok' : 'critico', Policy::version() ); $this->stat_card( __( 'Prueba del consentimiento', 'cumpleconsent' ), (string) array_sum( $stats ), 'ok', array_sum( $stats ) > 0 ? __( 'decisiones registradas', 'cumpleconsent' ) : __( 'aún sin decisiones', 'cumpleconsent' ) ); $this->stat_card( __( 'Política (art. 14 ter)', 'cumpleconsent' ), empty( $policy['linked'] ) ? '—' : $policy['score'] . '%', empty( $policy['linked'] ) ? 'critico' : ( $policy['score'] >= 90 ? 'ok' : ( $policy['score'] >= 70 ? 'atencion' : 'critico' ) ), empty( $policy['linked'] ) ? __( 'sin política enlazada', 'cumpleconsent' ) /* translators: %d: cuántos de los doce contenidos del artículo 14 ter se detectaron. */ : sprintf( __( '%d de 12 contenidos completos', 'cumpleconsent' ), $policy['ok'] ) ); $this->stat_card( __( 'Solicitudes de derechos', 'cumpleconsent' ), (string) $plugin->rights->count(), $alerts ? 'atencion' : 'ok', /* translators: %d: cantidad de solicitudes de derechos con plazo próximo o vencido. */ $alerts ? sprintf( __( '%d con plazo próximo o vencido', 'cumpleconsent' ), count( $alerts ) ) : __( 'sin plazos críticos', 'cumpleconsent' ) ); echo '
'; // Plazos. if ( $alerts ) { echo '

' . esc_html__( 'Plazos que corren (art. 11)', 'cumpleconsent' ) . '

'; foreach ( $alerts as $row ) { printf( '', esc_url( $this->admin_url( 'cumpleconsent-derechos', array( 'open' => (int) $row['id'] ) ) ), (int) $row['id'], esc_html( (string) $row['name'] ), esc_html( Rights::types()[ (string) $row['right_type'] ] ?? (string) $row['right_type'] ), esc_html( (string) $row['due_at'] . ' — ' . (string) $row['_due_status']['label'] ), ! empty( $row['_block_status'] ) ? esc_html( ' · ' . __( 'bloqueo', 'cumpleconsent' ) . ': ' . (string) $row['_block_status']['label'] ) : '' ); } echo '
' . esc_html__( 'Solicitud', 'cumpleconsent' ) . '' . esc_html__( 'Derecho', 'cumpleconsent' ) . '' . esc_html__( 'Plazo', 'cumpleconsent' ) . '
#%d · %s%s%s%s
'; } // Diagnóstico. echo '

' . esc_html__( 'Diagnóstico del sitio', 'cumpleconsent' ) . '

'; if ( empty( $findings ) ) { echo '

' . esc_html__( 'Todavía no has ejecutado el diagnóstico. Revisa qué terceros cargan en tu sitio, si el bloqueo está actuando y qué falta declarar.', 'cumpleconsent' ) . '

'; } else { printf( '

%s: %s

', esc_html__( 'Última ejecución (UTC)', 'cumpleconsent' ), esc_html( (string) Settings::get( 'diag_last' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge() y esc_html() ya devuelven HTML escapado. echo '

' . $this->badge( 'critico' ) . ' ' . esc_html( (string) $summary['critico'] ) . ' · ' . $this->badge( 'atencion' ) . ' ' . esc_html( (string) $summary['atencion'] ) . ' · ' . $this->badge( 'ok' ) . ' ' . esc_html( (string) $summary['ok'] ) . '

'; echo ''; foreach ( array_slice( $findings, 0, 12 ) as $row ) { printf( '', $this->badge( (string) $row['severity'] ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped esc_html( (string) $row['title'] ), esc_html( (string) $row['detail'] ), esc_html( (string) $row['article'] ), esc_html( (string) $row['action'] ), '' !== (string) $row['url'] ? ' ' . esc_html__( 'ir', 'cumpleconsent' ) . '' : '' ); } echo '
%s%s
%s
%s
%s%s
'; } printf( '

%s

', esc_url( $this->action_url( 'scan' ) ), empty( $findings ) ? esc_html__( 'Ejecutar diagnóstico', 'cumpleconsent' ) : esc_html__( 'Volver a ejecutar', 'cumpleconsent' ) ); // Ayuda y códigos cortos. echo '

' . esc_html__( 'Códigos cortos disponibles', 'cumpleconsent' ) . '

'; echo ''; $shortcodes = array( '[cumpleconsent_preferencias]' => __( 'Botón o enlace para reabrir las preferencias y revocar el consentimiento. Recomendado en el pie del sitio.', 'cumpleconsent' ), '[cumpleconsent_aviso]' => __( 'Aviso completo: finalidades, derechos y transferencias, en texto público.', 'cumpleconsent' ), '[cumpleconsent_transferencias]' => __( 'Tabla de transferencias internacionales (art. 14 ter h).', 'cumpleconsent' ), '[cumpleconsent_derechos]' => __( 'Formulario de solicitud de derechos con plazos legales.', 'cumpleconsent' ), '[cumpleconsent_casilla finalidad="publicidad" texto="…"]' => __( 'Casilla de consentimiento (desmarcada) para formularios, con trazabilidad del aviso.', 'cumpleconsent' ), ); foreach ( $shortcodes as $code => $help ) { printf( '', esc_html( (string) $code ), esc_html( $help ) ); } echo '
%s%s
'; echo '

' . esc_html__( 'Qué no hace este plugin (para no prometer de más)', 'cumpleconsent' ) . '

'; echo '
    '; echo '
  • ' . esc_html__( 'No constituye un programa de cumplimiento ni designa delegado de protección de datos: aporta elementos documentales para las letras a), d), e), f) y g) del art. 3 del reglamento, referidos al sitio web.', 'cumpleconsent' ) . '
  • '; echo '
  • ' . esc_html__( 'No certifica ni sustituye asesoría jurídica. El verificador del art. 14 ter detecta contenidos presentes o ausentes, no evalúa suficiencia legal.', 'cumpleconsent' ) . '
  • '; echo '
  • ' . esc_html__( 'No verifica la edad de los visitantes ni gestiona el consentimiento de niños, niñas y adolescentes (art. 16 quáter): eso requiere un flujo propio para tu caso de uso.', 'cumpleconsent' ) . '
  • '; echo '
  • ' . esc_html__( 'No cifra la base de datos ni reemplaza un plugin de seguridad.', 'cumpleconsent' ) . '
  • '; echo '
'; echo '
'; } private function require_view(): void { if ( ! current_user_can( CUMPLECONSENT_CAP ) ) { wp_die( esc_html__( 'No tienes permisos para ver esta página.', 'cumpleconsent' ) ); } } private function stat_card( string $label, string $value, string $state, string $note ): void { printf( '
%s%s%s
', esc_attr( $state ), esc_html( $label ), esc_html( $value ), esc_html( $note ) ); } // ── Ajustes ────────────────────────────────────────────────────────────── public function screen_settings(): void { $this->require_view(); $s = Settings::all(); $tab = isset( $_GET['tab'] ) ? sanitize_key( (string) $_GET['tab'] ) : 'identidad'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $prefill = isset( $_GET['prefill'] ) && '1' === sanitize_text_field( wp_unslash( (string) $_GET['prefill'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $transfers = Transfers::rows(); if ( $prefill && ! $transfers ) { $transfers = Transfers::suggestions(); } $ops = Rat::operations(); $this->tabs( 'cumpleconsent-ajustes' ); echo '
'; echo '

' . esc_html__( 'Todos los campos de todas las pestañas se guardan juntos: cambia de pestaña con el enlace de abajo, no con el navegador, para no perder ediciones.', 'cumpleconsent' ) . '

'; echo '
'; settings_fields( 'cumpleconsent_settings_group' ); echo ''; echo ''; // Pestañas internas (solo se muestran u ocultan con el parámetro tab). $sections = array( 'identidad' => __( 'Identidad', 'cumpleconsent' ), 'aviso' => __( 'Aviso', 'cumpleconsent' ), 'finalidades' => __( 'Finalidades', 'cumpleconsent' ), 'bloqueo' => __( 'Bloqueo', 'cumpleconsent' ), 'registro' => __( 'Registro', 'cumpleconsent' ), 'derechos' => __( 'Derechos', 'cumpleconsent' ), 'transferencias' => __( 'Transferencias', 'cumpleconsent' ), 'tratamientos' => __( 'Tratamientos', 'cumpleconsent' ), ); echo ''; foreach ( $sections as $slug => $label ) { printf( '
', $slug === $tab ? ' is-active' : '', esc_attr( $slug ) ); switch ( $slug ) { case 'identidad': $this->fields_identity( $s ); break; case 'aviso': $this->fields_notice( $s ); break; case 'finalidades': $this->fields_purposes( $s ); break; case 'bloqueo': $this->fields_blocking( $s ); break; case 'registro': $this->fields_log( $s ); break; case 'derechos': $this->fields_rights( $s ); break; case 'transferencias': $this->fields_transfers( $transfers ); break; case 'tratamientos': $this->fields_ops( $ops ); break; } echo '
'; } submit_button( __( 'Guardar y purgar cachés', 'cumpleconsent' ) ); echo '
'; } private function text_field( string $key, string $label, string $help = '', string $type = 'text' ): void { $s = Settings::all(); printf( '


%6$s

', esc_attr( $key ), esc_html( $label ), esc_attr( $type ), esc_attr( CUMPLECONSENT_OPT ), esc_attr( (string) ( $s[ $key ] ?? '' ) ), '' !== $help ? '
' . esc_html( $help ) . '' : '' ); } private function textarea_field( string $key, string $label, string $help = '', int $rows = 4 ): void { $s = Settings::all(); printf( '


%6$s

', esc_attr( $key ), esc_html( $label ), esc_attr( CUMPLECONSENT_OPT ), (int) $rows, esc_textarea( (string) ( $s[ $key ] ?? '' ) ), '' !== $help ? '
' . esc_html( $help ) . '' : '' ); } private function checkbox_field( string $key, string $label, string $help = '', bool $warn = false ): void { $s = Settings::all(); printf( '

%6$s

', esc_attr( $key ), esc_attr( CUMPLECONSENT_OPT ), checked( ! empty( $s[ $key ] ), true, false ), esc_html( $label ), $warn ? ' ccn-check--warn' : '', '' !== $help ? '
' . esc_html( $help ) . '' : '' ); } private function select_field( string $key, string $label, array $options, string $help = '' ): void { $s = Settings::all(); printf( '


'; if ( '' !== $help ) { echo '
' . esc_html( $help ) . ''; } echo '

'; } private function fields_identity( array $s ): void { $this->checkbox_field( 'enabled', __( 'Mostrar el aviso de privacidad en el sitio', 'cumpleconsent' ), __( 'Se activa automáticamente cuando guardas con la razón social completa. Mientras esté desactivado, no se bloquea nada ni se registra nada.', 'cumpleconsent' ) ); $this->text_field( 'owner_name', __( 'Razón social o nombre del responsable', 'cumpleconsent' ), __( 'Individualiza al responsable de datos, no a la marca: el nombre comercial no permite saber ante quién se reclama (art. 14 ter b).', 'cumpleconsent' ) ); $this->text_field( 'owner_legal_id', __( 'RUT del responsable', 'cumpleconsent' ) ); $this->text_field( 'owner_rep', __( 'Representante legal', 'cumpleconsent' ) ); $this->text_field( 'owner_rep_id', __( 'RUT del representante legal', 'cumpleconsent' ) ); $this->text_field( 'owner_address', __( 'Domicilio postal', 'cumpleconsent' ) ); $this->text_field( 'owner_email', __( 'Correo para solicitudes de titulares', 'cumpleconsent' ), __( 'El art. 14 ter c) exige un medio para notificar solicitudes. Se publica en el aviso y en la política.', 'cumpleconsent' ), 'email' ); $this->text_field( 'owner_phone', __( 'Teléfono de contacto', 'cumpleconsent' ) ); $this->text_field( 'dpo_name', __( 'Encargado de prevención o delegado (si existe)', 'cumpleconsent' ), __( 'Si no hay delegado designado, deja el campo vacío: la política lo dirá así. El delegado lo designa el responsable (reglamento D.S. 662, arts. 6 a 13).', 'cumpleconsent' ) ); $this->text_field( 'dpo_email', __( 'Correo del encargado de prevención', 'cumpleconsent' ), '', 'email' ); } private function fields_notice( array $s ): void { $this->text_field( 'notice_title', __( 'Título del aviso', 'cumpleconsent' ) ); $this->textarea_field( 'notice_body', __( 'Texto principal del aviso', 'cumpleconsent' ), __( 'Se muestra antes de decidir. Debe decir qué se hace con los datos, para qué y que puedes rechazar.', 'cumpleconsent' ), 5 ); echo ''; $rows = array( 'notice_accept' => __( 'Botón aceptar', 'cumpleconsent' ), 'notice_reject' => __( 'Botón rechazar', 'cumpleconsent' ), 'notice_configure' => __( 'Botón configurar', 'cumpleconsent' ), 'notice_save' => __( 'Botón guardar preferencias', 'cumpleconsent' ), 'notice_reopen' => __( 'Enlace para reabrir preferencias', 'cumpleconsent' ), 'notice_read_more' => __( 'Enlace al aviso completo', 'cumpleconsent' ), 'floating_label' => __( 'Etiqueta del botón flotante', 'cumpleconsent' ), ); foreach ( $rows as $key => $label ) { printf( '', esc_attr( $key ), esc_html( $label ), esc_attr( $key ), esc_attr( CUMPLECONSENT_OPT ), esc_attr( $key ), esc_attr( (string) ( $s[ $key ] ?? '' ) ) ); } echo '
'; $this->select_field( 'position', __( 'Posición del aviso', 'cumpleconsent' ), array( 'bottom' => __( 'Franja inferior', 'cumpleconsent' ), 'bottom-left' => __( 'Tarjeta abajo a la izquierda', 'cumpleconsent' ), 'center' => __( 'Centrado', 'cumpleconsent' ), 'top' => __( 'Franja superior', 'cumpleconsent' ), ) ); $this->select_field( 'theme', __( 'Tema', 'cumpleconsent' ), array( 'auto' => __( 'Automático (según el sistema del visitante)', 'cumpleconsent' ), 'light' => __( 'Claro', 'cumpleconsent' ), 'dark' => __( 'Oscuro', 'cumpleconsent' ), ) ); $this->text_field( 'accent', __( 'Color de acento (hexadecimal)', 'cumpleconsent' ), __( 'Ejemplo: #0b6e4f. Ajusta el contraste para que el texto se lea.', 'cumpleconsent' ) ); $this->select_field( 'reject_style', __( 'Peso visual de “Rechazar”', 'cumpleconsent' ), array( 'equal' => __( 'Igual que “Aceptar” (recomendado)', 'cumpleconsent' ), 'secondary' => __( 'Secundario (no recomendado: patrón oscuro)', 'cumpleconsent' ), ), __( 'El consentimiento debe ser libre. Si subordinas el rechazo, lo advierte el diagnóstico y queda en contra de tu prueba.', 'cumpleconsent' ) ); $this->checkbox_field( 'cookie_wall', __( 'Bloquear el contenido hasta que el visitante decida (muro de cookies)', 'cumpleconsent' ), __( 'La ley presume que el consentimiento no fue otorgado libremente cuando se condiciona el acceso. Actívalo solo si tienes una razón evaluada y documentada.', 'cumpleconsent' ), true ); $this->checkbox_field( 'floating_button', __( 'Mostrar botón flotante de privacidad', 'cumpleconsent' ) ); $this->text_field( 'consent_months', __( 'Meses de vigencia de la decisión', 'cumpleconsent' ), __( 'Cumplido el plazo, el sitio vuelve a preguntar. Doce meses es un valor habitual; el art. 12 no fija plazo, pero el retiro y la información deben estar siempre disponibles.', 'cumpleconsent' ), 'number' ); } private function fields_purposes( array $s ): void { echo '

' . esc_html__( 'El art. 12 exige consentimiento específico en cuanto a finalidades: cada finalidad se explica y se decide por separado. Las “necesarias” no se pueden desactivar porque sin ellas el sitio no funciona.', 'cumpleconsent' ) . '

'; foreach ( Purposes::base() as $key => $purpose ) { if ( ! empty( $purpose['required'] ) ) { printf( '


%s

', esc_html( (string) $purpose['label'] ), esc_html( (string) $purpose['desc'] ) ); continue; } printf( '


%s

', esc_attr( CUMPLECONSENT_OPT ), esc_attr( $key ), checked( in_array( $key, (array) $s['purposes'], true ), true, false ), esc_html( (string) $purpose['label'] ), esc_html( (string) $purpose['desc'] ) ); } $this->textarea_field( 'extra_purposes', __( 'Finalidades propias', 'cumpleconsent' ), __( 'Una por línea, con el formato clave|Etiqueta|Descripción|Categoría wp_consent_api (functional, preferences, statistics o marketing). Ejemplo: newsletter|Boletín|Te enviamos novedades por correo|marketing', 'cumpleconsent' ), 5 ); } private function fields_blocking( array $s ): void { $this->select_field( 'load_mode', __( 'Estrategia de carga antes del consentimiento', 'cumpleconsent' ), array( Script_Blocker::MODE_STRICT => __( 'Estricta: nada se carga hasta que el visitante acepta', 'cumpleconsent' ), Script_Blocker::MODE_BALANCED => __( 'Equilibrada: Google carga sin cookies con Consent Mode v2 y se personaliza al aceptar', 'cumpleconsent' ), ), __( 'En la estrategia equilibrada solo se adelantan los servicios que entienden Consent Mode v2; el resto (Meta Pixel, contenido incrustado, chats) sigue bloqueado hasta el consentimiento.', 'cumpleconsent' ) ); $this->checkbox_field( 'block_iframes', __( 'Bloquear contenido incrustado (videos, mapas, redes) con aviso de carga', 'cumpleconsent' ), __( 'Los iframes de terceros instalan cookies propias y pueden transferir datos. Con esta opción se reemplazan por un aviso y solo cargan si el visitante acepta esa finalidad.', 'cumpleconsent' ) ); $this->checkbox_field( 'block_embedded', __( 'Bloquear además objetos incrustados (<object> y <embed>)', 'cumpleconsent' ) ); $this->checkbox_field( 'honor_consent_server', __( 'Respetar el consentimiento en el servidor (solo sitios sin caché de página completa)', 'cumpleconsent' ), __( 'Con caché de página completa el HTML debe servirse siempre inerte para todos: el navegador de cada visitante decide. Actívalo únicamente si sabes que tu sitio no cachea páginas.', 'cumpleconsent' ) ); } private function fields_log( array $s ): void { $this->checkbox_field( 'log_enabled', __( 'Registrar cada decisión de consentimiento', 'cumpleconsent' ), __( 'Es la prueba que la ley pone en tu cabeza (art. 12). El registro no guarda nombre ni correo.', 'cumpleconsent' ) ); $this->checkbox_field( 'log_ip_hash', __( 'Guardar hash de IP con la sal del sitio', 'cumpleconsent' ), __( 'Nunca se guarda la IP en claro. El hash sirve para acreditar la unicidad del registro sin identificar a la persona.', 'cumpleconsent' ) ); $this->text_field( 'log_retention_months', __( 'Meses de conservación del registro', 'cumpleconsent' ), __( 'El mismo plazo que declares en la política. Una tarea diaria borra lo que exceda este plazo.', 'cumpleconsent' ), 'number' ); printf( '

%s %s

', esc_html__( 'Filas actuales en el registro:', 'cumpleconsent' ), esc_html( (string) Plugin::instance()->log->count() ) ); $this->checkbox_field( 'purge_on_uninstall', __( 'Borrar todos los datos del plugin al desinstalarlo', 'cumpleconsent' ), __( 'Desactivado por defecto: el registro de consentimiento y las solicitudes de derechos son prueba de cumplimiento y conviene conservarlos. Actívalo solo si sabes que ya no los necesitas.', 'cumpleconsent' ), true ); } private function fields_rights( array $s ): void { $this->checkbox_field( 'rights_enabled', __( 'Habilitar el canal de solicitudes de derechos', 'cumpleconsent' ), __( 'El formulario se publica con el código corto [cumpleconsent_derechos]. Al registrarse una solicitud, el sistema calcula los plazos y avisa por correo.', 'cumpleconsent' ) ); $this->text_field( 'rights_email', __( 'Correo que recibe las solicitudes', 'cumpleconsent' ), __( 'Si lo dejas vacío, se usa el correo del administrador del sitio.', 'cumpleconsent' ), 'email' ); printf( '


%s

', esc_html__( 'Página con el formulario de derechos', 'cumpleconsent' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_dropdown_pages() escapa su propia salida. wp_dropdown_pages( array( 'name' => CUMPLECONSENT_OPT . '[rights_page_id]', 'id' => 'ccn-rights_page_id', 'selected' => (int) $s['rights_page_id'], 'show_option_none' => __( '— Sin página asignada —', 'cumpleconsent' ), 'option_none_value'=> 0, 'echo' => false, ) ) ); $this->textarea_field( 'rights_holidays', __( 'Feriados adicionales para el cálculo de días hábiles', 'cumpleconsent' ), __( 'Uno por línea con formato AAAA-MM-DD. Se usa para el plazo de 2 días hábiles del bloqueo temporal. Carga los feriados trasladados por ley de tu región: el plugin calcula los fijos y Semana Santa, no adivina traslados.', 'cumpleconsent' ), 4 ); } private function fields_transfers( array $rows ): void { echo '

' . esc_html__( 'El art. 14 ter h) obliga a informar quién trata tus datos fuera de Chile, en qué país y con qué garantía. Declara solo lo que tienes suscrito: el diagnóstico cruza esta tabla con los terceros que detecta en el sitio.', 'cumpleconsent' ) . '

'; printf( '

%s %s

', esc_url( $this->admin_url( 'cumpleconsent-ajustes', array( 'tab' => 'transferencias', 'prefill' => 1 ) ) ), esc_html__( 'Precargar proveedores frecuentes', 'cumpleconsent' ), esc_html__( 'Precarga el formulario con proveedores habituales para que los revises. No se guarda nada hasta que tú guardes.', 'cumpleconsent' ) ); echo ''; $max = max( 6, count( $rows ) + 2 ); for ( $i = 0; $i < $max; $i++ ) { $row = $rows[ $i ] ?? array( 'provider' => '', 'country' => '', 'basis' => 'safeguards', 'note' => '', ); echo ''; printf( '', esc_attr( CUMPLECONSENT_OPT ), (int) $i, esc_attr( (string) $row['provider'] ) ); printf( '', esc_attr( CUMPLECONSENT_OPT ), (int) $i, esc_attr( (string) $row['country'] ) ); echo ''; printf( '', esc_attr( CUMPLECONSENT_OPT ), (int) $i, esc_attr( (string) $row['note'] ) ); echo ''; } echo '
' . esc_html__( 'Proveedor', 'cumpleconsent' ) . '' . esc_html__( 'País', 'cumpleconsent' ) . '' . esc_html__( 'Garantía (art. 27)', 'cumpleconsent' ) . '' . esc_html__( 'Nota', 'cumpleconsent' ) . '
'; } private function fields_ops( array $ops ): void { echo '

' . esc_html__( 'Registro de actividades de tratamiento del sitio web (art. 3 d) del reglamento D.S. 662). Si dejas la tabla vacía, el plugin propone un registro a partir de tu configuración; revísalo y guárdalo para que quede como registro revisado.', 'cumpleconsent' ) . '

'; $max = max( 8, count( $ops ) + 2 ); for ( $i = 0; $i < $max; $i++ ) { $op = $ops[ $i ] ?? array( 'name' => '', 'finalidad' => '', 'datos' => '', 'titulares' => '', 'base' => '', 'destinos' => '', 'pais' => '', 'plazo' => '', 'fuente' => '', 'transfiere' => 'no', 'perfilado' => 'no', 'sensibles' => 'no', ); /* translators: %d: número de la operación en el registro de actividades de tratamiento. */ echo '
' . esc_html( sprintf( __( 'Operación %d', 'cumpleconsent' ), $i + 1 ) ) . ''; echo ''; $fields = array( 'name' => __( 'Nombre de la operación', 'cumpleconsent' ), 'finalidad' => __( 'Finalidad (v. del art. 3 d)', 'cumpleconsent' ), 'datos' => __( 'Datos tratados y si hay sensibles (i)', 'cumpleconsent' ), 'titulares' => __( 'Categorías de titulares (iv)', 'cumpleconsent' ), 'base' => __( 'Base de licitud (vii)', 'cumpleconsent' ), 'destinos' => __( 'Destinatarios o encargados (viii)', 'cumpleconsent' ), 'pais' => __( 'País de destino si hay transferencia (ix)', 'cumpleconsent' ), 'plazo' => __( 'Plazo de supresión (x)', 'cumpleconsent' ), 'fuente' => __( 'Fuente de los datos (vi)', 'cumpleconsent' ), ); foreach ( $fields as $key => $label ) { printf( '', (int) $i, esc_attr( $key ), esc_html( $label ), esc_attr( CUMPLECONSENT_OPT ), esc_attr( (string) ( $op[ $key ] ?? '' ) ) ); } echo ''; echo '
' . esc_html__( 'Marcadores', 'cumpleconsent' ) . ''; foreach ( array( 'transfiere' => __( 'Transfiere al extranjero', 'cumpleconsent' ), 'perfilado' => __( 'Perfila o decide automáticamente', 'cumpleconsent' ), 'sensibles' => __( 'Trata datos sensibles', 'cumpleconsent' ), ) as $key => $label ) { printf( ' ', esc_attr( CUMPLECONSENT_OPT ), (int) $i, esc_attr( $key ), checked( 'si' === (string) ( $op[ $key ] ?? 'no' ), true, false ), esc_html( $label ) ); } echo '
'; } } // ── Registro ───────────────────────────────────────────────────────────── public function screen_log(): void { $this->require_view(); $plugin = Plugin::instance(); $event = isset( $_GET['event'] ) ? sanitize_key( (string) $_GET['event'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $paged = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $per = 50; $rows = $plugin->log->rows( array( 'limit' => $per, 'offset' => ( $paged - 1 ) * $per, 'event' => $event, ) ); $stats = $plugin->log->stats(); $this->tabs( 'cumpleconsent-registro' ); echo '
'; echo '

' . esc_html__( 'Prueba del consentimiento', 'cumpleconsent' ) . '

'; echo '

' . esc_html__( 'Cada fila guarda la decisión del visitante con la huella del aviso que estaba vigente cuando decidió. El HMAC permite detectar si un registro fue alterado después. No hay nombre, correo ni IP en claro.', 'cumpleconsent' ) . '

'; if ( $stats ) { echo '

'; foreach ( $stats as $key => $total ) { printf( '%s: %d ', esc_html( $key ), (int) $total ); } echo '

'; } echo '

'; printf( '%s ', esc_url( $this->action_url( 'export_log' ) ), esc_html__( 'Exportar CSV', 'cumpleconsent' ) ); printf( '%s', esc_url( $this->action_url( 'purge_log' ) ), esc_js( __( '¿Borrar todos los registros de consentimiento? Esta acción no se puede deshacer.', 'cumpleconsent' ) ), esc_html__( 'Borrar todo el registro', 'cumpleconsent' ) ); echo '

'; echo ''; foreach ( array( __( 'Fecha (UTC)', 'cumpleconsent' ), __( 'Seudónimo', 'cumpleconsent' ), __( 'Evento', 'cumpleconsent' ), __( 'Origen', 'cumpleconsent' ), __( 'Decisión', 'cumpleconsent' ), __( 'Aviso', 'cumpleconsent' ), __( 'Integridad', 'cumpleconsent' ) ) as $th ) { echo ''; } echo ''; if ( ! $rows ) { printf( '', esc_html__( 'Todavía no hay decisiones registradas.', 'cumpleconsent' ) ); } foreach ( $rows as $row ) { $choices = json_decode( (string) $row['choices'], true ); printf( '', esc_html( (string) $row['created_at'] ), esc_html( substr( (string) $row['anon_id'], 0, 12 ) ), esc_html( (string) $row['event'] ), esc_html( (string) $row['context'] ), esc_html( Purposes::describe( is_array( $choices ) ? $choices : array() ) ), esc_html( (string) $row['policy_version'] ), esc_html( substr( (string) $row['notice_hash'], 0, 12 ) ), $plugin->log->verify( $row ) ? 'OK' : '!' ); } echo '
' . esc_html( $th ) . '
%s
%s%s%s%s%s%s
%s
%s
'; echo '
'; } // ── Derechos ───────────────────────────────────────────────────────────── public function screen_rights(): void { $this->require_view(); $plugin = Plugin::instance(); $rows = $plugin->rights->rows( array( 'limit' => 100 ) ); $open = isset( $_GET['open'] ) ? (int) $_GET['open'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $this->tabs( 'cumpleconsent-derechos' ); echo '
'; echo '

' . esc_html__( 'Solicitudes de derechos (art. 11)', 'cumpleconsent' ) . '

'; echo '

' . esc_html__( 'Cada solicitud ingresa con su fecha y calcula sus plazos: 30 días corridos (prorrogables una sola vez por 30 más) y, si se pidió bloqueo temporal, 2 días hábiles. Guarda aquí la respuesta íntegra: es el respaldo que la ley te exige.', 'cumpleconsent' ) . '

'; printf( '

%s

', esc_url( $this->action_url( 'export_rights' ) ), esc_html__( 'Exportar CSV', 'cumpleconsent' ) ); $destino = Rights::notify_email(); if ( ! Rights::notifies_channel() ) { echo '

'; echo esc_html( '' !== $destino /* translators: %s: correo que recibe hoy las solicitudes. */ ? sprintf( __( 'Las solicitudes se avisan a %s, no al correo del canal de derechos. Completa “Correo que recibe las solicitudes” para que los avisos lleguen al buzón correcto.', 'cumpleconsent' ), $destino ) : __( 'No hay un correo receptor configurado para el canal de derechos: los avisos internos no se envían y una solicitud nueva puede pasar inadvertida.', 'cumpleconsent' ) ); echo '

'; } printf( '

%s %s

', esc_url( $this->action_url( 'test_mail' ) ), esc_html__( 'Enviar correo de prueba al canal', 'cumpleconsent' ), esc_html( '' !== $destino /* translators: %s: correo de destino de la prueba. */ ? sprintf( __( 'Se enviará a %s', 'cumpleconsent' ), $destino ) : __( 'Sin correo de destino', 'cumpleconsent' ) ) ); if ( ! $rows ) { echo '

' . esc_html__( 'No hay solicitudes registradas.', 'cumpleconsent' ) . '

'; return; } foreach ( $rows as $row ) { $due = Deadlines::status( (string) $row['due_at'] ); $block = ! empty( $row['block_due_at'] ) ? Deadlines::status( (string) $row['block_due_at'] ) : null; $is_open = ( (int) $row['id'] === $open ); echo '
'; echo '
'; printf( '#%d · %s · %s %s · %s', (int) $row['id'], esc_html( (string) $row['name'] ), esc_html( (string) ( $row['rut'] ?? '' ) ), esc_html( (string) $row['received_on'] ), esc_html( Rights::types()[ (string) $row['right_type'] ] ?? (string) $row['right_type'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge() devuelve HTML escapado. echo ' ' . $this->badge( 'overdue' === $due['state'] ? 'critico' : ( 'soon' === $due['state'] ? 'atencion' : 'ok' ) ); $fallo = trim( (string) ( $row['alert_error'] ?? '' ) ); $avisado = ! empty( $row['alert_at'] ); if ( '' !== $fallo ) { printf( ' %s', esc_attr( $fallo ), esc_html__( 'aviso interno falló', 'cumpleconsent' ) ); } elseif ( $avisado ) { printf( ' %s', esc_html__( 'avisado', 'cumpleconsent' ) ); } printf( ' %s · %s', esc_html( (string) $row['due_at'] . ' (' . (string) $due['label'] . ')' ), esc_html( (string) ( Rights::statuses()[ (string) $row['status'] ] ?? $row['status'] ) ) ); if ( $block ) { printf( ' %s', esc_html( __( 'bloqueo', 'cumpleconsent' ) . ': ' . (string) $row['block_due_at'] . ' (' . (string) $block['label'] . ')' ) ); } printf( ' %s', esc_url( $this->admin_url( 'cumpleconsent-derechos', array( 'open' => $is_open ? 0 : (int) $row['id'] ) ) ), esc_html( $is_open ? __( 'cerrar', 'cumpleconsent' ) : __( 'gestionar', 'cumpleconsent' ) ) ); echo '
'; if ( $is_open ) { echo '
'; printf( '

%s %s

', esc_html__( 'Documento de identidad:', 'cumpleconsent' ), esc_html( (string) ( $row['rut'] ?? '' ) ) ); printf( '

%s %s

', esc_html__( 'Correo:', 'cumpleconsent' ), esc_html( (string) $row['email'] ) ); printf( '

%s
%s

', esc_html__( 'Solicitud:', 'cumpleconsent' ), nl2br( esc_html( (string) $row['details'] ) ) ); if ( ! empty( $row['response_text'] ) ) { printf( '

%s (%s)
%s

', esc_html__( 'Respuesta registrada:', 'cumpleconsent' ), esc_html( (string) $row['responded_at'] ), nl2br( esc_html( (string) $row['response_text'] ) ) ); } if ( empty( $row['extended'] ) ) { printf( '

%s %s

', esc_url( $this->action_url( 'rights_extend', array( 'id' => (int) $row['id'] ) ) ), esc_html__( 'Aplicar la prórroga única de 30 días', 'cumpleconsent' ), esc_html__( 'El aviso de prórroga se envía por correo al titular y queda registrado.', 'cumpleconsent' ) ); } echo '
'; wp_nonce_field( self::ACTION ); echo ''; echo ''; echo ''; echo '

'; echo '


'; echo '

'; echo '

'; echo '

'; echo '
'; printf( '

%s %s

', esc_html__( 'Hash de evidencia del registro:', 'cumpleconsent' ), esc_html( (string) $row['proof_hash'] ) ); echo '
'; } echo '
'; } echo ''; } // ── Documentos ─────────────────────────────────────────────────────────── public function screen_documents(): void { $this->require_view(); $policy = Policy::score(); $this->tabs( 'cumpleconsent-documentos' ); echo '
'; echo '

' . esc_html__( 'Política de privacidad (art. 14 ter)', 'cumpleconsent' ) . '

'; if ( empty( $policy['linked'] ) ) { echo '

' . esc_html__( 'No hay una política publicada que verificar. Publica o asigna tu política de privacidad —o generala aquí abajo— y vuelve a este punto: entonces sí se revisan los doce contenidos.', 'cumpleconsent' ) . '

'; } else { printf( '

%s %d%% — %s

', esc_html__( 'Cobertura detectada de los doce contenidos mínimos:', 'cumpleconsent' ), (int) $policy['score'], /* translators: 1: contenidos completos, 2: parciales, 3: ausentes. */ esc_html( sprintf( __( '%1$d completos · %2$d parciales · %3$d ausentes', 'cumpleconsent' ), (int) $policy['ok'], (int) $policy['partial'], (int) $policy['missing'] ) ) ); } echo ''; foreach ( $policy['coverage'] as $key => $row ) { printf( '', esc_html( (string) $key ), esc_html( (string) $row['label'] ), /* translators: %s: contenido que falta para cubrir el literal. */ 'ok' !== $row['status'] ? '
' . esc_html( sprintf( __( 'Falta: %s', 'cumpleconsent' ), (string) $row['needs'] ) ) . '' : '', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge() devuelve HTML escapado. $this->badge( 'ok' === $row['status'] ? 'ok' : ( 'partial' === $row['status'] ? 'atencion' : 'critico' ) ) ); } echo '
' . esc_html__( 'Literal', 'cumpleconsent' ) . '' . esc_html__( 'Contenido exigido', 'cumpleconsent' ) . '' . esc_html__( 'Estado', 'cumpleconsent' ) . '
%s)%s%s%s
'; echo '

' . esc_html__( 'El verificador detecta contenidos presentes o ausentes en la política publicada (o en la generada, si aún no publicas). No evalúa suficiencia jurídica ni si el tratamiento real coincide con lo declarado: eso se revisa a mano.', 'cumpleconsent' ) . '

'; echo '

'; printf( '%s ', esc_url( $this->action_url( 'policy_generate' ) ), esc_html__( 'Generar o actualizar el borrador de política', 'cumpleconsent' ) ); printf( '%s ', esc_url( $this->action_url( 'policy_publish' ) ), esc_html__( 'Generar y publicar', 'cumpleconsent' ) ); if ( '' !== Policy::url() ) { printf( '%s', esc_url( Policy::url() ), esc_html__( 'Ver la política publicada', 'cumpleconsent' ) ); } echo '

'; echo '

' . esc_html__( 'Expediente para el programa de cumplimiento', 'cumpleconsent' ) . '

'; echo '

' . esc_html__( 'Documento imprimible con: identificación del responsable (art. 3 a) del reglamento), caracterización de las operaciones de tratamiento con sus once contenidos (d), matriz de riesgos graduada según la ley (e), protocolos del sitio (f) y mecanismo de reporte interno e incidentes (g).', 'cumpleconsent' ) . '

'; echo '

' . esc_html__( 'No constituye el programa de cumplimiento ni designa delegado: aporta los elementos documentales del sitio web para que el responsable los integre en su modelo.', 'cumpleconsent' ) . '

'; printf( '

%s

', esc_url( $this->action_url( 'expediente' ) ), esc_html__( 'Descargar el expediente (HTML imprimible)', 'cumpleconsent' ) ); echo '
'; // Matriz de riesgos visible. echo '

' . esc_html__( 'Matriz de riesgos del sitio web', 'cumpleconsent' ) . '

'; echo Rat::matrix_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML ya escapado en Rat. echo '
'; echo '
'; } // ── Acciones ───────────────────────────────────────────────────────────── public function handle_action(): void { $this->require_admin(); // phpcs:disable WordPress.Security.NonceVerification -- Las líneas siguientes leen // la entrada de esta acción, y lo primero que hace el manejador es require_admin(): // comprueba el nonce (check_admin_referer) y el permiso de administración. $do = isset( $_GET['do'] ) ? sanitize_key( (string) $_GET['do'] ) : ''; $plugin = Plugin::instance(); switch ( $do ) { case 'scan': $plugin->diagnostics->scan(); $this->redirect( 'cumpleconsent', array( 'scanned' => 1 ) ); break; case 'export_log': $plugin->log->export_csv(); exit; case 'purge_log': $deleted = $plugin->log->purge(); $this->redirect( 'cumpleconsent-registro', array( 'purged' => $deleted ) ); break; case 'export_rights': $plugin->rights->export_csv(); exit; case 'policy_generate': case 'policy_publish': $id = Policy::create_page( 'policy_publish' === $do ); Settings::save( array( 'policy_date' => current_time( 'mysql', true ) ) ); Plugin::purge_caches(); $this->redirect( 'cumpleconsent-documentos', array( 'policy' => $id, 'published' => 'policy_publish' === $do ? 1 : 0, 'policy_mode' => (string) Settings::get( 'policy_last_mode' ) ) ); break; case 'test_mail': $prueba = Rights::mail_test(); $this->redirect( 'cumpleconsent-derechos', array( 'mail' => $prueba['ok'] ? 1 : 0, 'mail_to' => (string) $prueba['to'], 'mail_err' => substr( (string) $prueba['error'], 0, 140 ), ) ); break; case 'expediente': $plugin->rat->download(); break; case 'rights_extend': $id = isset( $_GET['id'] ) ? (int) $_GET['id'] : 0; $plugin->rights->extend( $id ); $this->redirect( 'cumpleconsent-derechos', array( 'open' => $id, 'extended' => 1 ) ); break; case 'rights_respond': $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; $text = isset( $_POST['response_text'] ) ? wp_unslash( (string) $_POST['response_text'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $reject = ! empty( $_POST['reject'] ); $reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['reason'] ) ) : ''; $notify = ! empty( $_POST['notify'] ); $row = $plugin->rights->get( $id ); $saved = $plugin->rights->respond( $id, $text, 'correo', $reject, $reason ); if ( $saved && $notify && $row ) { Mail::send( (string) $row['email'], __( 'Respuesta a tu solicitud de derechos', 'cumpleconsent' ), sanitize_textarea_field( $text ), Rights::channel_email() ); } $this->redirect( 'cumpleconsent-derechos', array( 'open' => $id, 'responded' => $saved ? 1 : 0 ) ); break; } // phpcs:enable WordPress.Security.NonceVerification $this->redirect( 'cumpleconsent' ); } /** Redirige a una pantalla del plugin con mensajes de resultado. */ private function redirect( string $page, array $args = array() ): void { wp_safe_redirect( $this->admin_url( $page, $args ) ); exit; } }