Pulsa ESC para cerrar

Curso WordpressCurso Wordpress

Campos personalizados con una meta box: guarda datos extra de forma segura (nonce, permisos y limpieza)

Ya tenemos un tipo de contenido Recursos y una taxonomía Nivel. Ahora queremos guardar datos concretos de cada recurso: el enlace de descarga y la versión. No tiene sentido escribirlos dentro del texto: queremos campos propios, que podamos mostrar siempre en el mismo lugar y con el mismo formato.

Para eso existen los campos personalizados (post meta): datos extra asociados a una entrada. Y la forma clásica de editarlos es una meta box, una caja en la pantalla de edición.

Antes de empezar

  • Seguimos trabajando en el plugin cwp-recursos.php de las dos entradas anteriores de Estructuras Avanzadas.
  • Plugins como ACF (Advanced Custom Fields) hacen esto sin código. Hacerlo a mano una vez te enseña cómo se guardan datos de forma segura en WordPress, algo que vas a necesitar en cualquier plugin.

Paso 1. Agrega la caja

add_action( 'add_meta_boxes', 'cwp_recursos_agregar_caja' );

function cwp_recursos_agregar_caja() {
	add_meta_box(
		'cwp_recurso_datos',          // identificador
		'Datos del recurso',          // título de la caja
		'cwp_recursos_dibujar_caja',  // función que la dibuja
		'cwp_recurso',                // en qué tipo de contenido
		'side'                        // ubicación: 'side' o 'normal'
	);
}

function cwp_recursos_dibujar_caja( $post ) {
	wp_nonce_field( 'cwp_recurso_guardar', 'cwp_recurso_nonce' );

	$url     = get_post_meta( $post->ID, '_cwp_url_descarga', true );
	$version = get_post_meta( $post->ID, '_cwp_version', true );
	?>
	<p>
		<label for="cwp_url_descarga"><strong>Enlace de descarga</strong></label><br>
		<input type="url" id="cwp_url_descarga" name="cwp_url_descarga"
			value="<?php echo esc_attr( $url ); ?>" class="widefat" placeholder="https://">
	</p>
	<p>
		<label for="cwp_version"><strong>Versión</strong></label><br>
		<input type="text" id="cwp_version" name="cwp_version"
			value="<?php echo esc_attr( $version ); ?>" class="widefat" placeholder="1.0.0">
	</p>
	<?php
}

Abre un recurso para editar: en el panel derecho (en el editor de bloques, dentro de la pestaña del recurso, más abajo) aparece la caja Datos del recurso.

  • get_post_meta( $id, $clave, true ): lee un dato. El true pide un solo valor en vez de una lista.
  • Las claves empiezan con guion bajo (_cwp_version). Así WordPress las oculta del panel genérico de «Campos personalizados» y nadie las edita por error desde ahí.
  • esc_attr(): escapamos el valor al ponerlo dentro del atributo value.
  • wp_nonce_field(): agrega un campo oculto con un código de un solo uso. Lo explicamos en el paso siguiente.

Paso 2. Guarda los datos (con las 4 comprobaciones)

Esta es la parte importante. Antes de guardar cualquier dato que viene de un formulario, hay que comprobar cuatro cosas:

add_action( 'save_post_cwp_recurso', 'cwp_recursos_guardar_caja' );

function cwp_recursos_guardar_caja( $post_id ) {
	// 1. ¿El formulario salió realmente de nuestra caja?
	if ( ! isset( $_POST['cwp_recurso_nonce'] )
		|| ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['cwp_recurso_nonce'] ) ), 'cwp_recurso_guardar' ) ) {
		return;
	}

	// 2. ¿Es un guardado automático? Entonces no tocar nada.
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	// 3. ¿El usuario puede editar esta entrada?
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}

	// 4. Limpiar y guardar cada campo.
	if ( isset( $_POST['cwp_url_descarga'] ) ) {
		$url = esc_url_raw( wp_unslash( $_POST['cwp_url_descarga'] ) );
		if ( $url ) {
			update_post_meta( $post_id, '_cwp_url_descarga', $url );
		} else {
			delete_post_meta( $post_id, '_cwp_url_descarga' );
		}
	}

	if ( isset( $_POST['cwp_version'] ) ) {
		$version = sanitize_text_field( wp_unslash( $_POST['cwp_version'] ) );
		update_post_meta( $post_id, '_cwp_version', $version );
	}
}

