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.
*
* @since 2.1.0
* @see WP_Rewrite::add_endpoint()
* @global object $wp_rewrite
*
* @param string $name Name of the endpoint.
* @param int $places Endpoint mask describing the places the endpoint should be added.
*/
function add_rewrite_endpoint( $name, $places ) {
global $wp_rewrite;
$wp_rewrite->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 {@link add_rewrite_endpoint()} for full documentation.
*
* @see add_rewrite_endpoint()
* @since 2.1.0
* @access public
* @uses WP::add_query_var()
*
* @param string $name Name of the endpoint.
* @param int $places Endpoint mask describing the places the endpoint should be added.
*/
function add_endpoint($name, $places) {
global $wp;
$this->endpoints[] = array ( $places, $name );
$wp->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.
Jane Wells 11:40 am on December 28, 2012 Permalink |
I’ll start off by listing stats similar to the ones suggested for themes:
Ipstenu (Mika Epstein) 3:41 pm on December 28, 2012 Permalink |
Length of time from plugin submission to approval is averaging just around 48 hours, for a complete, fully working, plugin with a readme and no guideline violations (which is what ‘directory rules’ are). Once we get into people whom we push back, it’s as much up to their ability to reply to emails within 7 days as our ability to sort through the email
(holidays and weekends and ZOMG! busy! change that, ut we’re pretty good).
We’d need a way better way to track why a plugin was closed for the last four. Right now we have to document manually.
Jane Wells 4:44 pm on December 28, 2012 Permalink |
“This is brainstorming… don’t think about APIs or if/how it could be collected, just throw out ideas in the comments of what information you think it would great to start seeing”
In other words, don’t worry about how it could or couldn’t be done, that’s a different conversation.
Marcus 1:57 pm on December 28, 2012 Permalink |
Number of plugins “compatible” with latest version(s) of WP
Ipstenu (Mika Epstein) 3:42 pm on December 28, 2012 Permalink |
Marcus – the problem there is we don’t test them after submission, so it’s up to the developer to remember to update their readmes. And the lack of an update doesn’t mean the plugin isn’t compatible. That distinctions way too wibbly-wobbley to rely on.
Jane Wells 4:44 pm on December 28, 2012 Permalink |
I think Marcus’s suggestion is a good one. At the very least, gathering the stats on which ones say they’re compatible to which version will be useful.
Marcus 1:12 pm on December 29, 2012 Permalink |
True, but that’s why I used quotes when saying “compatible”
Agreed it’s not perfect, in my case for example I do have some plugins that aren’t marked as compatible for the latest version (haven’t had time to update readmes), yet they are.
I think it’d still be nice to know because it is still somewhat of an indicator of what plugins are getting updated for latest WP updates.
I’d say another bit of data that could be use is the Works/Doesn’t work, but then this info isn’t that reliable either I’ve found.
Charleston Software Associates 3:37 pm on December 28, 2012 Permalink |
Plugin aging report = number of plugins in these groups: updated 0-30 days ago, 30-90 days, 90-180, 180-365, 1y+. Provides a general “age” of the plugin repository at several strata.
Is the plan to publish this for the general public somewhere near the plugins home page? Some of these metrics would be nice to know for site developers & plugin authors.
Jane Wells 4:46 pm on December 28, 2012 Permalink |
There’s no plan yet, since none of these stats are being collected yet. Eventually I’d like to be able to post nice monthly stats reports on the wordpress.org blog, and team-specific stats could also live in the team site and the public sections of wordpress.org. First we need to decide what information is worth having, then figure out how/if we can gather it, THEN decide where it gets published.
Pippin (mordauk) 11:32 pm on December 28, 2012 Permalink |
Number of abandoned plugins (ones without updates for 2 years).
Number of plugins with over xxx downloads.