Proposal - Securing the WordPress JSON API

I was recently added to the WordPress API team and this post contains my thoughts about the recent authentication discussion.

WordPress have a reasonably robust authentication system built in, the username and password system and it would be possible to use it along with Basic Auth to allow for API authentication. Please forgive any typos in advance; this was long and I didn’t really have the time to fully proof it.

Authentication, Identity and Authorization

While Authentication is very important there is also Authorization to consider. Here’s a nice blog post from Apigee on the difference between three (3) terms: Identity, Authentication and Authorization (IMO Apigee are the leading experts on web API design at the moment). In a nutshell here’s what they terms mean:

The Term What it Means
Identity Who is making the request?
Authentication Are they really who they say they are?
Authorization Are they allowed to do what they are trying to do?

And as they point out we may not need them all but what we need is the point of this post.

As a side note they say "Take Twitter’s API; open for looking up public information about a user, but other operations require authentication." What this says to me is an API key would be ideal for most read activities but most write activities should require Authentication.

Authorization without Authentication

As much as we need Authentication I think we need Authorization even more. There are some API actions we’ll happily allow anyone to do such as download the list of our most popular posts and we don’t need to authenticate for that, we only need to authorize.

Why authorize? Why not just allow open access? So we can track who we authorized in case, for example we need to rate-limit their usage or even revoke their access.

About SSL

Let me get this out of the way sooner than later. Anything that requires SSL is a non-starter just as requiring PHP 5.3 for WordPress 3.7 is a non-starter. Need I say more on this point?

However we could allow support for SSL, assuming that for what we implement the SSL and non-SSL solutions are compatible.

Mainstream Options for API Security

Let’s discus the variety of methods for securing an API; some mainstream and some a bit esoteric. Bottom line is that most informed people seem to say "Don’t role your own." So with that in mind I believe we have these options:

Option Discussion
OAuth 2 Generally considered the best balanced security option for mainstream web apps where security and ease of interaction for users is balanced. But can be complex to implement, especially on the client end, and requires SSL to be secure.
Basic Auth Not as good as OAuth 2 but super easy for the client to implement OTOH it is not secure unless SSL is used.
Digest Auth More secure than Basic Auth but still not fully secure. Quite a pain for the client to implement..
Amazon Auth Well-tested and doesn’t require SSL but is non-standard (ignoring "defacto-" standards) and still requires an API key.
API Keys Very simple for the client to implement and as secure the Capabilities tied to the API key, i.e. if it can only see public data and not update then it’s "secure enough". Fully secure if used with SSL. Assuming users can’t change passwords with the API key then it’s more secure than Basic Auth because user credential are never in a position to be compromised.

(Did I miss anything?)

Given the available options it would seem to me that OAuth 2, Digest Auth and even Amazon Auth are non-starters as a requirement for use of a JSON API in WordPress core because of the complexity each of them heave onto the API client developer, at least if one of these is the option for accessing the JSON API.

Basic Auth vs. API Keys

Which leaves the unsecure Basic Auth and mildly secure API Keys. So review the pros and cons of using Basic Auth – which is tied to the WordPress user’s username and password in the current version of the JSON API – and API keys:

  Pros Cons
Basic Auth
  • Easy to implement
  • Insecure
  • If API login is compromised then user may loose their account or be made to go through the hassle of regaining access.
  • Since APIs access can be automated it’s much more likely that a hacker could capture a username/password on a non-SSL API call (calls might be made continuously) than for a user login (which comparatively happen very infrequently.)
  • Can only support one Authorization profile per user account.
  • To support multiple authorization profiles a user would need create multiple user accounts,
  • To allow another person API access they either need to share their username/password or create another user account for them.
  • If API access requires a user account some sites could go from 5-10 users to having 50,000+ users (think of smaller sites like Mashable.)
  • If multiple user accounts are required then we’ll need a way to relate user accounts and allow one user account to manage other user accounts.
API Keys
  • API authorization is decoupled from user accounts.
  • One or many API keys can be tied to a single user account.
  • If API Key is compromised user can login and deactivate it.
  • Plugins could easily deactivate API keys if they follow an abuse pattern.
  • API keys could be added with expiration dates.
  • Sites with a large number of API users do not gain an explosion of regular users.
  • Each API Key can potentially support a different Authorization Profile (example use-case: I provide on API key to a social network – the key has limited capability – and use another API key – one that can do anything my user account can do – for an official WordPress mobile app that I use to access my site.)
  • Requires what appears to be more architecture

