Common Mistakes WP Engineers Make

Chigozie Orunta
7 min readMar 11, 2024

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 make it a favourite among developers and website owners alike.

However, due to poor best practices and insufficient knowledge of the workings of WordPress, it has become commonplace to see WP Engineers make certain types of mistakes 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. E.g. take a look at the following logic:

<a href="<?php echo get_permalink(); ?>">Read More</a>

It displays a link with the text ‘Read More’.

At first glance, there seems to be nothing wrong with it and we could simply leave it that way. However, when outputting data, it is important that you safely escape the information that is presented to the user.

The scenario above could have easily gone sideways if the URL displayed 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 (while pressing CTRL/CMD keys), 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 if our website provided registered users 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 might have been harmful to them if that was not their intent.

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 and links 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 Hooks Return Type

Hooks provides a flexible way for WP engineers to add custom logic to existing code. They are in large part 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 return types which happens when filters are not correctly typecast on return.

Let’s see the following example:

<?php

$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 a user incorrectly uses the ‘custom_pages’ filter by returning a string like so?

<?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 returned but the user returns 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 where the global query (WP_Query) and post objects may not be set up yet. It needs to be called at the right hook 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 the hook in which your logic is tied is currently available at the time of binding (event/hook subscription), otherwise, it might not work as expected.

Fetching, Not Caching

In today’s world of fast-paced technology and gadgets, there is an ever-prevalent need to get information to the user in a quick and timely manner. An engineering technique which helps us achieve this for high-end, large-scale WP applications is called 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 request loads on the servers.

Unfortunately, most WP engineers still perform large database query operations which are expensive for each site user.

Let’s see 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 cache storage and use that to serve customers and 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 Cache it is important to not assume that the cache key would contain data or information. 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 not even 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.

--

--