Common Mistakes WP Engineers Make
WordPress is famous for being one of the most popular content management systems (CMS) worldwide, powering millions of websites across various industries. Its flexibility, ease of use and extensive plugin ecosystem makes it a favourite among developers and website owners alike.
Unfortunately, due to insufficient knowledge of the inner workings of WordPress, Engineers often find themselves falling into common types of errors, repeatedly. In this article, I'll explore some of these mistakes and provide insights on how to avoid them.
Escaping Output
This is by far one of the most common mistakes WP engineers make. Let's take a look at the following logic, shall we:
<a href="<?php echo get_permalink(); ?>">Read More</a>
It displays a link with the text 'Read More'.
At first glance, this seems harmless and does not pose any real danger or threat since we are simply displaying the link using a valid WordPress function. However, when outputting data, it is important that you safely escape the information that is presented to the user. The reason is because, you can never fully trust what you are being given to output to the user.
The scenario above could have easily gone sideways if the URL given to you to display contained some malicious JavaScript code like so:
<?php $url = 'javascript:alert(document.domain)'; ?>
<a href="<?php echo $url; ?>">Read More</a>
By clicking the above link, the user is forced to run the attached JavaScript code, unknowingly. This is what is also known as a Cross-Site scripting (XSS) attack and could have been very well easily staged by an attacker if our website provided users with the opportunity to enter their custom URL via a form.
To prevent this from happening, we could have simply escaped the URL at the point of output like so:
<?php $url = 'javascript:alert(document.domain)'; ?>
<a href="<?php echo esc_url( $url ); ?>">Read More</a>
In this way, the URL is safely escaped when the user clicks on the link and they would have potentially avoided the mistake of running a script which would have been harmful to them.
In addition to the above escape function, WordPress provides a collection of other useful escape functions like:
esc_attr()
esc_html()
esc_textarea()
esc_js()
wp_kses()
As a general rule of thumb, please note that it is good practice to always safely escape your data at the point of output and never before.
Sanitizing Input
On the internet, we constantly use forms to provide a way for our clients to interact and collect data from their customers. These forms would usually contain input fields where users could provide their names, ages, locations, and other relevant information.
As WP engineers, we know better than to trust the user's input because it could contain malicious code waiting to happen. It is very important to sanitize data correctly before storing them in the database to prevent unexpected behaviour in our WordPress applications.
Let's take a look at the form below:
<form method="POST" action="<?php echo $_SERVER['REQUEST_URI']; ?>">
<p>
<label>Website URL</label>
<input type="text" name="url" placeholder="https://your-website.com"/>
</p>
</form>
When the form is submitted, we can save our URL to custom metadata tied to the current post like so:
<?php
if ( 'POST' === $_SERVER['REQUEST'] ) {
global $post;
update_post_meta( $post->ID, 'url', $_POST['url'] ?? '' );
}
?>
The problem with the above implementation is that it gives room for attackers to inject all kinds of malicious JavaScript code directly into our database because it is not sanitized.
By updating this line, we can safely store our user data without any concerns:
update_post_meta( $post->ID, 'url', sanitize_url( $_POST['url'] ?? '' ) );
For more sanitization functions, please check out the following:
sanitize_title()
sanitize_key()
sanitize_term()
sanitize_meta()
sanitize_text_field()
sanitize_textarea_field()
sanitize_email()
sanitize_option()
sanitize_mime_type()
Incorrect Hook Return Type
Hooks provides a flexible way for WP engineers to add custom logic to existing code. They are largely responsible for the ease with which WordPress developers have been able to build 3rd-party integrations into very popular plugins & WP applications.
Unfortunately, with Hooks comes a potential pitfall of incorrect return types which happens when filters are not correctly typecast on return.
Let's see the following example:
<?php
/*
* Get Page IDs.
*
* This method returns the Page IDs based on slug.
*
* @return array
*/
public function get_page_ids(): array {
$pages = [
'Home' => 'home',
'Sample Page' => 'sample-page',
];
$ids = array_map(
function( $page ) {
return is_page( $page ) ? get_page_by_path( $slug )->ID : false;
},
apply_filters( 'custom_pages', $pages )
);
return $ids;
}
?>
What happens when our user incorrectly uses the 'custom_pages' filter by returning a string like so?
Recommended by LinkedIn
<?php
add_filter( 'custom_pages', function( $pages ) {
return 'about-us';
} );
?>
Our WordPress plugin breaks and displays an error message because it is expecting to see an array but the user has now returned a string instead.
We can prevent this from happening by simply typecasting the return value from the apply_filters line. In this way, even if the user makes the erroneous mistake of sending back the wrong return type, we can gracefully catch it and proceed to return the appropriate type like so:
(array) apply_filters( 'custom_pages', $pages )
Invoking WP Function on Wrong Hook
In WordPress, timing is a crucial aspect of how the WordPress Hook architecture works. When a page is loaded in WP, hooks are fired in a chronological or sequential order and this means that certain functions are only available when specific hooks have been fired.
Let's take a look at an example, shall we:
<?php
add_action( 'init', function() {
if ( is_page() ) {
update_post_meta( $post->ID, 'movie', get_query_var( 'movie_id' ) );
}
} );
This implementation will not work because the is_page function is called way too early in the init hook, where the global query (WP_Query) and post objects may have not been set up yet. It needs to be called at the right hook (wp) like so:
<?php
add_action( 'wp', function() {
if ( is_page() ) {
update_post_meta( $post->ID, 'movie', get_query_var( 'movie_id' ) );
}
} );
If you have written an implementation that requires a WP function to be called within a certain hook, please ensure that it is currently available at the time of binding, otherwise, it might not work as expected.
Fetching, Not Caching
Caching is the process by which we store relevant information that is needed by users in temporary storage objects such as Redis or Memcache to avoid having users hit our database for every single request.
In this way, when users visit our website, we just serve the data which has already been fetched and stored in the Cache. This helps us serve pages with faster response times and fewer HTTP requests to the database.
Unfortunately, most WP engineers still perform large, expensive, database query operations for site users.
Let's take a look at the following example:
<?php
$posts = get_posts(
[
'post_type' => 'movies',
'post_status' => 'publish',
'posts_per_page' => -1,
'tax_query' => [
[
'taxonomy' => 'category',
'field' => 'slug'
'terms' => 'action'
]
],
'meta_query' => [
'relation' => 'AND',
[
'key' => 'actor',
'value' => 'Sean Connery',
'compare' => '='
],
[
'key' => 'director',
'value' => 'Albert Broccoli',
'compare' => '='
],
[
'key' => 'price',
'value' => '50',
'compare' => '>'
]
]
]
);
return $posts;
?>
Assuming this query returns 100,000 movies for each request, we could potentially run into a situation where it becomes very difficult for us to serve many customers due to bandwidth constraints and database challenges.
The better way to do this would be to store this piece of information in some cache storage and use that to serve customers. We then proceed to only update it when a new movie (or post entry) is saved like so:
<?php
function get_movies(): array {
$cache_key = 'get_movies_cache';
$cache_data = wp_cache_get( $cache_key );
if ( ! $cache_data ) {
$posts = get_posts(
[
'post_type' => 'movies',
'post_status' => 'publish',
'posts_per_page' => -1,
'tax_query' => [
[
'taxonomy' => 'category',
'field' => 'slug'
'terms' => 'action'
]
],
'meta_query' => [
'relation' => 'AND',
[
'key' => 'actor',
'value' => 'Sean Connery',
'compare' => '='
],
[
'key' => 'director',
'value' => 'Albert Broccoli',
'compare' => '='
],
[
'key' => 'price',
'value' => '50',
'compare' => '>'
]
]
]
);
wp_cache_set( $cache_key, $posts );
return (array) $posts;
}
return (array) $cache_data;
}
?>
The only time the cache is updated or flushed is when a new movie is saved. You can tie this logic to a publish hook like so:
<?php
add_action( 'publish_movies', function( $post_id, $post ) ) {
wp_cache_replace( 'get_movies_cache', '' );
} );
?>
Cache Assumption
On the flip side of things, when using a Cache storage, it is important to not assume that it will always contain data. Caches represent a temporary storage layer for data and therefore by extension can be flushed or cleaned!
This means you should never fully rely on the assumption that your cache storage would have existing data like your database. You should always test to see if your cache has data before using it.
Let's take a look at this example:
<?php
$movies = wp_cache_get( 'get_movies_cache' );
?>
If we try to use our movies variable directly, we could end up with an empty data set because we haven't tested to see if it contains anything. Remember our cache may have been flushed or may not even be set in the first place!
To solve this problem, we can perform a simple check before attempting to use our movies' variable like so:
<?php
$movies = wp_cache_get( 'get_movies_cache' );
if ( ! $movies ) {
$movies = get_posts(
[
'post_type' => 'movies'
]
);
wp_cache_set( 'get_movies_cache', $movies );
}
// Now proceed to use $movies here...
?>
Conclusion
If you've made it this far, I want to say thank you for reading to the end. This list is by no means exhaustive, and from time to time, I'll be updating it as much as I can remember.
If you've come across some mistakes that I've failed to mention, please do not hesitate to drop by in the comments section, someone might find it useful.
Thanks and happy coding.