ControlledChaos
7/9/2018 - 10:59 PM

WordPress plugin to store custom and built-in user fields on a per-site basis for WP multisite. Good for multilingual setups. Customise to y

WordPress plugin to store custom and built-in user fields on a per-site basis for WP multisite. Good for multilingual setups. Customise to your needs.

<?php
/**
 * Per-Site User Fields
 *
 * @package   GaryJones\PerSiteUserfields
 * @author    Gary Jones
 * @copyright 2018 Gary Jones, Gamajo
 * @license   GPL-2.0-or-later
 *
 * @wordpress-plugin
 * Plugin Name:       Per-Site User Fields
 * Description:       Handle user fields to work on a per-site basis.
 * Version:           1.0.0
 * Author:            Gary Jones, Giuseppe Mazzapica, and Sami Keijonen
 * Author URI:        https://garyjones.io
 * Text Domain:       per-site-user-fields
 * License:           GPL-2.0-or-later
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Requires PHP:      7.1
 * Requires WP:       4.7
 */

declare( strict_types = 1 );

namespace GaryJones\PerSiteUserFields;

// Exit if accessed directly
if ( ! \defined( 'ABSPATH' ) ) {
	exit;
}

// 0. Define which fields we're targetting. Anything not already being sanitized elsewhere can have a callback here.

function per_site_user_settings() {
	return [
		'job_title' => [ // Custom added by this plugin.
			'sanitization' => function( $value ) {
				return sanitize_text_field( $value );
			},
		],
		'description'          => [], // Built-in field (bio).
		'intro_text'           => [], // Added by Genesis.
	];
}

// 1. First, handle the custom field[s]. These are saved and retrieved as global user settings (user_meta).

\add_action( 'show_user_profile', __NAMESPACE__ .  '\\add_user_field' );
\add_action( 'edit_user_profile', __NAMESPACE__ .  '\\add_user_field' );
/**
 * Add field to user profile.
 *
 * For this example, its just one field - job title, which we'll use on an author byline via code in our theme.
 *
 * @since  1.0.0
 */
function add_user_field( $user ) {
	$job_title_field = 'job_title';

	$job_title = \get_user_meta( $user->ID, $job_title_field, true );

	?>
	<h3><?php \esc_html_e( 'Bylines', 'per-site-user-fields' ); ?></h3>

	<table class="form-table">
		<tbody>
			<tr>
				<th scope="row"><label for="<?php echo esc_attr( $job_title_field ); ?>"><?php \esc_html_e( 'Job Title', 'per-site-user-fields' ); ?></label></th>
				<td>
					<input type="text" name="<?php echo esc_attr( $job_title_field ); ?>" id="<?php echo \esc_attr( $job_title_field ); ?>" value="<?php if ( ! empty( $job_title ) ) esc_attr_e( $job_title ); ?>" class="medium-text" />
				</td>
			</tr>
		<tbody>
	</table>
	<?php
}

\add_action( 'personal_options_update', __NAMESPACE__ .  '\\save_user_field' );
\add_action( 'edit_user_profile_update', __NAMESPACE__ .  '\\save_user_field' );
/**
 * Save field for user profile.
 *
 * Even though we're going to change _how_ we save the data, we still need to kickstart the saving process,
 * so WP is aware of the field.
 *
 * @since  1.0.0
 */
function save_user_field( $user_id ) {
	// Bail if we don't have permission to edit user.
	if ( ! \current_user_can( 'edit_user', $user_id ) ) {
		return false;
	}

	foreach ( per_site_user_settings() as $setting => $config ) {
		if ( array_key_exists( 'sanitization', $config ) ) { // Is a custom field.
			$value = isset( $_POST[ $setting ] ) ? call_user_func( $config['sanitization'], $_POST[ $setting ] ) : null;
		} else {
			// Default to text field sanitization.
			$value = isset( $_POST[ $setting ] ) ? \sanitize_text_field( $_POST[ $setting ] ) : null;
		}

		// Save value as user meta - this makes it a standard global setting,
		// but we overwrite this later to be per-site.
		\update_user_meta( $user_id, $setting, $value );
	}
}

// 2. Now adjust the targetted built-in and custom fields so they are handled on a per-site basis.

\add_filter(
	'update_user_metadata', // update_{$meta_type}_metadata hook.
	function ( $check, $user_id, $meta_key, $meta_value ) {
		if ( should_intercept_user_meta( $meta_key ) ) {
			// Remove site-specific value if it's empty, so that it falls
			// back to network value.
			if ( null === $meta_value || '' === $meta_value ) {
				\delete_user_option( $user_id, $meta_key );
				return true;
			}

			\update_user_option( $user_id, $meta_key, $meta_value );

			// Return a non-null value, so that the rest of update_user_meta()
			// does NOT run, and the unprefixed meta key doesn't get updated as well.
			return true;
		}

		// Effectively return null, so that non-primary sites, or other fields
		// we're not interested in, will fully run update_user_meta().
		return $check;
	},
	10,
	4
);


\add_filter( 'get_user_metadata', __NAMESPACE__ . '\\get_user_metadata', 10, 3 ); // get_{$meta_type}_metadata hook.

function get_user_metadata( $return, $user, $meta_key ) {
	if ( should_intercept_user_meta( $meta_key ) ) {
		// get_user_option() can call get_user_meta(), which would call this filter callback recursively,
		// so temporarily disable this filter, then add it back afterwards.
		\remove_filter( 'get_user_metadata', __NAMESPACE__ . '\\get_user_metadata', 10 );
		$return = \get_user_option( $meta_key, $user ) ?: '';
		\add_filter( 'get_user_metadata', __NAMESPACE__ . '\\get_user_metadata', 10, 3 );
	}

    return $return;
}

function should_intercept_user_meta( $meta_key ) {
	return \in_array( $meta_key, per_site_user_settings(), true )
		&& ! \is_network_admin()
		&& \get_current_blog_id() !== \get_main_site_id();
}