Wordpress - Empty search input returns all posts

Just an alternative to the informative answer by @PieterGoosen.

After Pieter posted this part:

if ( ! empty( $q['s'] ) ) {
    $search = $this->parse_search( $q );
}

it came to mind that it might be possible to re-parse the search query, within the posts_search filter, for empty search string. But the parse_search() method is protected and even if we could access it, the empty search would just give us:

AND (((wp_posts.post_title LIKE '%%') OR (wp_posts.post_content LIKE '%%'))) 

and that would just search for everything. So that path wasn't fruitful ;-)

In WordPress 4.5 it has changed to

if ( strlen( $q['s'] ) ) {
    $search = $this->parse_search( $q );
}

Instead we could try to halt the main query, in the case of an empty search:

/**
 * Halt the main query in the case of an empty search 
 */
add_filter( 'posts_search', function( $search, \WP_Query $q )
{
    if( ! is_admin() && empty( $search ) && $q->is_search() && $q->is_main_query() )
        $search .=" AND 0=1 ";

    return $search;
}, 10, 2 );

I'm not sure if this is an intended bug or just a bug that was never anticipated, but it is definitely a flaw in design.

This behavior exists in the following cases that I took note of before

  • Setting an empty array to post__in returns all posts

  • Passing an invalid term to a tax_query or using the name field with a name with special characters or more than one word removes the join clause from the SQL query which also results in all posts being returned. I did an answer regarding this issue

So what happens here is that when we pass a valid string to our search function, the WHERE clause is altered to include our search terms. Normally this is how the WHERE clause looks when we enter a search term called search

AND (((wp_posts.post_title LIKE \'%search%\') 
OR (wp_posts.post_content LIKE \'%search%\'))) 
AND wp_posts.post_type IN (\'post\', \'page\', \'attachment\', \'information\', \'event_type\', \'cameras\') 
AND (wp_posts.post_status = \'publish\' 
OR wp_posts.post_author = 1 
AND wp_posts.post_status = \'private\')

When we pass an empty string, the search clause is removed from the WHERE clause which causes all posts to be returned. This is how the WHERE clause looks like when we pass an empty string

AND wp_posts.post_type IN (\'post\', \'page\', \'attachment\', \'information\', \'event_type\', \'cameras\') 
AND (wp_posts.post_status = \'publish\' OR wp_posts.post_author = 1 
AND wp_posts.post_status = \'private\')

This is the section in WP_Query which is responsible for this

if ( ! empty( $q['s'] ) ) {
    $search = $this->parse_search( $q );
}

The easiest way to get out of this is to return a 404 whenever we pass an empty string as search terms. For this we need to check if we have a valid search string or not, and then set a 404 according to that. You can try the following

add_action( 'pre_get_posts', function ( $q )
{
    if(    !is_admin() // Only target the front end
        && $q->is_main_query() // Only target the main query
        && $q->is_search() // Only target the search page
    ) {
        // Get the search terms
        $search_terms = $q->get( 's' );

        // Set a 404 if s is empty
        if ( !$search_terms ) {
            add_action( 'wp', function () use ( $q )
            {
                $q->set_404();
                status_header(404);
                nocache_headers();
            });
        }
    }
});

Tags:

Search