It seems to me from this comparison that API keys are the only reasonable option for allowing JSON API access to much of WordPress. However they are only appropriate for some use-cases and not even as-is they are not as a complete solution. Let’s discuss the rest of the solution for the use-cases in which I think they apply.

It also seems to me that tying API access to users accounts could easily create an explosion of complexity and significant user experience problems as users see their logins hacked by unsecure usage and then are locked out of or even loose their blogs.

API Roles and Capabilities

One of the ways in which API Keys might be acceptable without Authentication is that some things can be made freely available holders of API keys if we add in "API Roles and Capabilities."

Just like User Roles that are assigned a collection of Capabilities we could add "API Roles" that also have "API Capabilities". These Capabilities could be used to determine the Authorization status for each (what I’ll name) an "API Service" when requested.

Note: I’m defining an "API Service" as a URL + an HTTP method (GET, POST, etc.) and I’m calling the collection of Authorizations for all API Services as a "Authorization Profile."

I’ve reviewed the code for the WP_Role, WP_Roles and WP_User classes and I think the first two could be used without modification. If so then we only introduce a WP_API_Request class. And depending on the opinion of others the WP_API_Request class could be standalone or the WP_User class could be refactored to extend from an abstract WP_Auth class thereby allowing the new WP_API_Request class to also extend from WP_Auth.

We could then decide on a convention that any Capability name prefixed with 'api_' is a capability for an API Service and we add a function current_api_request_can() or just api_request_can(). Armed with api_request_can() we could write code like the following (note that api_request_can() assumes 'api_' as a prefix and thus does not require it to be passed):

Source code example

Are We Adding Too Much Code?

Although a comment was made that "we don’t want a huge chunk of code just for authentication" I would suggest that even if it were to be a large amount of code, which I doubt there would be, it shouldn’t matter how much code we add as long as that code doesn’t require significant maintenance and more importantly does not impose significant complexity onto the admin user in terms of "more options."

  • Assume that in Settings > General we add only one (1) single checkbox with the label "Enable JSON API" which by default we leave unchecked.

  • Once the user has explicitly chosen to enable the API (the equivalent of activating the plugin we have today) a single "Tools > JSON API" option is added.

  • The Tools/JSON API admin page can use tabs to organize the information so it would not be overwhelming, if even needed.

  • To offer the user the list of API keys we can reuse/modify the Taxonomy add/edit functionality assuming we add a 'user_api_key' taxonomy to allow us to store, lookup and manage API keys related to Users who would "own" the API keys.

  • Another tab for the Tools/JSON API admin page could potentially offer the ability to add and manage API Roles and another tab for API Capabilities. Or not, we could require these be managed programmatically just like User roles currently are.

  • And finally a main tab that allows you to force SSL use, or not.

What I’ve describe above it really not that much code. Would it make sense to risk the potential downside of tying the API to username and password in order to simply avoid the code that the API keys management would require?

Handling Escalating Security Requirements

Consider the "API Services" discussed earlier; we could implement a mapping of authentication requirements to API services such that different services have different authentication/authorization requirements. Consider this table:

Requirement HTTP Methods API Services That Allows Example API Service
No API Key Required GET Access to public information with a low risk of needing a rate limiter. An API service that returns site name and other metadata. The metadata could also including a links to an API service to request an API key via API.
API Key GET Access to public information that might need to be rate limited. Return the current list of blog posts.
API Key + Nonce POST, PUT Add Content or Update Revertible Content Update of Posts, add Taxonomy Terms.
Nonce GET Add Content or Update Revertible Content Update of Posts, add Taxonomy Terms.
SSL+Basic Auth GET Returns secure information for client w/o API Key Retrieve an API key programatically.
SSL+API Key POST, PUT Updates secure information Modify User Profile, Deletes Posts.
SSL+Basic Auth POST, PUT Update highly sensitive information Change user password

API Keys + Nonces

Note that we combine nonces with API keys. One of the ways WordPress handles security is with nonces, and the API need be no different. Note that the nonce would be generated by WordPress core or a plugin for the logged in user to allow their browser’s to use the API via AJAX. These use-cases would authorize for the JSON API similar to how the current AJAX system in WordPress authorizes.

