Rewrite endpoints API

During the development of WordPress 3.4 I have spent some time working on improving the rewrite API. One of the tickets this involved was #16303: “Improve documentation and usability of WP_Rewrite Endpoint support”. Endpoints are a really cool feature of the rewrite API, but unfortunately also little known and misunderstood. So, with this post my aim is to get more plugin developers to read and understand the new and improved endpoint documentation.

What are endpoints?

Using endpoints allows you to easily create rewrite rules to catch the normal WordPress URLs, but with a little extra at the end. For example, you could use an endpoint to match all post URLs followed by “gallery” and display all of the images used in a post, e.g. http://example.com/my-fantastic-post/gallery/.

A simple case like this is relatively easy to achieve with your own custom rewrite rules. However, the power of endpoints shines for more complex situations. What if you wanted to recognise URLs for posts and pages ending with “gallery”? What if you wanted to be able to catch multiple different archive URLs, e.g. day, month, year and category archives, with “xml” appended in order to output an XML representation of the archive? For these situations endpoints are very useful as they allow you to add a string to the end of multiple rewrite structures with a single function call.

How to use them

There is one function for interacting with endpoints: add_rewrite_endpoint(). It takes two parameters $name and $places.

$name is a string and is, wait for it… the name of the endpoint. $name is what is used in the URL and is the name of the query variable that the endpoint URL will be rewritten to. For example, an endpoint named “print” added to post permalinks would use a URL like http://example.com/my-awesome-post/print/.

$places is an integer value which represents the locations (places) to which the endpoint will be added, e.g. posts, pages or year achives. To understand $places you need to learn about the endpoint mask constants.

In wp-includes/rewrite.php (browse wp-includes/rewrite.php on Trac) a number of constants are defined all with names beginning with “EP_”:

define('EP_NONE', 0);        // 0000000000000
define('EP_PERMALINK', 1);   // 0000000000001
define('EP_ATTACHMENT', 2);  // 0000000000010
define('EP_DATE', 4);        // 0000000000100
define('EP_YEAR', 8);        // 0000000001000
// ...
define('EP_PAGES', 4096);    // 1000000000000
define('EP_ALL', 8191);      // 1111111111111

These are the endpoint masks which describe sets of URLs; post permalinks are described by EP_PERMALINK, year archives are EP_YEAR, etc. They should be thought of in terms of their binary values (see the comment I’ve added to the end of each line). Every EP_* mask, except for EP_ALL, is a different power of two and so has a different bit set to one. This allows us to build up combinations of endpoint masks by using the bitwise OR operator:

// all posts or attachments
EP_PERMALINK | EP_ATTACHMENT // 0000000000011

// all full dates (yyyy/mm/dd), years or pages
EP_DATE | EP_YEAR | EP_PAGES // 1000000001100

$places should also be thought of as a binary number. It should be set to one of the EP_* constants or a combination of them using the bitwise OR operator. If we wanted to add our endpoint to all post permalinks we would use EP_PERMALINK. For both posts and pages: EP_PERMALINK | EP_PAGES. For posts, pages, and categories: EP_PERMALINK | EP_PAGES | EP_CATEGORIES. There is also a special value to add an endpoint to all URLs that support endpoints: EP_ALL.

NB: The values of the EP_* constants are not guaranteed to stay the same which is why you must say $places = EP_PERMALINK and not $places = 2. This is particularly important for EP_ALL which will change every time a new endpoint mask is added.

It’s time to put this information into practise. The running example will be a plugin that adds JSON representations of our content using a new rewrite endpoint called “json”. So, the goal is to get URLs such as http://example.com/about/json/ to return a JSON response that gives information about the “about” page. To add the “json” endpoint to post and page rewrite structures:

add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES );

This is called in a function hooked into the init action:

function makeplugins_add_json_endpoint() {
    add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES );
}
add_action( 'init', 'makeplugins_add_json_endpoint' );

Now we want to act on requests for JSON content. This is done by hooking into template_redirect. We want to detect appropriate requests and include our custom template for serving up posts and pages in JSON format:

function makeplugins_json_template_redirect() {
    global $wp_query;

    // if this is not a request for json or a singular object then bail
    if ( ! isset( $wp_query->query_vars['json'] ) || ! is_singular() )
        return;

    // include custom template
    include dirname( __FILE__ ) . '/json-template.php';
    exit;
}
add_action( 'template_redirect', 'makeplugins_json_template_redirect' );

And we’re done. For a full example plugin see https://gist.github.com/2891111.

How do they work?

The best way to understand how anything works is to take a look at the source, so let’s do that. Endpoints are added with the add_rewrite_endpoint() function in wp-includes/rewrite.php:

