Published on

How to create a WordPress edit profile shortcode and add it to any page

Create a WordPress User Profile shortcode

Using the Shortcode API we register our new ‘edit_profile’ shortcode using the add_shortcode function.

We check to see if the user is logged in, if not we display a link to the login page, otherwise we get the logged in users details that will be used to pre populate any fields added to the profile form.

Form submission errors and notices will be displayed using the not yet created function jcul_display_edit_profile_errors.

The edit profile form adds the ‘show_user_profile’ action allowing other plugins to add custom fields to the form, the ‘jcul-edit-profile’ hidden input is used to pass a WordPress nonce to authenticate the save request, ‘jcul-action’ is used to identify the edit profile submission.

The edit user profile shortcode by default does not include any fields, allowing it to be customized with any required fields, fields added to the form should be placed between the Edit profile fields comments.

function jcul_edit_profile_shortcode()
{

    if (!is_user_logged_in()) {
        echo '<p>You must be logged in edit your profile, <a href="' . esc_attr(wp_login_url()) . '">Click here to login</a>.</p>';
        return;
    }

    $redirect_to = apply_filters('editprofile_redirect', '');

    $user_id = get_current_user_id();
    $profile_user = get_userdata($user_id);

    if ($profile_user) {
        $profile_user->filter = 'edit';
    }

    ob_start();
?>
    <form name="editprofileform" id="editprofileform" method="post">

        <?php 
        if(function_exists('jcul_display_edit_profile_errors')){
            jcul_display_edit_profile_errors();
        }
        ?>
        
        <!-- BEGIN Edit Profile Fields -->

        <!-- END Edit Profile Fields -->

        <?php
        do_action('show_user_profile', $profile_user);
        ?>
        <input type="hidden" name="jcul-edit-profile" value="<?= esc_attr(wp_create_nonce('jcul_edit_profile_nonce')); ?>" />
        <input type="hidden" name="redirect_to" value="<?php echo esc_attr($redirect_to); ?>" />
        <input type="hidden" name="jcul-action" value="edit-profile" />
        <input type="hidden" name="user_id" value="<?= esc_attr($user_id); ?>" />
        <p class="submit">
            <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e('Update Profile'); ?>" />
        </p>
    </form>
<?php
    return ob_get_clean();
}

add_shortcode('edit_profile', 'jcul_edit_profile_shortcode');

Add the edit profile page shortcode to a page

Using a shortcode in a WordPress page can be done multiple ways for example if you are using the gutenberg editor you need to add a shortcode block to the page then enter the shortcode, if you are using the classic editor all you need to do is add the shortcode anywhere in the main content editor.

[edit_profile]

You are not restricted to outputting the new shortcode within a pages content, you can however output the wordpress edit profile form using the do_shortcode function in your wordpress theme files.

do_shortcode('[edit_profile]')

Once you have added the edit profile page we need to set the id of this page to a constant, this allows us later in this article to create redirects based on the set id, you can either define a constant in your wordpress config file or in your themes functions file.

if (!defined('JCUL_EDIT_PROFILE_ID')) {
    define('JCUL_EDIT_PROFILE_ID', 5);
}

Saving WordPress User Profile Data

Similar to the edit profile shortcode, the method used to save the user profile data is used as a base to be expanded upon to allow what ever profile fields needed, all fields added to the shortcode need to be processed between the Saving Profile Data comments within this function.

Using the WordPress action we check to see if the edit profile form has been submitted using the ‘jcul-action’ request variable, if so we validate the security nonce and continue to process the submitted form data, if any errors do occur we store them in a global variable to be displayed when returned to the edit profile page, otherwise the users profile is updated and the user is redirected back to the edit profile page showing the success message.

function jcul_save_user_profile()
{
    if (!isset($_REQUEST['jcul-action']) || $_REQUEST['jcul-action'] !== 'edit-profile') {
        return;
    }

    if (!wp_verify_nonce($_POST['jcul-edit-profile'], 'jcul_edit_profile_nonce')) {
        return;
    }

    global $jcul_errors;

    $jcul_errors = new \WP_Error();

    $user             = new stdClass;
    $user->ID         = get_current_user_id();
    $userdata         = get_userdata($user->ID);
    
    // BEGIN Saving Profile Data
    
    // END Saving Profile Data

    if ($jcul_errors->has_errors()) {
        return;
    }

    $user_id = wp_update_user($user);
    if (is_wp_error($user_id)) {
        $jcul_errors->merge_from($user_id);
        return;
    }

    wp_redirect(add_query_arg(array('updated' => 'true')));
    exit;
}