For mobile apps nonces could also be offered to last for longer, requiring a mobile device to retrieve a new nonce once every 15 minutes or so but then allowing them to just use the nonce + API key within those windows. Of course you wouldn’t want a 15 minute window for nonces used with AJAX apps

Using SSL

So if we follow the outlined approach we can provide a reasonably level of API access without requiring SSL but we can still enforce the benefit of SSL for those who are likely to have the where-with-all to upgrade to SSL.

Consider this, if they need their sensitive parts of their site updated via API then they are likely special enough that they can make sure that SSL happens. But if unexpected consequences occur and someone builds a SaaS that people want to use but that requires SSL then frankly it creates an opportunity for hosting companies to see a high level of demand for turnkey SSL setup.

And optionally we can add an 'WPAPI_ALLOW_NO_SSL' constant for those site builders and site owners with a "Devil May Care" attitude.

Summary

In summary I’m proposing for the JSON API for WordPress to:

  • Use API Keys for Authorization
    • (And if you are still not convinced, read this).
  • Incorporate API Roles and Capabilities
  • Support Escalating Authentication Requirements for API Services
  • Build Single Menu Item Admin UI for the admin to Manage the API.

Let me know your reactions in the comments below.

RESTful Web Services in a WordPress Plugin?

UPDATE (2011-04-15):

Since I wrote this post I’ve learned a tremendous amount about WordPress plugin development; so much so that I can’t overstate how much more I know today than when I wrote this post years ago.  So, while the following post might be a novelty to read, I highly recommend that you don’t use this approach.

The only way that a RESTful approach to web services in WordPress would make sense to me is if a team of rockstar WordPress plugin developers were to create a fully fleshed-out extension to WordPress that offered a complete RESTful web service implementation including one that addressed all edge cases and security concerns; only then I would consider not defaulting to the non-RESTful approach WordPress uses for AJAX.  

In summary I recommend not trying to swim upstream today and instead use the approach provided by WordPress. Who knows, maybe in the future there will be a viable method of doing RESTful web services in WordPress.


So I’ve got a project where I need to have a Flash component built in Flex to call to a WordPress blog and get information about it’s latest post. Should be no problem right?  The easy way to do this would be to just write a "rest.php" file and brute-force all the setup that WordPress does but I thought it would be so much more valuable to implement this as a plug-in.

I figured that I’d just quickly learn how to build a WordPress plug-in and create one for exposing RESTful web services; after all with a year of programming Drupal modules WordPress’ plug-in API can’t be that hard, right?  Well turns out it wasn’t that easy and I think I have run into a design limitation with WordPress and I’m beginning to wish I’d just taken the brute-force approach and said to hell with writing a plug-in.

Although I am not 100% certain, and I hope someone can point out that I’m just doing something wrong, it seems sadly like I’m pushing the edges of the WordPress API and exposing where it’s design falls short. By the way, I’m working with WordPress v2.5 because why upgrade mid-project when god knows if WordPress will release another in the remaining days before this project is done and I’ll just have to do again before deployment?

Here’s the details.  I started writing a plug-in called "RESTful Services" with a goal of implementing URLs that behave in the following fashion; {format} could potentially be html, xhtml, json, xml, rss, atom, etc.:

http://example.com/services.{format}
Provide a list of RESTful services in specified {format}, defaults to html
http://example.com/services/{service}.{format}/{data}?{params
Provide a RESTful service in specified {format}, defaults to html, with optional provided data and parameters.

But before I got all those options working I just wanted to service a page from my RESTful Services plugin where Content-Type: text/plain. I found this page that professes to explain how to hook into the URL routing and after a few fits and starts I can came up with the following code for my plugin that would indeed response to my http://example.com/services URL:

wp-content/plugins/restful-services/restful-web-services.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
add_action('init', 'restful_services_flush_rewrite_rules');
function restful_services_flush_rewrite_rules() {    
  global $wp_rewrite;
  $wp_rewrite->flush_rules();
}       
 
add_filter('generate_rewrite_rules', 'restful_services_add_rewrite_rules');
function restful_services_add_rewrite_rules( $wp_rewrite ) {      
  $new_rules = array(        
    'services' => 'wp-content/plugins/restful-services/rest.php',     
  );
  $wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
}

