User Contact Methods Generator

Adds fields to the user profile through user_contactmethods, with the escaping the output needs and the removal of the defaults WordPress no longer uses.

Enable JavaScript to customise; default output below.

Prefixes the functions. The meta keys use your own key names below, unprefixed, because that is the convention the filter established.

Fields
  1. Stored as user meta under exactly this name.

    Shown on the profile screen. Translatable in the generated code.

    Kind of value

    Decides how the output helper escapes it: esc_url, esc_html or sanitize_email plus esc_html.

The key is the user meta key, so changing it later orphans the data already saved.

Live preview user-contact-methods.php
<?php
/**
 * User profile contact fields.
 *
 * The user_contactmethods filter is the supported way to add a field to the
 * profile screen. WordPress renders the input, saves the value as user meta under
 * the key you give it, and sanitises it with sanitize_text_field on the way in.
 *
 * That sanitising is not escaping. A value that is safe to store is not
 * necessarily safe to print, which is why the output helper below escapes per
 * field rather than trusting what came out of the database.
 */

defined( 'ABSPATH' ) || exit;

add_filter( 'user_contactmethods', 'my_plugin_contact_methods' );

/**
 * Adds the fields, and removes the ones nobody uses any more.
 *
 * @param array $methods Existing methods, keyed by meta key.
 * @return array
 */
function my_plugin_contact_methods( $methods ) {
	$methods['mastodon'] = __( 'Mastodon', 'my_plugin' );

	// Core dropped these in 3.6. A theme or an older plugin can still add them
	// back, so they are removed here rather than assumed absent.
	unset( $methods['aim'], $methods['yim'], $methods['jabber'] );

	return $methods;
}

/**
 * Prints a user's contact links.
 *
 * Escaping is per field and happens here, on output. esc_url for a URL, esc_html
 * for text, and a sanitize_email check before printing an address so a broken one
 * does not become a broken mailto link.
 *
 * @param int $user_id User ID.
 * @return void
 */
function my_plugin_contact_links( $user_id ) {
	$user_id = absint( $user_id );

	if ( ! $user_id ) {
		return;
	}

	$links = array();

	$value = get_user_meta( $user_id, 'mastodon', true );

	if ( $value ) {
		$links[] = sprintf(
			'<a class="contact-mastodon" href="%1$s" rel="me noopener">%2$s</a>',
			esc_url( $value ),
			esc_html__( 'Mastodon', 'my_plugin' )
		);
	}

	if ( ! $links ) {
		return;
	}

	printf(
		'<ul class="contact-methods"><li>%s</li></ul>',
		implode( '</li><li>', $links )
	);
}

Output is valid and updates as you type.

The user_contactmethods filter adds fields to the user profile screen. WordPress renders the input, saves the value as user meta under the key you choose, and sanitises it with sanitize_text_field on the way in.

That sanitising is not escaping, and the difference is the point of this page. A value that is safe to store is not automatically safe to print: sanitize_text_field strips tags but it does not make a URL safe for an href, and it will happily store javascript:alert(1) as a perfectly ordinary string. The escaping has to happen on output, per field, according to what the field is.

How to use

  1. Set a function prefix.
  2. Add a row per field: the meta key, the label, and what kind of value it is.
  3. Keep the output helper, which escapes each kind correctly.
  4. Paste the file into a plugin.

Example

add_filter( 'user_contactmethods', 'my_plugin_contact_methods' );

function my_plugin_contact_methods( $methods ) {
	$methods['mastodon'] = __( 'Mastodon', 'my_plugin' );
	$methods['linkedin'] = __( 'LinkedIn', 'my_plugin' );
	$methods['phone']    = __( 'Phone', 'my_plugin' );

	// Core dropped these in 3.6. A theme or an older plugin can still add them
	// back, so they are removed here rather than assumed absent.
	unset( $methods['aim'], $methods['yim'], $methods['jabber'] );

	return $methods;
}

and the output side, where the escaping lives:

$value = get_user_meta( $user_id, 'mastodon', true );

if ( $value ) {
	$links[] = sprintf(
		'<a class="contact-mastodon" href="%1$s" rel="me noopener">%2$s</a>',
		esc_url( $value ),
		esc_html__( 'Mastodon', 'my_plugin' )
	);
}

esc_url is doing real work there: it drops a javascript: scheme entirely, which sanitize_text_field on the way in did not.

Pitfalls

The meta key is permanent. Change mastodon to fediverse later and every value already saved is orphaned under the old key. Pick the key you can live with, and if you must rename, migrate the meta.

Escape on output, per kind. esc_url for a URL, esc_html for text, and sanitize_email plus is_email before building a mailto:. Printing raw user meta into an attribute is the actual vulnerability here, and it is easy to reach because the field looked sanitised.

rel="me" is what makes a link verifiable. Mastodon and other fediverse servers check for it when confirming that a profile link is yours. It costs nothing and it is the reason to use it on a profile link rather than a plain anchor.

AIM, Yahoo IM and Jabber left core in 3.6. If you can still see them, something is adding them back, so the unset is worth keeping.

The filter does not validate. There is no per-field validation hook on the profile screen, so a user can save anything that survives sanitize_text_field. If a field has to be a URL, check it on output or add your own personal_options_update handler.

REST exposure is a decision, not a detail. show_in_rest on user meta makes the value readable by anyone who can read the user, which for a published author is everybody. The generated code leaves it off and includes an auth_callback when you turn it on.

Fields appear for every user. There is no built-in way to show a field to one role. If that matters, filter user_contactmethods conditionally on the user being edited, which is available as the second argument.

Compatibility

Everything runs in the browser: nothing is uploaded and nothing is stored.

user_contactmethods has been in WordPress since 2.9 and is unchanged. It takes the methods array and, as a second argument, the WP_User being edited, which is how a per-role field would work.

Values are stored as ordinary user meta, so get_user_meta() reads them anywhere and they appear in an export. They are not automatically shown on the front end: themes decide, and most themes show none of them, which is what the output helper is for.

The generated code was checked with php -l, including labels containing apostrophes, */ and ?>. Labels are wrapped in __() with the text domain you give, so the fields are translatable, and esc_html__() is used on output rather than __() followed by an escape.

register_meta with show_in_rest needs to run on init, and the auth_callback in the generated file limits editing to users who can edit_user on that ID, which is the check the REST API would otherwise skip for meta.

Frequently asked questions

Where do these fields appear?
On the user profile screen, in the Contact Info section, under the existing website field. Administrators see them on every user; a user sees them on their own profile.
How do I display them in a theme?
Call the generated helper with a user ID, usually get_the_author_meta( 'ID' ) inside a post loop. Or read the meta directly with get_user_meta and escape it yourself.
Can I remove the Website field?
Not with this filter: user_url is a column on the users table rather than a contact method. Hiding it means CSS or a bit of JavaScript on the profile screen, and it will still be editable through the API.
Should I use ACF instead?
If you already have ACF and you want field types, conditional logic and validation, yes. This filter is the zero-dependency way to add a handful of text fields, and it puts them where users expect to find them.
Is the value sanitised when saved?
With sanitize_text_field, which strips tags and control characters. It does not validate a URL or an email address, and it does not make the value safe for an HTML attribute. That is what the output escaping is for.

From the people who built this tool

WP Adminify

The WordPress admin, rebuilt: a dashboard worth looking at, menu and column control, a real file manager and the login page your client sees.

See WP Adminify Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.