I have to react to this post. It would be criminal not to point out all the security problems you're currently facing. Warning it's long to read but short to understand.
I'm far from being an expert but most of the people looking at posts on Stackoverflow regarding front-end user data inputs and updates are (usually) mostly new to Wordpress and are here to learn. It's easy to get overwhelmed. Let's go easy and get started.
First of all, when checking permissions, you should always use triple operators. ===
instead of ==
. It doesn't seems like much but one means EQUAL and the other one means IDENTICAL. You can learn more about Comparison Operators here.
Checking the identity of a user is your only line of defense in regards to malicious practices. Granting front end editing capability is just a series of conditional statement a user MUST pass before being granted access to the database. Let's not mess around.
My assumption is that you're letting your users update their personal informations from their user profile page served by the author.php
template. Therefore the following thinking is based around it, but can be easily adapted. I have to insist that nothing is constrain to any templates. Here is the simple form we're going to use, currently in our author.php
template. It's a simplified version of your form.
<form method="POST">
<input type="text" name="first_name" value="<?php echo esc_attr( wp_get_current_user()->first_name ); ?>">
<input type="email" name="user_email" value="<?php echo esc_attr( wp_get_current_user()->user_email ); ?>">
<?php wp_nonce_field( esc_attr( 'update-user-' . get_current_user_id() ), esc_attr( 'nonce-user-' . get_current_user_id() ) ); ?>
<button type="submit">Submit</button>
</form>
For clarity and consistency, it's always a good idea to use Wordpress default function when you can. get_current_user_id()
Get the current user’s ID, instead of wp_get_current_user()->ID
.
Let's talk about action's hook. init
is a hook that fire really early in the Wordpress firing sequence.
Fires after WordPress has finished loading but before any headers are sent.
The WP_Object
hasn't even finished to set up. When looking for the right hook you can refer to the Plugin API/Action Reference. A more suiting firing hook would be wp
which is the recommended hook for any high-level filtering or validation, following queries, but before WordPress does any routing, processing, or handling.
Fires once the WordPress environment has been set up. After WP object is set up.
add_action( 'wp', 'wpso62938497_user_frontend_privy_editing' );
function wpso62938497_user_frontend_privy_editing() {
//...
};
The "hors d'oeuvre" of user checking in regards to front end editing is to know if the user is actually logged in. Which you are missing to do. We can use is_user_logged_in()
which determines whether the current visitor is a logged in user.
We also want to be sure that the user actually on the author.php
page is the page owner. get_current_user_id() === get_queried_object()->ID
will do just that. We will also be checking if the page we're on is an author page and has a WP_User
object set up. Finally we will compare both queried user and page author to be sure both are the same.
add_action( 'wp', 'wpso62938497_user_frontend_privy_editing' );
function wpso62938497_user_frontend_privy_editing() {
/**
* Determines whether the current visitor is a logged in user.
* Retrieves the currently queried object. Check whether an it belongs to a WP_User class.
* Determines whether the query is for an existing author archive page. Compare it to the current logged in user ID.
* Compare current logged in user ID with the current queried ID.
*/
if ( is_user_logged_in() && get_queried_object() instanceof \WP_User && is_author( get_current_user_id() ) && get_current_user_id() === get_queried_object()->ID ) {
//...
};
};
Let's talk about nonce. In your form you're declaring a nonce but you're not checking against it. A nonce is a "number used once" to help protect URLs and forms from certain types of misuse, malicious or otherwise. It's like you serial number from your ID card, that number is you. A nonce is your temporary ID card for 24 hours. As you can imagine, it's quite important. Wordpress as a whole page on Nonces, you should have a read. We will also be checking the REQUEST_METHOD
.
add_action( 'wp', 'wpso62938497_user_frontend_privy_editing' );
function wpso62938497_user_frontend_privy_editing() {
/**
* Determines whether the current visitor is a logged in user.
* Retrieves the currently queried object. Check whether an it belongs to a WP_User class.
* Determines whether the query is for an existing author archive page. Compare it to the current logged in user ID.
* Compare current logged in user ID with the current queried ID.
*/
if ( is_user_logged_in() && get_queried_object() instanceof \WP_User && is_author( get_current_user_id() ) && get_current_user_id() === get_queried_object()->ID ) {
/**
* Determines whether the POST request method was used to access the page.
* Determine if our nonce variable is declared and is different than null.
* Verifies that a correct security nonce was used with time limit.
*/
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['nonce-user-' . get_current_user_id()] ) && wp_verify_nonce( $_POST['nonce-user-' . get_current_user_id()], 'update-user-' . get_current_user_id() ) ) {
//...
};
};
};
We're nearly done! For any sanitization requirements you can check the following post which states what data is sanitize or not by wp_insert_user()
or wp_update_user()
.
Of course you should restrict the form using javascript, but javascript can easily be disabled. That's why we also need a check on the php side.
There is also a case to have in mind and handle, which is the constant prompt to submit on page refresh, F5, CTRL+R after a submission. That behavior can be avoided by redirecting the user to the previous page, and setting a 303 header. You can read more about that issue here.
Finally we're done, and our script is safe to use on the front end. And we're feeling like this!
add_action( 'wp', 'wpso62938497_user_frontend_privy_editing' );
function wpso62938497_user_frontend_privy_editing() {
/**
* Determines whether the current visitor is a logged in user.
* Retrieves the currently queried object. Check whether an it belongs to a WP_User class.
* Determines whether the query is for an existing author archive page. Compare it to the current logged in user ID.
* Compare current logged in user ID with the current queried ID.
*/
if ( is_user_logged_in() && get_queried_object() instanceof \WP_User && is_author( get_current_user_id() ) && get_current_user_id() === get_queried_object()->ID ) {
/**
* Determines whether the POST request method was used to access the page.
* Determine if our nonce variable is declared and is different than null.
* Verifies that a correct security nonce was used with time limit.
*/
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['nonce-user-' . get_current_user_id()] ) && wp_verify_nonce( $_POST['nonce-user-' . get_current_user_id()], 'update-user-' . get_current_user_id() ) ) {
if ( isset( $_POST['first_name'] ) && ! empty( $_POST['first_name'] ) && wp_get_current_user()->first_name !== $_POST['first_name'] && strcasecmp( sanitize_text_field( $_POST['first_name'] ), $_POST['first_name'] ) ) {
update_user_meta( get_current_user_id(), 'first_name', sanitize_text_field( $_POST['first_name'] ), esc_attr( wp_get_current_user()->first_name ) );
};
if ( is_email( sanitize_email( $_POST['user_email'] ) ) && strcasecmp( sanitize_email( $_POST['user_email'] ), $_POST['user_email'] ) ) {
$userdata = array (
'ID' => get_current_user_id(),
'user_email' => $_POST['user_email'],
);
wp_update_user( $userdata );
};
wp_safe_redirect( esc_url( get_author_posts_url( get_current_user_id() ) ), 303 );
exit;
};
};
};
This function is missing any error handling, which is pretty much adding else statements.
Some final wised words: "Sanitize Inputs, Escape Outputs". https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/