WordPress Plugin Pet Peeve #2: Direct Calls to Plugin Files

This is actually very similar to my first pet peeve of hardcoding the path to wp-content, in that it makes assumptions about where files are placed on the filesystem. Oftentimes, plugins need to handle certain kinds of requests, maybe for some specific protocol, or to handle an AJAX request. Some plugins will do this by making an HTTP request directly to one of the files in the plugin… something like:

echo '<script type="text/javascript">
    jQuery.get("' . plugins_url('my-plugin/ajax-handler.php') . '");
    // do something with AJAX data ...

This is not a problem in and of itself, in fact it’s great that the plugin developer is actually using the plugins_url function! The problem arises in the my-plugin/ajax-handler.php file itself.

If that plugin needs to make use of any WordPress data or functions, then the developer is certain to do something very ugly. You’ll know it when you see it:


// or sometimes you'll see...

So what is this, and why is it so bad? Well, the wp-load.php is a file provided by WordPress core that bootstraps the WordPress environment. It loads in the wp-config.php file, loads all the common WordPress functions and classes, initializes the database connection, and gets everything in place to process the request. These are all important things, and provides a lot of functionality that the plugin developer may need. The problem is that in the require_once call above, the plugin developer is assuming that the wp-load.php file is in a directory exactly three levels up in the filesystem from their plugin directory. In a standard WordPress deployment, this will be true and everything will work fine (for the most part). But as we’ve already seen, WordPress allows deployers to move where their wp-content or their plugins directory lives, so the above code will break completely. Yes, there are files in WordPress core that do something similar to the above, for example wp-admin/admin.php. But these are safe for WordPress core files because they do know where certain files will be. It is never safe to make this assumption from a plugin. I think it’s pretty safe to say that if you have a require or include call that goes up more than a couple of directories ("../../"), you’re almost certainly doing it wrong.

A couple of points to emphasize before moving on: if your plugin files are not accessing any built-in WordPress functions, classes, or data, calling them directly should be just fine. Also, if you’re doing AJAX calls on admin pages, use the built-in functionality WordPress core provides.

The Right Way

So the right way of doing this is actually pretty straightforward conceptually, but in practice there are a couple of ways to do it depending on your needs. The short answer is that instead of calling your plugin file directly, you must send the request through the standard WordPress request handling mechanisms. This requires two things: constructing your request properly, and then hooking into the WordPress request handling code at the appropriate time to take over the request handling yourself.

All standard WordPress requests eventually get broken down to a request to /index.php with a bunch of URL parameters. For example, when someone goes to your blog post at


WordPress uses the configured permalink structure to break this down to


And from that, WordPress can determine which post the request is for, and serve that up accordingly. So what we want to do is be able to construct a request along the lines of


and then hook into the WordPress request handling logic so that we can process this request ourselves. Fortunately, WordPress provides an action hook for exactly that purpose, called process_request. Functions that hook into this action are passed one parameter, an instance of the WP class, which encapsulates most of the specific parameters for the current request. If we want to process only the requests which include my-plugin=ajax-handler, we would add something like this to the plugin:

