A WordPress ajax handler for custom themes

Something I have been noodling on is a better way to handle ajax requests in my custom themes. Seems to me that a relatively complex theme ends up with a lot of add_action calls for custom ajax handlers, and this could be simplified/reduced. Every time a new endpoint is required we have to add two new add_action calls to our theme. Maybe a better approach is to write a single ajax endpoint that will route requests to the proper classes/methods?

The goal would be that all ajax requests are run through our custom ajax handler and routed to the appropriate controller & method for execution. This method allows us to encapsulate functionality into separate classes/modules instead of cluttering functions.php with ajax functions. Potentially this makes the code more reusable.

With some sane configuration I think that this could be a good way to build a flexible ajax interface into your theme. To protect against malicious calls classes are namespaced (\Ecs\Modules\), and the methods are prefixed (ajax*). This should stop most attempts to execute arbitrary theme code. There are issues though. There would be a fatal error if the class does not exist which could expose information about the environment. We have to make sure that the $_REQUEST params are well sanitized. This would need to be scrutinized and tested for security issues. We don't want someone to be able to craft a request to execute code we don't explicitly want executed.

Here is an example structure in a hypothetical Theme class.

class Theme
{

    public function __construct()
    {
        ... snip ...

        add_action('wp_ajax_ecs_ajax', array(&$this, 'executeAjax'));
        add_action('wp_ajax_nopriv_ecs_ajax', array(&$this, 'executeAjax'));
    }

    ... snip ...

    /**
     * Simple interface for executing ajax requests
     *
     * Usage: /wp-admin/admin-ajax.php?action=ecs_ajax&c=CLASS&m=METHOD&_wpnonce=NONCE
     *
     * Params for ajax request:
     * c         = class to instantiate
     * m         = method to run
     * _wpnonce  = WordPress Nonce
     * display   = json,html
     *
     * Naming Conventions
     * Method names will be prefixed with "ajax_" and then run through the Inflector to camelize it
     *     - eg: "doThing" would become "ajaxDoThing", so you need a method in your class called "ajaxDoThing"
     *
     * Classes can be whatever you want. They are expected to be namespaces to \Ecs\Modules
     *
     * Output can be rendered as JSON, or HTML
     *
     * Generate a nonce: wp_create_nonce('execute_ajax_nonce');
     */
    public function executeAjax()
    {
        try {
            // We expect a valid wp nonce
            if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce($_REQUEST['_wpnonce'], 'execute_ajax_nonce')) {
                throw new \Exception('Invalid ajax request');
            }

            // Make sure we have a class and a method to execute
            if (!isset($_REQUEST['c']) && !isset($_REQUEST['m'])) {
                throw new \Exception('Invalid params in ajax request');
            }

            // Make sure that the requested class exists and instantiate it
            $c = filter_var($_REQUEST['c'], FILTER_SANITIZE_STRING);
            $class = "\Ecs\\Modules\\$c";

            if (!class_exists($class)) {
                throw new \Exception('Class does not exist');
            }

            $Obj = new $class();

            // Add our prefix and camelize the requested method
            // eg: "method" becomes "ajaxMethod"
            // eg: "do_thing" becomes "ajaxDoThing", or "doThing" becomes "ajaxDoThing"
            $Inflector = new \Ecs\Core\Inflector();
            $m = $Inflector->camelize('ajax_' . filter_var($_REQUEST['m'], FILTER_SANITIZE_STRING));

            // Make sure that the requested method exists in our object
            if (!method_exists($Obj, $m)) {
                throw new \Exception('Ajax method does not exist');
            }

            // Execute
            $result = $Obj->$m();

            // Render the response
            \Ecs\Helpers\json_response($result);

        } catch (\Exception $e) {
            \Ecs\Helpers\json_response(array('error' => $e->getMessage()));
        }

        // Make sure this thing dies so it never echoes back that damn zero.
        die();
    }
}
 

Did you like this post? Let me know by sending me a message. Is there a topic you would like me to cover? Let me know about that too. I look forward to hearing from you!

Let's Connect!