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.
<?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.
Fix the highlighted fields to update the output.
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
- Set a function prefix.
- Add a row per field: the meta key, the label, and what kind of value it is.
- Keep the output helper, which escapes each kind correctly.
- 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?
How do I display them in a theme?
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?
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?
Is the value sanitised when saved?
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.