function my_plugin_parse_request($wp) {
    // only process requests with "my-plugin=ajax-handler"
    if (array_key_exists('my-plugin', $wp->query_vars)
            && $wp->query_vars['my-plugin'] == 'ajax-handler') {

        // process the request.
        // For now, we'll just call wp_die, so we know it got processed
        wp_die('my-plugin ajax-handler!');
add_action('parse_request', 'my_plugin_parse_request');

If you’re following along at home, try accessing http://example.com/index.php?my-plugin=ajax-handler. What happens? Most likely, nothing at all… it still loads your normal blog index page. So what went wrong? In order to have the WordPress request handling code process custom URL parameters, we have to register them. This is necessary for a number of reasons including security, performance, and also to ensure that WordPress doesn’t accidentally process something it wasn’t meant to. Fortunately, registering a new query variable is very simple using the query_vars filter hook:

function my_plugin_query_vars($vars) {
    $vars[] = 'my-plugin';
    return $vars;
add_filter('query_vars', 'my_plugin_query_vars');

This simply registers ‘my-plugin’ as a valid query variable to be processed by WordPress, but says nothing about valid values for that variable. So now try reloading the page, and you should be greeted with a simple styled page reading “my-plugin ajax-handler!”. Now you just need to modify the my_plugin_parse_request function to actually do your custom request logic, and you’re good to go.

I mentioned earlier that there are a couple of ways to do this. The one caveat to be aware of with the above approach is that you are hooking into the WordPress request processing logic before WordPress has processed the query itself. WordPress’s query handling logic is responsible for examining the request and figuring out what page or post within WordPress the request maps to. This is also what makes the is_* functions work: is_page, is_front_page, is_feed, etc. If your plugin is actually doing things on normal WordPress page requests and needs access to these functions, then instead of hooking into the parse_request action, you should use the wp action. You are still passed an instance of the WP class, so all you need to modify is the add_action call:

add_action('wp', 'my_plugin_parse_request');

I would recommend that you not make this change unless you know that you need to. Otherwise, you’re having WordPress perform a lot of logic that isn’t necessary (including hits against the database), potentially making the request take longer than it needs to be. Though the difference is likely to be imperceptible, it’s just good practice to keep things as fast as possible. And when dealing with custom request handling, you can keep things fast by hooking into as soon in the flow as possible.

Additional Exercise for the reader

If you don’t like the look of URL containing index.php?my-plugin=ajax-handler, and instead want something pretty like


that is certainly possible with just a little bit more work (see WP_Rewrite). The WordPress OpenID Plugin does this to achieve nice endpoint URLs for the OpenID authentication protocol. A word of caution with this however: if you want to make sure that your plugin works on the widest number of deployments, where you have no idea what their permalink structure is going to be, there is a bit more work that is necessary. You can see the rewrite rules I setup for the OpenID plugin in the common.php file. There’s a lot of other stuff in there, and the rewrite code is a little confusing at parts, but it’s all there. I will actually be changing this in the future to simplify things a bit, so avoid complicating matters unless there is a real reason to.

Further codex reading:

Comments and responses

Have you written a response to this? Let me know the URL:

Thank you so much Will! I´ve been googleing up and down but this was the exact answer I needed. Very well written and understandable. It´s a gold nugget & saved me heaps of time.

I used the process while writing a shop-plugin fpr wordpress that implements onpage price calculation for the variations of a product (via ajax). Since it all has to come within one plugin, including the ajax-handler that needs to do requests to the normal wp-functions, your post described exactly what I had to do.

Wouldn´t have known otherwise. Thx again!

This is a fantastic article. Next chance I get, I'm going to review my plugin code with this information in mind.

Okay, I got to admit I've been doing this dirty trick on my plugin for a while now. After reading this, I've converted to using HOOK to "parse_request".

But there comes errors, My plugin uses wp_mail() and wp_verify_nonce()! Both failed as Undefined functions? so I try changing the parse_request HOOK to 'wp'. Same result, I guess this method have its limit!

both wp_mail and wp_verify_nonce are defined in pluggable.php which is loaded before either of those hooks. If you're seeing an error about the functions being undefined, it must be caused by something else.
[...] be the solution for my problems and in the same second I spin my ideas he linked to an article from Will Norris. I never ever get the same idea since I wrote my first plugin, it’s really simple to realize [...]


Great post however I am not really clear on how to deal with the : require_once('../../../wp-config.php');

I need to use the get_option function to get the site url which I then use in an ajax call : jQuery.post("/wp-admin/admin-ajax.php"...

I have been through the methods you suggest but I don't really understand. The codex describes the method to perform Ajax calls on the admin and frontend sides. You need to include the wp-config.php to get the site url... I fully agree on the fact that this may cause some problems if the installation uses non standard paths but I don't get from your note the way to deal with that... I should have missed something...

Thank you Will for this great post and thanks to Alex that posted about it so I could find it! I have been delving deeper and deeper under the hood of WP lately and it is great to find articles that are so clear and to the point. It is sometimes hard to see the forest for all the trees...

Olivier: the only way you would need something like require_once('../../../wp-config.php') in your code is if you are calling your plugin files directly, and they need access to WP functions. This is really bad, you never want to do this.

So instead of calling your plugin file directly, and having it try to bootstrap the WP environment, what you want to do is actually send the request through WordPress (so the environment gets setup properly) and then have the requested passed off to your plugin to process it. You accomplish the same thing, but this is the Right Way to do it.

The AJAX in Plugins Codex page suggests that you can still use /wp-admin/admin-ajax.php to process user facing AJAX calls. I was thinking that wasn't possibly, but I certainly could be wrong... might be worth looking into.

[...] Removed hard coded reference to config.php, routing Facebook comment submission through WordPress’ built in query parser instead (See http://willnorris.com/2009/06/wordpress-plugin-pet-peeve-2-direct-calls-to-plugin-files) [...]

Thanks for this great series, Will - I've updated WPBook to comply with them (I think!).

When constructing in code the full url that will be used (example.com/index.php in your example), is it better to use get_bloginfo('wpurl') or get_bloginfo('home')?

That is, do I need to point at index.php in the directory where WordPress is installed or the index.php where they set their home url to be, in cases where they've moved WordPress into a subdirectory?

[...] Using jQuery to Query Featured Images To achieve these goals, I began coding the front-end using jQuery. Then for my AJAX calls, I added code to my theme’s functions.php so that WordPress could receive my query requests directly. This meant my AJAX calls could reference the theme’s main URL (e.g. http://example.com?action=getimage) rather than referencing some additional PHP file setup to specifically handle AJAX requests (for more info on this, check out this post by Will Norris). [...]