The problem with the above was that it wouldn’t call "wp-content/plugins/restful-services/rest.php"; it would simply continued to call "index.php" and display the home page!  After literally hours and hours of debugging with my trusty PhpEd IDE & debugger I was able to find that the code on lines 737 & 738 of "wp-includes/query.php" told WordPress that my service was the home page! It is almost seems like the "generate_rewrite_rules" was implemented as "a good idea" yet no testing has ever been done on it because for the best I can tell it doesn’t work. (Note I’ve reformatted the code to multiple lines so that it is easier to read and does not extend past the right margin of my blog):

wp-includes/query.php:

if ( !(  $this->is_singular                           
      || $this->is_archive                           
      || $this->is_search                           
      || $this->is_feed                           
      || $this->is_trackback                           
      || $this->is_404                           
      || $this->is_admin                           
      || $this->is_comments_popup ) )   
       $this->is_home = true;

I could possibly hack it to get past this by setting one of those to "true", but none of them are really appropriate; there is nothing there quite like an "is_service" instance variable. Setting something like "this->is_singular" or "this->is_feed" might work but it could manifest incompatibility problems with other plugins or future versions of WordPress. Frankly it was rather disappointing to discover this because it tells me that WordPress has hard-coded all the potential scenarios and doesn’t really have a way around it. Seems to me there should really be a hook here and the type of pages should be allowed to be expanded by plugins rather than be hardcoded as only one of ’singular’, ‘archive’, ’search’, ‘feed’, … and ‘home.’

Anyway, where this manifests itself is "wp-includes/template-loader.php" file which I have included in it’s entirety below.  It is on lines 24 and 25 where the template loader loaded the home page because it’s not possible to specify otherwise:

wp-includes/template-loader.php:

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
 * Loads the correct template based on the visitor's url
 * @package WordPress
 */
if ( defined('WP_USE_THEMES') && constant('WP_USE_THEMES') ) {
  do_action('template_redirect');
  $is_home = is_home() ;
  if ( is_robots() ) {
    do_action('do_robots');
    return;
  } else if ( is_feed() ) {
    do_feed();
    return;
  } else if ( is_trackback() ) {
    include(ABSPATH . 'wp-trackback.php');
    return;
  } else if ( is_404() && $template = get_404_template() ) {
    include($template);
    return;
  } else if ( is_search() && $template = get_search_template() ) {
    include($template);
    return;
  } else if ( is_home() && $template = get_home_template() ) {
    include($template);
    return;
  } else if ( is_attachment() && $template = get_attachment_template() ) {
    remove_filter('the_content', 'prepend_attachment');
    include($template);
    return;
  } else if ( is_single() && $template = get_single_template() ) {
    include($template);
    return;
  } else if ( is_page() && $template = get_page_template() ) {
    include($template);
    return;
  } else if ( is_category() && $template = get_category_template()) {
    include($template);
    return;
  } else if ( is_tag() && $template = get_tag_template()) {
    include($template);
    return;
  } else if ( is_tax() && $template = get_taxonomy_template()) {
    include($template);
    return;
  } else if ( is_author() && $template = get_author_template() ) {
    include($template);
    return;
  } else if ( is_date() && $template = get_date_template() ) {
    include($template);
    return;
  } else if ( is_archive() && $template = get_archive_template() ) {
    include($template);
    return;
  } else if ( is_comments_popup() && $template = get_comments_popup_template() ) {
    include($template);
    return;
  } else if ( is_paged() && $template = get_paged_template() ) {
    include($template);
    return;
  } else if ( file_exists(TEMPLATEPATH . "/index.php") ) {
    include(TEMPLATEPATH . "/index.php");
    return;
  }
} else {
  // Process feeds and trackbacks even if not using themes.
  if ( is_robots() ) {
    do_action('do_robots');
    return;
  } else if ( is_feed() ) {
    do_feed();
    return;
  } else if ( is_trackback() ) {
    include(ABSPATH . 'wp-trackback.php');
    return;
  }
}

Still another problem in this puzzle is the $wp->send_headers() method shown being called here on line 293 of "wp-includes/classes.php":

wp-includes/classes.php:

