RESTful Web Services in a WordPress Plugin?

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
16
17
18
19
20
21
22
23
/*
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 = < <<POST
<?xml version="1.0" encoding="$charset" version="1.0">
<post id="{$post->ID}">
  <title>{$post->title}</title>
  <video>$file</video>
  <link>$link</link> 
</post> 
POST;
print $html;

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

1
2
3
4
5
6
< ?xml version="1.0" encoding="UTF-8"?> 
<post id="2">
  <title>Sample Post #1</title>
  <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. 

FCKEditor’s ShowBlocks Feature ROCKS!

FCKeditor ShowBlocks Feature

FCKeditor ShowBlocks Feature

The other day I wrote about how I was using Dean’s FCKeditor Plugin for Wordpress to solve the problems with WordPress’ default TinyMCE editor eating my hand-coded HTML tags.  Well Joe Banks left a great and detailed comment giving several tips including one about the ShowBlocks button. Unfortunately the default comment system in WordPress ate part of his comment so I couldn’t see what he was talking about and went exploring. 

Well it turns out he was right; ShowBlocks is awesome!  It shows the <p> tags, the <div> tags, and more. Check the screen shot above to see a larger version.

Joe Banks forgot to include his blog URL on his comment (or at least I think he forgot), but I was able to dig it up via a quick google.

WordPress as Wiki?

Interesting.  I guess my recognition of the similarity of WordPress’ new Revisions control to Mediawiki/Wikipedia was not unique.  Andrew Hyde proposed Wikify, as a plugin for WordPress to allow readers to revise posts for facts, spelling errors, et. al. Great idea. 

Of course it becaming popular would be a mixed blessing as we’d have yet another thing for which we’d need to manage spam. :-)

Fixing WordPress’ Eating of HTML div Tags

FCK Editor Logo
TinyMCE Logo

I should have known that not all would be rosy in my move to WordPress. It seems that core WordPress’ default implementation of TinyMCE consumes <div> tags in hand-coded HTML posts for breakfast, lunch, and dinner. That behavior has caused me no end of headache as I’ve tried porting the HTML pages from my old completely hand-coded website as WordPress has mangling those finely-coded masterpieces <sic> beyond all normal recognition.

A little googling and I uncovered lots of discontent on the topic along with a few hacks and a hackish plugin, none of which I really want to implement in given WordPress constant upgrade cycle. The suggestions to fix this in core were so well received that I was surprised that the topic does not appear to have been addressed by the core WordPress team.

However, I was finally able to stumble upon a plug-in called Dean’s FCKEditor For Wordpress that appears like may solve the problem by simply replacing TinyMCE with FCKEditor.  Don’t know if this is going to be my panacea or not, but after installing it the first thing I notice is how FCKEditor’s normal fonts are too small for my tastes (I guess I’m going to have to dive in and figure out how to change that.)

If Dean’s plugin does not ultimate solve that problem I ran across two other plugins worth considering. The heavyweight TextControl Plugin and the lightweight Disable wpautop Plugin. I’ll revisit this issue and those plugins in the future if the topic becomes obviously worth revisiting.

WordPress, Finally!

It’s been a really long time since I last blogged, and it’s all because I got totally fed up with my old blog software and vowed never again to blog until I replaced it with WordPress. Well as you can guess getting around to replacing it took far longer than I planned, but now it is finally here! I’ve still have other non-blog related things that were housed at my domain I still need to fix such as this but now that the domain is switched over to WordPress I’ll have a bit more urgency to get those fixed. I look forward to rejoining to ranks of the blogging community. 

What’s more, a lot has happened since I last blogged so I have lots of things to blog about in the coming weeks and months. Of course I have plenty of billable work that needs to get done so for all those of you who are waiting with baited breath for me to blog (LOL!), future blog posts won’t be coming as fast and furious as I’d like but at least with the new blog they can start to trickle out.

Long Time, No Blog

Yes I know, it’s a blogger’s cardinal sin to post about why he hasn’t posted in a while. But live with it.

The irony is I’ve had so much to blog about. The reason I haven’t is because a while back I finally gave up on dasBlog and decided I’d switch to WordPress before I blogged again. dasBlog makes so many things difficult that are either easy or trival on WordPress, such as commenting and monitoring spam. After years of putting up with dasBlog I just finally got fed up and decided I’d wait to switch to Wordpress. Sadly I’ve waited a long time, and it’s possible it may still be a while before I can move everything over.

Of course I could have tried upgrading dasBlog, but it’s so much harder to enhance dasBlog with it’s limited templating system that requires compiled .NET plugins vs. WordPress’ PHP scripting (reminiscient of classic ASP+VBScript, only better) that I was finally able to shed my programmer’s guilt for not learning how to write usable .NET plugins just as I was able to shed my guilt for never becoming proficient in x86 assembler back in the late 80’s.

I’ve got a huge backlog of posts that are anywhere from 10% to 99% complete, many of which will never see the light of day because they just won’t be appropriately timely enough by the time I’m ready to finish and post them. Ah well, story of my life; I can envision far more than I ever have time to complete.

Anyway, the reason for this post is to introduce the next post about a module I’m writing for Drupal. I’ve spend a lot of time recently with Drupal and am getting quite good at it, even if I do say so myself. I would have liked to have posted several Drupal related posts as a recursor but if I waited for that I doubt I’d ever manage to post about the module!

So without further adieu, on to the next post!

P.S. It may actually be a few days before I get that post finalized, but if it is not posted yes I am working diligently on it so just hold your breath… :-)

Learning about Adobe AIR in Atlanta…

I’m at the Fox Theatre in my hometown of Atlanta today checking out the Adobe AIR Bus Tour Summer 07. It’s nice to be at the first event nationwide. I’m attending at the behest of a friend who thinks it going to be the "next big thing." I’m skeptical. I fear yet another proprietary attempt to empower developers to craft unique custom web interfaces to provide desktop functionality as a layer over web technologies, and that’s not a compliment. These types of things, especially when looking at the black box nature of opaque Flash SWF files, do their best to ignore those things that make the web work, i.e. stateless URL-addressed resources. The reality of Adobe AIR remains to be seen… P.S. It would have been nice if Adobe had consulted me to ensure that this event was more convenient for me. I mean, I actually had to leave my home and cross the street to attend. Adobe, Please! ‘-)

Carson Workshop’s “Future of Web Apps” Conference was Incredible!

Place of the Fine Arts; inside and out The past two days I attended Carson Workshop’s "The Future of Web Apps" presented at The Palace of Fine Arts in San Francisco and I must say it was one of the best conferences I’ve been to in years!  Every one of the speakers was excellent each providing invaluable insight, and the energy level was just electric! 

I really liked the venure too; an ~800 seat auditorium where the entire single-track conference was held. It had so much better feel than getting stuffed into lots of little breakout rooms at a hotel or a convention center.

Not everything was perfect, i.e. not enough networking opportunities, flaky WiFi, and no exhibit hall, but at $2951 for two days the event was otherwise so incredible that I feel really bad2 even mentioning any negatives! OTOH, Ryan Carson was made fully aware of those problems by people other than me and I get the sense that next time it will be corrected.

Lastly, Ryan announced plans to publish online the audio ala T.E.D. for each presentation which the presenter the agrees, which Ryan definitely encouraged! That’s a very "Creative Commons" approach, and oh so right for a Web 2.0 conference (or any other future conference, for that matter.)

Maybe I had such a good time because I was burned out on 12+ years of Microsoft-oriented conferences and just needed something new.

Whatever the case; Bravo Carson, you definitely made a fan!  If you get a chance to attend one of there future conferences on a subject of interest to you, don’t hesitate, don’t think about it; just do it! I’m sure you won’t be disappointed.

Footnotes

  1. The $295 price was also so very in line with the ethos of "Web 2.0"; created high value for little money, and benefitting from the goodwill that creates. I so totally feel like they practice what they preach at Carson!
  2. Of course if it had been a Microsoft TechEd for $1000+ I was have been totally pissy about any lack of perfection, but not at $295 for the quality that Carson delivered!