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.
[…] release is just the same as 1.5b2, but fixes the duplicate custom field issue for people running WordPress […]
I have checked the function add_post_meta in WP 2.6.1 and I found this:
// make sure meta is added to the post, not a revision
if ( $the_post = wp_is_post_revision($post_id) )
$post_id = $the_post;
I guess wp_is_post_revision() could also be used instead of $post->post_type.
When I look in the database, I see post meta added to the post revision post id instead of the original post id. So something isn’t working quite right in there.
I agree, something isn’t right, but in a different way:
When I look in my database, it does add the meta to the actual post id, but it doesn’t check if it wasn’t added already.
A patch is needed, in any case. Until then, thanks for the elegant solution, 🙂
I’m having this problem and it is pretty annoying. Guessing it must be from one of my plugins, but the ones I am using are all very popular and main stream…others must be having the same issue???
I haven’t looked into the code involving the ‘save_post’ hook, but it seems it needs some changes to account for the revisions feature. More importantly, it should behave as it did before in older WP versions.
From what I understand, I think it is the same hook that introduced a lot of problems in wp-podpress after WP 2.6 was released.
Hi
Excuse my blinding ignorance, but where do I make these changes?
Thank you
HAI! I just spoke to your friend, ‘Safety Letter’ (Notice that I did include a Safety letter in the word ‘Hi’)
It seems he is doing well, however I am concerned by his lack of activity of late, could you please confirm his existence?
Many thanks, Name
Very much looking forward to integrating Twitter Tools with a couple of my sites. Having trouble, though, understanding where things stand in relation to this issue, whether the current beta version addressing known issues, when the next version will come out, etc. Can you clarify? Thanks so much.
Maybe another solution, if revisions are not needed, is to turn off that feature. Just add the following line to the wp-config.php file.
define(‘WP_POST_REVISIONS’, false);
[…] a month ago Alex King discovered an “undocumented feature”1 in WordPress 2.6 that affects plugins like Cricket Moods. […]
Thanks for the posting, it really cleared up the $wpdb->post_meta duplicates I was seeing in a plugin I wrote.
The “if ($post->post_type == ‘revision’)” check wasn’t working for me for some reason, so even though this would seemingly be called twice, I changed the beginning of my function to:
function postarchiver_save_post($unusued, $post){
if (!$post) { return; }
if ($post->post_parent != 0) {
$post_id = $post->post_parent;
} else {
$post_id = $post->ID;
}
Wow, thanks for posting this! I’ve been running into this problem for the past week or so and it’s been driving me crazy.
[…] the duplicate custom field issue for people running WordPress […]
Thanks for the second arg tip, this revision calls on save_post was bugging me a lot 🙂
You did a great job of explaining this. Thanks a lot it helped me with a project I am working on.