Por qué cada comprobación

  1. Nonce: protege contra CSRF. Sin él, un atacante podría preparar una página que, al ser visitada por un administrador con sesión iniciada, envíe un formulario falso a tu sitio y cambie datos sin que lo note. El nonce demuestra que el formulario lo generó tu panel, para ese usuario, hace poco.
  2. Autoguardado: WordPress guarda borradores automáticamente cada cierto tiempo, sin enviar nuestros campos. Si no lo excluimos, podríamos borrar datos.
  3. Permisos: que alguien pueda enviar el formulario no significa que pueda editar esa entrada. current_user_can( 'edit_post', $id ) lo verifica.
  4. Limpieza: esc_url_raw() deja solo direcciones válidas (descarta, por ejemplo, un javascript:); sanitize_text_field() quita etiquetas y caracteres raros. wp_unslash() quita las barras invertidas que WordPress agrega a los datos de formularios.

El gancho save_post_cwp_recurso (con el nombre del tipo de contenido al final) solo se ejecuta al guardar recursos. Así no gastamos recursos revisando cada entrada o página que se guarde.

Paso 3. Muestra los datos en el sitio

add_filter( 'the_content', 'cwp_recursos_mostrar_datos', 20 );

function cwp_recursos_mostrar_datos( $contenido ) {
	if ( ! is_singular( 'cwp_recurso' ) || ! in_the_loop() || ! is_main_query() ) {
		return $contenido;
	}

	$url     = get_post_meta( get_the_ID(), '_cwp_url_descarga', true );
	$version = get_post_meta( get_the_ID(), '_cwp_version', true );

	if ( ! $url ) {
		return $contenido;
	}

	$texto = $version ? 'Descargar (versión ' . $version . ')' : 'Descargar';

	return $contenido . sprintf(
		'<p class="cwp-descarga"><a href="%s" rel="nofollow noopener" target="_blank">%s</a></p>',
		esc_url( $url ),
		esc_html( $texto )
	);
}

Otra vez la regla: limpiar al guardar y escapar al mostrar. Aunque limpiamos al guardar, escapamos de nuevo al mostrar, porque el dato pudo llegar a la base de datos por otro camino (una importación, otro plugin).

Paso 4 (opcional). Que el editor de bloques también los conozca

Si quieres leer o editar estos campos desde bloques o desde la API REST, regístralos:

add_action( 'init', function () {
	register_post_meta( 'cwp_recurso', '_cwp_version', array(
		'type'              => 'string',
		'single'            => true,
		'show_in_rest'      => true,
		'sanitize_callback' => 'sanitize_text_field',
		'auth_callback'     => function () {
			return current_user_can( 'edit_posts' );
		},
	) );
} );

Con esto, el campo queda documentado y protegido también fuera de nuestra meta box.

Resumen de seguridad para cualquier formulario

  • Nonce al dibujar (wp_nonce_field) y al guardar (wp_verify_nonce).
  • Permisos con current_user_can.
  • Limpiar con sanitize_* o esc_url_raw antes de guardar.
  • Escapar con esc_html, esc_attr o esc_url al mostrar.

Si te quedas con estas cuatro reglas, ya escribes código más seguro que muchos plugins publicados.

¿Qué campos necesitan tus contenidos? Comparte tu caso en el foro y conversemos si conviene hacerlo a mano, con ACF o con bloques.