290
291
292
293
294
295
296
297
298
function main($query_args = '') {   
  $this->init();   
  $this->parse_request($query_args);   
  $this->send_headers(); 
  $this->query_posts();  
  $this->handle_404();   
  $this->register_globals();   
  do_action_ref_array('wp', array(&$this));   
}

The problem with the $wp->send_headers(), also from "wp-includes/classes.php", is that it seems to have the option of either serving an HTML content type on line 183 and 185, or a content type based on a feed (the content types for the feeds are set in their respective "wp-includes/feed-*.php" files) but no custom content types as far as I can determine as there seems to be no way to override calling this function or the logic path contained within:

wp-includes/classes.php:

175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
function send_headers() {
  @header('X-Pingback: '. get_bloginfo('pingback_url'));
  if ( is_user_logged_in() )
    nocache_headers();
  if ( !empty($this->query_vars['error']) && '404' == $this->query_vars['error'] ) {
    status_header( 404 );
    if ( !is_user_logged_in() )
      nocache_headers();
    @header('Content-Type: ' . get_option('html_type') . '; charset=' . get_option('blog_charset'));
  } else if ( empty($this->query_vars['feed']) ) {
    @header('Content-Type: ' . get_option('html_type') . '; charset=' . get_option('blog_charset'));
  } else {
    // We're showing a feed, so WP is indeed the only thing that last changed
    if ( !empty($this->query_vars['withcomments'])
      || ( empty($this->query_vars['withoutcomments'])
        && ( !empty($this->query_vars['p'])
          || !empty($this->query_vars['name'])
          || !empty($this->query_vars['page_id'])
          || !empty($this->query_vars['pagename'])
          || !empty($this->query_vars['attachment'])
          || !empty($this->query_vars['attachment_id'])
        )
      )
    )
      $wp_last_modified = mysql2date('D, d M Y H:i:s', get_lastcommentmodified('GMT'), 0).' GMT';
    else
      $wp_last_modified = mysql2date('D, d M Y H:i:s', get_lastpostmodified('GMT'), 0).' GMT';
    $wp_etag = '"' . md5($wp_last_modified) . '"';
    @header("Last-Modified: $wp_last_modified");
    @header("ETag: $wp_etag");
 
    // Support for Conditional GET
    if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
      $client_etag = stripslashes(stripslashes($_SERVER['HTTP_IF_NONE_MATCH']));
    else $client_etag = false;
 
    $client_last_modified = empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? '' : trim($_SERVER['HTTP_IF_MODIFIED_SINCE']);
    // If string is empty, return 0. If not, attempt to parse into a timestamp
    $client_modified_timestamp = $client_last_modified ? strtotime($client_last_modified) : 0;
 
    // Make a timestamp for our most recent modification...
    $wp_modified_timestamp = strtotime($wp_last_modified);
 
    if ( ($client_last_modified && $client_etag) ?
         (($client_modified_timestamp >= $wp_modified_timestamp) && ($client_etag == $wp_etag)) :
         (($client_modified_timestamp >= $wp_modified_timestamp) || ($client_etag == $wp_etag)) ) {
      status_header( 304 );
      exit;
    }
  }
 
  do_action_ref_array('send_headers', array(&$this));
}

Still, I was able to come up with a solution although it is so very hackish.  My solution was to hook the "template_redirect" action on line 7 of "wp-includes/template-loader.php" (see code from that file above.) Though it seems to works thus far, my solution just feels wrong for the following reasons:

  1. It ignores the fact that WordPress continues to think that my web service URL is the home page,
  2. It first lets "$wp->send_headers()" set the content type before it overrides it,
  3. It uses an "exit" rather than a return to keep WordPress from serving up the home page template, and
  4. It doesn’t use the routing mechanism apparent built into WordPress (see "null" on line 30 of "wp-content/plugins/restful-services/restful-web-services.php" below, I assume it should have been the URL of the .php file I plan to execute but WordPress doesn’t see to use what I put there.)

The function "restful_web_services_exec_service()" is what is called to execute the appropriate web service:

wp-content/plugins/restful-services/restful-web-services.php:

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
Plugin Name: RESTful Web Services
Plugin URI: http://mikeschinkel.com/wordpress/restful-web-services/
Description: This plugin enables REST-based API web services for WordPress.
Author: Mike Schinkel
Version: 0.1
Author URI: http://mikeschinkel.com/
*/
 