add_action('wp', 'jcul_save_user_profile');

Display edit profile form errors

Form errors and messages are displayed at the top of the edit profile form, errors are fetched from the previously declared global variable $jcul_errors, and form messages are displayed based on the ‘updated’ query parameter result.

function jcul_display_edit_profile_errors(){

    global $jcul_errors;

    if(!is_null($jcul_errors) && $jcul_errors->has_errors()):

        // Display error messages
        ?>
        <div class="jcul-message jcul-message--error">
            <?= implode('<br />', $jcul_errors->get_error_messages()); ?>
        </div>
    <?php elseif(isset($_GET['updated'])):
        
        // Display updated message

        switch($_GET['updated']){
            case 'email-changed':

                // email changed notice
                ?>
                <div class="jcul-message jcul-message--success">
                    <p>Email has been changed to <strong><?= esc_html($profile_user->user_email); ?></strong>.</p>
                </div>
                <?php
                break;
            case 'email-cancelled':

                // email change cancelled notice
                ?>
                <div class="jcul-message jcul-message--success">
                    <p>Email change has been cancelled.</p>
                </div>
                <?php
                break;
            case 'true':
            default:

                // profile updated notice
                ?>
                <div class="jcul-message jcul-message--success">
                    <p>Profile has been updated.</p>
                </div>
                <?php
            break;
        }
    endif;
}

Adding a First Name field to user profile page

The first name field is added by creating an input field with the name ‘first_name’ and setting the default value using the first_name property of $profile_user.

<p>
    <label for="first_name"><?php _e('First Name'); ?></label>
    <input 
        type="text"
        name="first_name"
        id="first_name"
        class="input"
        value="<?php echo esc_attr($profile_user->first_name); ?>"
        size="20"
        autocapitalize="off"
    />
</p>

Saving the first name field is done by checking to see if the first_name field exists, and if so sanitizing that data as a text field before updating the $user->first_name variable.

if (isset($_POST['first_name'])) {
    $user->first_name = sanitize_text_field($_POST['first_name']);
}

Adding a Last Name field to edit profile page

The last name field is added to the edit profile screen by creating an input field with the name ‘last_name’, and setting the default value using ‘$profile_user->last_name’.

<p>
    <label for="last_name"><?php _e('Last Name'); ?></label>
    <input
        type="text"
        name="last_name"
        id="last_name"
        class="input"
        value="<?php echo esc_attr($profile_user->last_name); ?>"
        size="20"
        autocapitalize="off"
    />
</p>

Saving of the last name field is done by confirming the last_name field has been posted, sanitizing it using ‘sanitize_text_field’ before setting it to $user->last_name variable.

if (isset($_POST['last_name'])) {
    $user->last_name = sanitize_text_field($_POST['last_name']);
}

Adding an Email Address field to user profile page

Adding a field to update the users email address can be done simply by adding an input field with the name ’email’, but in this example we will follow the core wordpress approach of alerting the user to the email address change, with an email send to the old email address to confirm, before changing the users email address.

This is accomplished by adding an input field to the page with the name email, followed by some descriptive text alerting the user to the email change process, next we check to see if the user has already attempted to change there email address but has not yet confirmed it, then we display a message to notify them of the pending change and allow them to cancel the current email change request.

<table>
    <tr class="user-email-wrap">
        <th>
            <label for="email">
                <?php _e('Email'); ?> <span class="description"><?php _e('(required)'); ?></span>
            </label>
        </th>
        <td>
            <input
                type="email"
                name="email"
                id="email"
                aria-describedby="email-description"
                value="<?php echo esc_attr($profile_user->user_email); ?>"
                class="regular-text ltr"
            />
            <p class="description" id="email-description">
                <?php _e('If you change this, we will send you an email at your new address to confirm it. <strong>The new address will not become active until confirmed.</strong>'); ?>
            </p>
            <?php

            $new_email = get_user_meta($profile_user->ID, '_new_email', true);
            if ($new_email && $new_email['newemail'] != $profile_user->user_email) :
            ?>
                <div class="updated inline">
                    <p>
                        <?php
                        printf(
                            /* translators: %s: New email. */
                            __('There is a pending change of your email to %s.'),
                            '<code>' . esc_html($new_email['newemail']) . '</code>'
                        );
                        printf(
                            ' <a href="%1$s">%2$s</a>',
                            esc_url(wp_nonce_url(add_query_arg([
                                'dismiss' => $profile_user->ID . '_new_email'
                            ], get_permalink(JCUL_EDIT_PROFILE_ID)), 'dismiss-' . $profile_user->ID . '_new_email')),
                            __('Cancel')
                        );
                        ?>
                    </p>
                </div>
            <?php endif; ?>
        </td>
    </tr>
