WordPress Rest Points

There are two ways to create a REST interface in WordPress:

  1. Create a new post type
    This is not something I have experimented much with. Essentially it requires two steps:

    • register_post_type
    • register_rest_field

    From a certain point of view, this is the most natural way to interact in WordPress, since everything is a post, and it’s therefore natural to just use them for a rest interface.

  2. Create a custom routing using register_rest_route. This is useful especially for json-rpc api or in all cases where the resource is not maintained in WordPress (e.g. a facade to some other service)

The latter is the one I had some experience with, and I can write something about :)

Working with rest routes

Documentation is quite comprehensive, but at the same time is not completely up do date, and there are few inconsistencies.
In particular, the authentication callback is now always required: I did not add in my own custom routing, and then another test started crashing, without any useful hint in the error message. Most documentation does not say that it is now required, so I did not add in the initial version. Sidebar: eventually, the best way to debug the issue was to use curl and issue calls to the API. Surprisingly, curl is an excellent tool for HTTP debugging!

A typical route is then defined in this way:

function register_mail_routes() {
  register_rest_route(
    'namespace/v1',
    '/route',
    array(
      'methods'             => 'POST',
      'callback'            => 'function_handling_the_route',
      'permission_callback' => 'function_handling_the_authentication',
    )
  );
}
add_action( 'rest_api_init', 'register_mail_routes' );

function function_handling_the_route( $data ) {
}

function function_handling_the_authentication() {
  return true;
}

Some notes:

  • The rest_api_init hook is the standard extension point in WordPress for registering new routes. It accepts a function (as usual, as a string) with no parameters, which will be in charge of register new routes.
  • register_rest_route accepts the namespace (usually with version!), then the route name, and then an array with all parameters; the required are method, callback and permission.
  • callback is a normal function accepting one parameter, which is an objects with all the request details, and returning a json
  • permission callback is a function without parameters, and returning a boolean value – true if the caller should be allowed to access the api.

The actual url of the api might be differnt than what specified in the route request: if url rewrite is disabled, then it will be embedded as parameter, e.g. https://localhost/?rest_route=/namespace/v1/route. This will also have an impact on the other parameters, as the separator between the url and the first GET parameter will be & instead of ?.

Parameters

The $data object will hold all parameters passed to the function – be them GET, POST, or even embedded in the url.

For instance, the two different ways to access a resource:

function register_mail_routes() {
  register_rest_route(
    'namespace/v1',
    '/route/(?P<resource_id>[a-f0-9]+)',
    array(
      'methods'             => 'GET',
      'callback'            => 'function_handling_the_route',
      'permission_callback' => 'function_handling_the_authentication',
    )
  );
  register_rest_route(
    'namespace/v2',
    '/route',
    array(
      'methods'             => 'GET',
      'callback'            => 'function_handling_the_route',
      'permission_callback' => 'function_handling_the_authentication',
    )
  );
}

function function_handling_the_route( $data ) {
  $resource_id = $data->get_param( 'resource_id' );
  return rest_ensure_response( [ 'id' => $resource_id ] );
}

function function_handling_the_authentication() {
  return true;
}

will work both, with the same handler, and just require different way to call them:

curl -X GET -i 'https://localhost/namespace/v1/route/107533ccdbffaa5a'
curl -X GET -i 'https://localhost/namespace/v2/route?resource_id=107533ccdbffaa5a'

(the same can also apply to POST requests, but that was more understandable)

Authentication

Here, it gets even more complex; as a general idea you want to check the capabilities of the current user, but getting the current user is hard.

The documentation about authentication does tell the story; still it is a bit confusing.

WordPress uses cookies for authentication; to use them in curl, you need to export them from your browser (e.g. using export cookies extension for firefox), and then using them in curl, like:

curl -b ./cookies-localhost.txt -X GET -i 'https://localhost/namespace/v1/route/107533ccdbffaa5a'

Unfortunately,, this approach will still not work: problem is that if there is no nonce, cookie auth will laways fail and give user 0 (unauthenticated).
Note that the problem will not occur when calling the API from javascript in WordPress; because the js API will already take care about setting the correct cookies and nonce to the request. However, for testing purposes it is important to be able to use a command-line client like curl, and here the problem is almost unsolvable.

I then tried to solve the issue using Tried with wp_create_nonce but it ended in nothing, and I stopped investigating as the solution was only important for testing – again, on production this is already managed by the javascript api.

Eventually, I settled up using basic auth plugin (which should be enabled only on development box!) that allowed me to use just basic authentication with curl:

curl -X GET -i 'https://localhost/namespace/v1/route/107533ccdbffaa5a' --user admin:password

Once we are able to access the user, then we can finally check for permissions.
The usual way is to check capabilities, using current_user_can; ideally using some ad-hoc capability, but it can be also one of the already available:


  function function_handling_the_authentication() {
  return current_user_can( 'edit_dashboard' );
  }

Testing

Generally speaking, WordPress has its own extensions to PHPUnit, which will manage the state before and after tests, and will also add some utilities.
They are available only on WordPress’ svn repository, though, and I am not sure about their status; I used them and they were working, but it was on an environment already fully setup and configured.

Anyways, once that’s set up, it’s enough to derive the test class from WP_Test_REST_TestCase to ensure that the database is correctly set up, and an assertErrorResponse additional assertion type. There are also other test classes available, but I did not use them.

Side note: options

As a bonus when using WordPress, there is the possibility to easily create configuration pages, using the Settings API.
“Easily” might be a bit of a stretch; however, it was definitely quite a quick way to get through and store options in the database.

How I did it:

  • Generate code using an online generator
  • Simplify and adapt the code – especially, remove usage of a class

Overall, the simplified code was quite small; the most difficult part was tracking down the relationships between settings, sections, and fields, and the parameters passed to those functions. Documentation is – as usual – a bit old and lacking, but with a couple of hours max it is possible to arrive to a reasonable result; and then the configuration option will be available in the whole WordPress site, including the rest routes.