GeneratorsAdminUser Contact Methods

User Contact Methods Generator

Add fields to the Contact Info table on the user profile screen, with sanitisation, validation, REST exposure and an output helper.

acme-contact-methods.php
Saved just now
Your fields
4 fields
Mastodonmastodon
Required
In REST
LinkedInlinkedin
Required
In REST
GitHubgithub
Required
In REST
Press emailpress_email
Required
In REST
What ships alongside
Validate on save — hooks user_profile_update_errors so bad URLs, bad emails and empty required fields are rejected before core writes them.
Core methods
3 removed
<?php
// Generated with WP CodeKit — powered by GrowQuest (https://growquest.io).
/**
 * Plugin Name:       acme contact methods
 * Description:       Adds 4 contact fields to the user profile screen.
 * Version:           1.0.0
 * Requires PHP:      7.4
 * Text Domain:       acme
 */

defined( 'ABSPATH' ) || exit;

/**
 * Add the fields this site needs to the Contact Info table.
 *
 * @param array        $methods Method key => label.
 * @param WP_User|null $user    The user being edited, null on the list screen.
 * @return array
 */
function acme_user_contact_methods( $methods, $user ) {
	unset( $methods['aim'] );
	unset( $methods['yim'] );
	unset( $methods['jabber'] );

	$methods = array_merge(
		$methods,
		array(
			'acme_mastodon'    => __( 'Mastodon', 'acme' ),
			'acme_linkedin'    => __( 'LinkedIn', 'acme' ),
			'acme_github'      => __( 'GitHub', 'acme' ),
			'acme_press_email' => __( 'Press email', 'acme' ),
		)
	);

	return $methods;
}
add_filter( 'user_contact_methods', 'acme_user_contact_methods', 10, 2 );

/**
 * The fields worth checking before the profile saves.
 *
 * @return array
 */
function acme_contact_field_rules() {
	return array(
		'acme_mastodon'    => array(
			'label'    => __( 'Mastodon', 'acme' ),
			'type'     => 'url',
			'required' => false,
		),
		'acme_linkedin'    => array(
			'label'    => __( 'LinkedIn', 'acme' ),
			'type'     => 'url',
			'required' => false,
		),
		'acme_press_email' => array(
			'label'    => __( 'Press email', 'acme' ),
			'type'     => 'email',
			'required' => false,
		),
	);
}

/**
 * Reject bad values before core writes them to usermeta.
 *
 * Core sanitises contact methods with sanitize_text_field() and nothing more,
 * so this is the only chance to say no.
 *
 * @param WP_Error $errors Error collector, passed by reference.
 * @param bool     $update Whether this is an existing user.
 * @param stdClass $user   The user object about to be written.
 */
function acme_validate_contact_methods( $errors, $update, $user ) {
	foreach ( acme_contact_field_rules() as $key => $rule ) {
		$value = isset( $_POST[ $key ] ) ? trim( sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) ) : '';

		if ( '' === $value ) {
			if ( ! empty( $rule['required'] ) ) {
				$errors->add(
					$key . '_empty',
					sprintf(
						/* translators: %s: field label */
						__( '<strong>Error</strong>: %s cannot be empty.', 'acme' ),
						$rule['label']
					)
				);
			}

			continue;
		}

		if ( 'url' === $rule['type'] && ! wp_http_validate_url( $value ) ) {
			$errors->add(
				$key . '_invalid',
				sprintf(
					/* translators: %s: field label */
					__( '<strong>Error</strong>: %s must be a full URL, starting with https://.', 'acme' ),
					$rule['label']
				)
			);
		}

		if ( 'email' === $rule['type'] && ! is_email( $value ) ) {
			$errors->add(
				$key . '_invalid',
				sprintf(
					/* translators: %s: field label */
					__( '<strong>Error</strong>: %s is not a valid email address.', 'acme' ),
					$rule['label']
				)
			);
		}
	}
}
add_action( 'user_profile_update_errors', 'acme_validate_contact_methods', 10, 3 );

/**
 * Expose the fields on the user REST resource.
 *
 * Without this the values exist in usermeta but never appear in /wp/v2/users.
 */
function acme_register_contact_meta() {
	register_meta(
		'user',
		'acme_mastodon',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'description'       => __( 'Mastodon', 'acme' ),
			'sanitize_callback' => 'esc_url_raw',
			'auth_callback'     => function () {
				return current_user_can( 'edit_users' );
			},
		)
	);

	register_meta(
		'user',
		'acme_linkedin',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'description'       => __( 'LinkedIn', 'acme' ),
			'sanitize_callback' => 'esc_url_raw',
			'auth_callback'     => function () {
				return current_user_can( 'edit_users' );
			},
		)
	);

	register_meta(
		'user',
		'acme_github',
		array(
			'type'              => 'string',
			'single'            => true,
			'show_in_rest'      => true,
			'description'       => __( 'GitHub', 'acme' ),
			'sanitize_callback' => 'sanitize_text_field',
			'auth_callback'     => function () {
				return current_user_can( 'edit_users' );
			},
		)
	);
}
add_action( 'init', 'acme_register_contact_meta' );

/**
 * The author contact links, ready to print.
 *
 * @param int $user_id User id. Defaults to the current post author.
 * @return string Escaped markup, or an empty string when nothing is filled in.
 */
function acme_contact_links( $user_id = 0 ) {
	$user_id = $user_id ? (int) $user_id : get_the_author_meta( 'ID' );
	$fields  = array(
		'acme_mastodon'    => array(
			'label'  => __( 'Mastodon', 'acme' ),
			'escape' => 'esc_url',
			'link'   => true,
		),
		'acme_linkedin'    => array(
			'label'  => __( 'LinkedIn', 'acme' ),
			'escape' => 'esc_url',
			'link'   => true,
		),
		'acme_github'      => array(
			'label'  => __( 'GitHub', 'acme' ),
			'escape' => 'esc_html',
			'link'   => false,
		),
		'acme_press_email' => array(
			'label'  => __( 'Press email', 'acme' ),
			'escape' => 'esc_html',
			'link'   => true,
		),
	);

	$items = array();

	foreach ( $fields as $key => $field ) {
		$value = get_user_meta( $user_id, $key, true );

		if ( ! $value ) {
			continue;
		}

		$label = esc_html( $field['label'] );

		if ( $field['link'] ) {
			$href    = ( false === strpos( $value, '@' ) || false !== strpos( $value, '://' ) ) ? esc_url( $value ) : 'mailto:' . esc_attr( sanitize_email( $value ) );
			$items[] = sprintf( '<li><a href="%1$s" rel="me nofollow">%2$s</a></li>', $href, $label );
			continue;
		}

		$items[] = sprintf( '<li>%1$s: %2$s</li>', $label, call_user_func( $field['escape'], $value ) );
	}

	if ( ! $items ) {
		return '';
	}

	return '<ul class="acme-contact">' . implode( '', $items ) . '</ul>';
}