This is an explanation of a solution to a specific problem with one of my plugins, but the issue is larger and should be understood by all WordPress plugin developers. Posting the write-up here with the hope it will be useful.
The issue is a change in the activity happening in the save_post
hook. I’d probably consider this change in WP to be a bug or regression – but it’s out there now in 2.6.x, so as plugin developers we have to work around it (even it a better solution is found in a future WP release).
I received a couple of bug reports from people recently that Twitter Tools was creating duplicate custom fields for them. This struck me as odd, as that code hasn’t changed and it’s pretty darn simple. Turns out it was WordPress that had changed, and Twitter Tools (and probably many other plugins) now need to adapt.
The Problem
After publishing a post, future edits to that post would cause additional, duplicate custom fields to be added to the post. Here is an example:
The Cause
It took be a little while to debug this. The code is simple:
function aktt_store_post_options($post_id) {
[...]
if (!update_post_meta($post_id, 'aktt_notify_twitter', $notify)) {
add_post_meta($post_id, 'aktt_notify_twitter', $notify);
}
}
add_action('save_post', 'aktt_store_post_options', 1);
This code fires when a post is saved, and updates the custom fields appropriately.1 The aktt_store_post_options
function is attached to the save_post
hook, so that code is executed when a post is saved.
The save_post
hook actually sends two parameters $post_id
(just the ID) and $post
(the full post data). I’d chosen to use just the $post_id
parameter here because I only needed to store the post ID along with the custom field key and value.
This works great on versions of WordPress prior to 2.6, but a new feature in WP 2.6 changed that. The new feature in question is: post revisions.
With the post revision feature, post revisions are saved along with the post – also triggering the save_post
hook. For these, the $post_id
(for an already published post) being passed to my function is the id of the post revision, not the original post id. As a result, my function is checking to see if the newly created revision (not the original post) has this custom field and adding it when it doesn’t find it.
WordPress then seems to aggregate the custom fields from all revisions into the display for the post on the Write page in the admin interface. I haven’t looked into the details of this yet, but that’s a fair guess. The point is that these custom fields are being added to post id 12, 13, 14, etc. but all being displayed for post id 12.
This isn’t what we want.
The Solution
The solution is to look at the post itself instead of just the post id. Several changes need to be made to do this.
The code that attaches my function to the save_post
hook needs to be changed to request both parameters instead of just the post id:
add_action('save_post', 'aktt_store_post_options', 1, 2);
The last parameter tells it to give my function 2 parameters. Now my function will get the full post info, not just the post id.
Now my code needs to get smarter.
A post revision in WP 2.6 has a couple of things we can look for:
$post->post_type
– this will be set to ‘revision’ for post revisions.$post->post_parent
– this will be set to the post id of the original post.$post->post_status
– this will be set to ‘inherit’ for post revisions.$post->ancestors
– this is an array of post ids – I’ve only seen it have a single item in the array so far, but I haven’t looked through the code around it.
I think we can just work with the $post->post_type
in this situation – we just add some code at the top of the function:
function aktt_store_post_options($post_id, $post) {
if ($post->post_type == 'revision') {
return;
}
[...]
if (!update_post_meta($post_id, 'aktt_notify_twitter', $notify)) {
add_post_meta($post_id, 'aktt_notify_twitter', $notify);
}
}
Now we won’t do anything for revision posts, but the regular posts get the proper post meta treatments.
Here is the full replacement for the original code (top of post):
function aktt_store_post_options($post_id, $post) {
if ($post->post_type == 'revision') {
return;
}
[...]
if (!update_post_meta($post_id, 'aktt_notify_twitter', $notify)) {
add_post_meta($post_id, 'aktt_notify_twitter', $notify);
}
}
add_action('save_post', 'aktt_store_post_options', 1, 2);
Conclusion
Prior to WP 2.6, you could expect that the data coming through the save_post
hook was a post. The post could have various states (draft, published, etc.) but it was a single logical data type. In WP 2.6.x, you need to account for a new data type coming through the save_post
hook: revisions.
I don’t think revisions should be sent through the same save_post
hook that normal posts are sent through. I think that sending them through a new save_revision
hook would have been a better solution (and still would be). But since WP 2.6.x does treat them this way, our plugins need to handle the situation gracefully.
Remember this is in issue in WP 2.6+ only, so when working around it make sure your code is conditional so that it is backwards compatible with WP 2.5.x, 2.3.x, etc.
This change is already committed to the SVN repository for Twitter Tools, I’ll get a new beta out soon.
- Note that the
update_post_meta
function was changed in WP 2.5 or 2.6 to also do an add if needed, but does not do this in WP 2.3. [back]
This post is part of the project: Twitter Tools. View the project timeline for more context on this post.