I know this is probably going to upset, but this is the best way to save an input from a form to a post meta field in WordPress:
update_post_meta( $post->ID, '_slug_number_field', $_POST[ 'number_field' ] );
And yes, on its own that line of code is a terrible, no good idea. Most likely, you mentally rewrote it to something like this:
if ( isset( $_POST[ 'number_field' ] ) && 0 < absint( $_POST[ 'number_field' ] ) ) { update_post_meta( $post->ID, '_slug_number_field', (int) $_POST[ 'number_field' ] ); }
Before I discuss how to make that first line of code acceptable, I will explain what makes it so terrible. Then I’ll tackle why the second code snippet is not the best way to save input from a form to a post meta field in WordPress.
The problem with the first line of code is that we are saving a form field that should be a number to the database, but without sanitization and validation it could be anything—it could be a string, an object, or an array. This presents a problem when you retrieve the data with the expectation that it’s going to be an integer. More importantly, $_POST[ ‘number_field’ ] could contain a MySQL injection attack or some other malicious code that we need to prevent from getting in the database.
That’s why we write it the second way. That way the value is only saved if it is actually zero or a positive number.
Looks good, right? So are we done?
Not quite, as it still presents several other problems.
Regardless if you only save this meta field once in your plugin or if you save it with all other checks in place, we still have a serious problem.
The first problem is that repeating the sanitization routine is in direct violation of the Don’t Repeat Yourself (DRY) principle, and is therefore quantifiably bad. Not only does it make it easy to introduce cut and paste errors, but it also makes it more difficult to change how you sanitize the input.
Even if you keep it consistent every time you save that meta key in your plugin (from now to every future update) and never have to change how the sanitization works, and if you do, you change it in every place, it’s still not good enough.
What if someone else is customizing or extending your plugin and they save a form input into that field? Is it your fault if someone else passes the wrong data type or pass malicious code to MySQL? That question doesn’t matter, because it could have been prevented.
Making sure that the data coming in and out of the database is in the right format, and not dangerous as it passes through the database is our job as developers. If it’s not your concern, it should be.
The actual WordPress way
The solution is easy and relies only on core functionality. It’s a simple function called register_meta(). This function is almost identical to the more commonly used register_setting(). When we use register_setting(), we are able to register a callback function. That way, when anyone saves this setting that’s being registered, it passes through that callback for sanitation and validation.
We can do the exact same thing for meta fields with register_meta and not just post meta, but any type of meta data. And, it’s easy to do.
All we have to do to make the example code safe and secure is add this:
add_action( 'init', function() { register_meta( 'post', '_slug_number_field', 'absint' ); });
Now all values being saved to this field will pass through absint(). No need to worry about repeating ourselves properly. No need to worry about other developers doing it wrong. It’s all right there.
This applies no matter where the input is coming from. We’re no longer only addressing the possibility of bad POST data. A meta field can be saved via a variety of methods. This covers them all.
The third argument of register_meta() is the actual callback. That means that we can pass any of the built-in WordPress sanitization functions there. For example, what if we had a user meta field for entering more information about a user’s skills:
add_action( 'init', function() { register_meta( 'user', '_slug_skill_detail', 'sanitize_text_field' ); });
Of course, we can use a function of our own design here as well. For example,
function slug_user_skill_cb( $input ) { $input = wp_kses_post( $input ); if ( ! $input ) { if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { wp_send_json_error( array( 'fail' => true, 'message' => __( 'Input invalid', 'text-domain' ) ) ); else { $input = __( 'No skills set', 'text-domain' ); } return $input; }); add_action( 'init', function() { register_meta( 'user', '_slug_skill_detail', slug_user_skill_cb' ); });
Now we have input validation, a default save value, and error handling for AJAX based forms!
Stay safe and DRY, and leverage core functionality!
I’m always on the look out for ways to reduce redundant code. When it can be done in a way that protects me from making a simple human error that can undo all of the work I’ve done, for security reasons, that’s even better. And when WordPress core offers the solution, well that’s just extra shiny.
7 Comments