</table>

Using the send_confirmation_on_profile_email function combined with the previously set hidden input named ‘user_id’ will send a confirmation request email if required to alert the user to the attempted email address change.

Next we check to see if the email address field is empty, not a valid email address, or if the email address has already be registered, if so the error is added to the list and displayed on the edit profile page to be rectified before resubmitting.

send_confirmation_on_profile_email($user->ID);

if (isset($_POST['email'])) {
    $user->user_email = sanitize_text_field(wp_unslash($_POST['email']));
}

/* checking email address */
if (empty($user->user_email)) {
    $jcul_errors->add('empty_email', __('<strong>Error</strong>: Please enter an email address.'), array('form-field' => 'email'));
} elseif (!is_email($user->user_email)) {
    $jcul_errors->add('invalid_email', __('<strong>Error</strong>: The email address isn&#8217;t correct.'), array('form-field' => 'email'));
} else {
    $owner_id = email_exists($user->user_email);
    if ($owner_id && (($owner_id != $user->ID))) {
        $jcul_errors->add('email_exists', __('<strong>Error</strong>: This email is already registered. Please choose another one.'), array('form-field' => 'email'));
    }
}

With the email change request sent to the users existing email account, we need to capture if they are cancelling or confirming the request.

Confirming the email change request is done by checking to see if the ‘newusermail’ query parameter has bee set, and that it matches the change request hash stored by wordpress, if so the new email address is updated, otherwise an error is returned.

Cancellation of the email change request is done via the ‘dismiss’ query parameter, validating the request before deleting the stored email change data.

function jcul_cancel_email_change()
{
    /**
     * @var \WPDB $wpdb
     */
    global $wpdb;

    $user_id = get_current_user_id();
    $current_user = get_userdata($user_id);

    if (isset($_GET['newuseremail']) && $current_user->ID) {
        $new_email = get_user_meta($current_user->ID, '_new_email', true);
        if ($new_email && hash_equals($new_email['hash'], $_GET['newuseremail'])) {
            $user             = new stdClass;
            $user->ID         = $current_user->ID;
            $user->user_email = esc_html(trim($new_email['newemail']));
            if (is_multisite() && $wpdb->get_var($wpdb->prepare("SELECT user_login FROM {$wpdb->signups} WHERE user_login = %s", $current_user->user_login))) {
                $wpdb->query($wpdb->prepare("UPDATE {$wpdb->signups} SET user_email = %s WHERE user_login = %s", $user->user_email, $current_user->user_login));
            }
            wp_update_user($user);
            delete_user_meta($current_user->ID, '_new_email');
            wp_redirect(add_query_arg(array('updated' => 'email-changed'), get_permalink(JCUL_EDIT_PROFILE_ID)));
            die();
        } else {
            wp_redirect(add_query_arg(array('error' => 'new-email'), get_permalink(JCUL_EDIT_PROFILE_ID)));
        }
    } elseif (!empty($_GET['dismiss']) && $current_user->ID . '_new_email' === $_GET['dismiss']) {
        check_admin_referer('dismiss-' . $current_user->ID . '_new_email');
        delete_user_meta($current_user->ID, '_new_email');
        wp_redirect(add_query_arg(array('updated' => 'email-cancelled'), get_permalink(JCUL_EDIT_PROFILE_ID)));
        die();
    }
}

add_action('wp', 'jcul_cancel_email_change');

With the email change request now handled via our edit profile form, we can redirect the request via the default wordpress location and set it to our user profile form.

function jcul_update_new_user_email_content($email_text, $new_user_email)
{
    return str_replace('###ADMIN_URL###', esc_url(add_query_arg(array('newuseremail' => $new_user_email['hash']), get_permalink(JCUL_EDIT_PROFILE_ID))), $email_text);
}

add_filter('new_user_email_content', 'jcul_update_new_user_email_content', 10, 2);

Adding a Password Field to user profile page

To change the users password on the edit profile screen we need to have two password inputs, called ‘pass1’ and ‘pass2’, a button to generate the password, a button to cancel the password change, and a checkbox to allow the user to confirm the usage of a weak password.

Since we have the same functionality as the core wordpress we can include the default ‘user-profile’ script to link this all together.