/**
 * Add an endpoint, like /trackback/.
 *
 * Adding an endpoint creates extra rewrite rules for each of the matching
 * places specified by the provided bitmask. For example:
 *
 * <code>
 * add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES );
 * </code>
 *
 * will add a new rewrite rule ending with "json(/(.*))?/?$" for every permastruct
 * that describes a permalink (post) or page. This is rewritten to "json=$match"
 * where $match is the part of the URL matched by the endpoint regex (e.g. "foo" in
 * "/json/foo/").
 *
 * A new query var with the same name as the endpoint will also be created.
 *
 * When specifying $places ensure that you are using the EP_* constants (or a
 * combination of them using the bitwise OR operator) as their values are not
 * guaranteed to remain static (especially EP_ALL).
 *
 * Be sure to flush the rewrite rules - flush_rewrite_rules() - when your plugin gets
 * activated and deactivated.
 *
 * <a href='http://profiles.wordpress.org/since' class='mention'>@since</a> 2.1.0
 * <a href='http://profiles.wordpress.org/see' class='mention'>@see</a> WP_Rewrite::add_endpoint()
 * <a href='http://profiles.wordpress.org/global' class='mention'>@global</a> object $wp_rewrite
 *
 * <a href='http://profiles.wordpress.org/param' class='mention'>@param</a> string $name Name of the endpoint.
 * <a href='http://profiles.wordpress.org/param' class='mention'>@param</a> int $places Endpoint mask describing the places the endpoint should be added.
 */
function add_rewrite_endpoint( $name, $places ) {
      global $wp_rewrite;
      $wp_rewrite-&gt;add_endpoint( $name, $places );
}

So this is just a wrapper for the add_endpoint() method of the WP_Rewrite class. Although the (excellent!) documentation gives us some clues as to what it does we’ll have to dig deeper to find the how:

/**
 * Add an endpoint, like /trackback/.
 *
 * See {<a href='http://profiles.wordpress.org/link' class='mention'>@link</a> add_rewrite_endpoint()} for full documentation.
 *
 * <a href='http://profiles.wordpress.org/see' class='mention'>@see</a> add_rewrite_endpoint()
 * <a href='http://profiles.wordpress.org/since' class='mention'>@since</a> 2.1.0
 * <a href='http://profiles.wordpress.org/access' class='mention'>@access</a> public
 * <a href='http://profiles.wordpress.org/uses' class='mention'>@uses</a> WP::add_query_var()
 *
 * <a href='http://profiles.wordpress.org/param' class='mention'>@param</a> string $name Name of the endpoint.
 * <a href='http://profiles.wordpress.org/param' class='mention'>@param</a> int $places Endpoint mask describing the places the endpoint should be added.
 */
function add_endpoint($name, $places) {
    global $wp;
    $this-&gt;endpoints[] = array ( $places, $name );
    $wp-&gt;add_query_var($name);
}

Another very short and simple function. All it does is append the two parameters passed to it to the private $endpoints property of the WP_Rewrite class and also add a new query variable using WP::add_query_var().

Okay, so that’s still not useful for a full understanding of endpoints. All we know is that the arguments you pass to add_rewrite_endpoint() are stored in a private array of the $wp_rewrite global. To find out more we’ll have to search wp-includes/rewrite.php for “>endpoints” (i.e. code accessing the WP_Rewrite::$endpoints property). There are only three references to this: WP_Rewrite::add_endpoint() we have seen, WP_Rewrite::init() is boring (initialising the array), and the third is WP_Rewrite::generate_rewrite_rules():

$ep_query_append = array ();
foreach ( (array) $this->endpoints as $endpoint) {
    //match everything after the endpoint name, but allow for nothing to appear there
    $epmatch = $endpoint[1] . '(/(.*))?/?$';
    //this will be appended on to the rest of the query for each dir
    $epquery = '&' . $endpoint[1] . '=';
    $ep_query_append[$epmatch] = array ( $endpoint[0], $epquery );
}

// ... a lot of code removed ...

foreach ( (array) $ep_query_append as $regex => $ep) {
    //add the endpoints on if the mask fits
    if ( $ep[0] & $ep_mask || $ep[0] & $ep_mask_specific )
        $rewrite[$match . $regex] = $index . '?' . $query . $ep[1] . $this->preg_index($num_toks + 2);
}

In the code above the first foreach is looping through the defined endpoints and building a new array called $ep_query_append. This new array uses regular expressions that match a specific endpoint as keys and the values are the endpoint $places and $epquery which is a partial query string to append to a full query. So, for our JSON endpoint example we would get:

$ep_query_append[ 'json(/(.*))?/?$' ] = array( EP_PERMALINK | EP_PAGES, '&json=' );

The second loop generates the final rewrite rules for our endpoint. It loops through $ep_query_append checking if the current permastructure being generated has an endpoint mask, $ep_mask, that matches any of the endpoints. If the bitwise AND produces a non-zero value then there’s a match and the endpoint rewrite rules should be added to this permastructure.

For our JSON example, if WP_Rewrite::generate_rewrites_rules() has been called for the posts permalink structure then $ep_mask = EP_PERMALINK and $ep[0] = EP_PERMALINK | EP_PAGES. The bitwise AND of these values produces 1, therefore a new entry is added to $rewrite. Assuming that the post permalink structure is “/%postname%/” it would look something like:

$rewrite[ '([^/]+)/json(/(.*))?/?$' ] = 'index.php?name=$1&json=$3'

This is the final rewrite rule for our JSON endpoint applied to post permalinks. It matches a request for “post-slug/json/” and sets up the appropriate query variables “name” and “json”. Our template_redirect hook now picks this up and produces the required response.

And you made it to the end, phew! Time for a drink…

Conclusion

I hope that after all of that you understand how to use endpoints and how they work. If you have any questions please don’t hesitate to ask them in the comments. Always remember that the best way to understand a function is to look at the source and follow its execution.