define('RESTFUL_WEB_SERVICES_DIR', dirname(__FILE__));
define('RESTFUL_WEB_SERVICES_URL_PATTERN','services(/?.*)?');
$abspath = trim(str_replace('\\','/',ABSPATH),'/');
$rest_services_dir = str_replace('\\','/',RESTFUL_WEB_SERVICES_DIR);
$rest_services_path = trim(str_replace($abspath,'',$rest_services_dir),'/');
define('RESTFUL_WEB_SERVICES_PATH', $rest_services_path);
 
// NOTE, See: http://codex.wordpress.org/Custom_Queries#Permalinks_for_Custom_Archives
add_action('init', 'restful_web_services_flush_rewrite_rules');
add_filter('generate_rewrite_rules', 'restful_web_services_add_rewrite_rules');
add_action('template_redirect', 'restful_web_services_exec_service');
 
add_action('init', 'restful_web_services_flush_rewrite_rules');
function restful_web_services_flush_rewrite_rules() {
 global $wp_rewrite;
 $wp_rewrite->flush_rules();
}
 
add_filter('generate_rewrite_rules', 'restful_web_services_add_rewrite_rules');
function restful_web_services_add_rewrite_rules( $wp_rewrite ) {
  $new_rules = array(
    RESTFUL_WEB_SERVICES_URL_PATTERN => null,
  );
  $wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
}
 
function restful_web_services_exec_service() {
  global $wp;
  if ($wp->matched_rule==RESTFUL_WEB_SERVICES_URL_PATTERN) {
    if ($wp->request == 'services') {
      header('Content-Type: text/plain');
      print 'TODO: Generate a list of RESTful Web Services here for this WordPress Blog.';
    } else {
      list($dummy,$service_name) = explode('/',$wp->request);
      if (file_exists($service_php = (RESTFUL_WEB_SERVICES_DIR . '/services/' . $service_name . '.php'))) {
        include_once $service_php;
      } else {
        header('Content-Type: text/plain');
        status_header(404);
        print '404 - Service not found.';
      }
    }
    exit;
  }
}

You’ll note that my function "restful_web_services_exec_service()" is very bare-bones at the moment serving only a plain text message "TODO:" for the path http://example.com/services, and assuming that any path http://example.com/services/{service} will execute a same-named .php file in the services subdirectory, i.e. for http://example.com/services/latest-post it will look for "wp-content/plugins/restful-web-services/services/lastest-post.php" and then delegate all the work to that .php file.

wp-content/plugins/restful-services/services/latest-vidclip.php:

2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
Filename: latest-post.php
Service Name: Latest Post
*/
global $wp_query;
$post = $wp_query->post;
$file = get_attached_file($post->ID);
if (empty($file)) {
  list($file)= get_enclosed($post->ID);
}
$charset = get_option('blog_charset');
header('Content-Type: text/xml; charset=' . get_option('blog_charset'), true);
$link = get_permalink($post->ID);
$html = &lt; &lt;<post></post><post id="{$post-&gt;ID}">      <video>$file</video>   <link />$link  </post>  POST; print $html;

Here is an example output returned by calling http://example.com/services/latest-post:

1
2
&lt; ?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; 
<post id="2">      <video>http://videos example.org/video1.flv</video>   <link />http://example.org/sample-post-1/  </post>

While bare-bones, that will meet my needs for the moment since I really only have the need for one service that responds to the HTTP GET verb. However, I can see where I might end up fleshing this out and create lots of helper functions that would streamline the creation of web services for RESTful access to the entirety of the WordPress database. If I do so I’ll be happy to donate this plugin to the community.  In the mean time if you’d like to use this for your own use feel free but caveat emptor. However, if you’d like to use this code to create a plugin to contribute to the community, please contact me for collaboration.  Of course if you’d like to retain me to fully flesh it out for you, I’m always open to that too! :-)

Finally, if anyone who knows the WordPress API better than I do can tell me where I erred in my analysis, and I really hope I did, please let me know so I can architect this thing better. On the other hand, if I was spot-on in my analysis maybe this will help the WordPress team understand what needs to be done so they can empower WordPress to generate any arbitrary content type without having to resort to hacks or bypassing the WordPress index.php startup code.