$show_password_fields = apply_filters('show_password_fields', true, $profile_user);
if ($show_password_fields) :
    wp_enqueue_script('user-profile');
    ?>
    <div class="user-pass1-wrap">
        <button type="button" class="button wp-generate-pw hide-if-no-js" aria-expanded="false">
            <?php _e('Set New Password'); ?>
        </button>
        <div class="wp-pwd hide-if-js" style="display: none;">
            <span class="password-input-wrapper">
                <input
                    type="password"
                    name="pass1"
                    id="pass1"
                    class="regular-text"
                    value=""
                    autocomplete="off"
                    data-pw="<?php echo esc_attr(wp_generate_password(24)); ?>"
                    aria-describedby="pass-strength-result"
                />
            </span>
            <button type="button" class="button wp-hide-pw hide-if-no-js" data-toggle="0" aria-label="<?php esc_attr_e('Hide password'); ?>">
                <span class="dashicons dashicons-hidden" aria-hidden="true"></span>
                <span class="text"><?php _e('Hide'); ?></span>
            </button>
            <button type="button" class="button wp-cancel-pw hide-if-no-js" data-toggle="0" aria-label="<?php esc_attr_e('Cancel password change'); ?>">
                <span class="dashicons dashicons-no" aria-hidden="true"></span>
                <span class="text"><?php _e('Cancel'); ?></span>
            </button>
            <div style="display:none" id="pass-strength-result" aria-live="polite"></div>
        </div>
    </div>
    <div class="user-pass2-wrap hide-if-js">
        <div scope="row"><label for="pass2"><?php _e('Repeat New Password'); ?></label></div>
        <div>
            <input
                name="pass2"
                type="password"
                id="pass2"
                class="regular-text"
                value=""
                autocomplete="off"
                aria-describedby="pass2-desc"
            />
            <p class="description" id="pass2-desc"><?php _e('Type your new password again.'); ?></p>
        </div>
    </div>
    <div class="pw-weak" style="display: none;">
        <div><?php _e('Confirm Password'); ?></div>
        <div>
            <label>
                <input type="checkbox" name="pw_weak" class="pw-checkbox" />
                <span id="pw-weak-text-label"><?php _e('Confirm use of weak password'); ?></span>
            </label>
        </div>
    </div>
<?php endif; ?>

To change the users password we use the check_passwords action to confirm password fields are checked for congruity, make sure the password does not contain backslashes, and confirm that the password has been entered twice, then set the password to $user->user_pass.

$pass1 = '';
$pass2 = '';
if (isset($_POST['pass1'])) {
    $pass1 = trim($_POST['pass1']);
}
if (isset($_POST['pass2'])) {
    $pass2 = trim($_POST['pass2']);
}

/**
 * Fires before the password and confirm password fields are checked for congruity.
 *
 * @since 1.5.1
 *
 * @param string $user_login The username.
 * @param string $pass1     The password (passed by reference).
 * @param string $pass2     The confirmed password (passed by reference).
 */
do_action_ref_array('check_passwords', array($user->user_login, &$pass1, &$pass2));

// Check for "\" in password.
if (false !== strpos(wp_unslash($pass1), '\\')) {
    $jcul_errors->add('pass', __('<strong>Error</strong>: Passwords may not contain the character "\\".'), array('form-field' => 'pass1'));
}

// Checking the password has been typed twice the same.
if ($pass1 != $pass2) {
    $jcul_errors->add('pass', __('<strong>Error</strong>: Passwords don&#8217;t match. Please enter the same password in both password fields.'), array('form-field' => 'pass1'));
}

if (!empty($pass1)) {
    $user->user_pass = $pass1;
}

Redirect WordPress Dashboard to the edit profile page

Now that we have a frontend edit profile screen for WordPress users, we can redirect access from the WordPress admin dashboard to our edit profile page.

function jcul_disable_dashboard()
{
    if (current_user_can('subscriber') && is_admin()) {
        wp_redirect(get_permalink(JCUL_EDIT_PROFILE_ID));
        exit;
    }
}
add_action('admin_init', 'jcul_disable_dashboard');

Conclusion

In this article we covered how to display the edit profile form on any page of a WordPress website using a custom shortcode, how to add basic profile fields such as first and last name.

Next we covered how to add an email address field that has to be confirmed via an email sent to the existing email address, also giving the user an option to cancel an email address change.

We then covered how to add a password field onto the edit profile form, allowing the user to generate a new password, or manually enter it and display a confirm message if the password is weak.

Finally we look into redirecting subscribers from the WordPress Dashboard to our frontend edit profile page.