Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to differentiate between REST and POST requests of Gutenberg inside "save_post"? #12903

Open
manzoorwanijk opened this issue Dec 15, 2018 · 17 comments
Labels
[Feature] Saving Related to saving functionality REST API Interaction Related to REST API [Type] Enhancement A suggestion for improvement.

Comments

@manzoorwanijk
Copy link
Contributor

As we know that Gutenberg sends two requests when we hit the Publish/Update button:

  1. Request to WP REST API to create/update the post
  2. Request to post.php to save/submit the metaboxes

save_post is triggered by both the requests. How do I know inside the save_post callback that the post is created/updated via Gutenberg, preferably in the second request? This is to avoid the double effect of the hook.

I know some would suggest to add some meta value in the first request and use it to short-circuit the second callback or use a transient in a similar way. Filling up the meta table with useless data is not a good idea and transients don't look to be a better solution IMHO.

Is there a consistent query argument or a header that can be relied upon? I couldn't find any such thing in the two requests.

@swissspidy swissspidy added [Type] Help Request Help with setup, implementation, or "How do I?" questions. REST API Interaction Related to REST API [Feature] Saving Related to saving functionality labels Dec 15, 2018
@manzoorwanijk
Copy link
Contributor Author

Or may be if it's possible to add some parameter to the REST request so as to use it in save_post or rest_insert_{$post_type} callback. I checked the JS document for Gutenberg but couldn't find it.

@TimothyBJacobs
Copy link
Member

What are you doing on the save_post hook?

If you only want to do something on REST requests, you could use the rest_after_insert_ hooks.

@manzoorwanijk
Copy link
Contributor Author

manzoorwanijk commented Dec 17, 2018

@TimothyBJacobs thank you for your contribution to WP REST API - rest_after_insert_ hooks :)

Let me explain:
My plugin uses save_post to share the posts to Social Media depending on the user preferences - instantly or with some delay. The user can override the default preferences for a post on Post Edit Screen, currently via metaboxes.
Now there are many ways a post can be published/updated:

  1. Using Classic Editor - allows to override the settings via metaboxes
  2. Using Gutenberg:
    (i) WP REST API request - no metabox data available to save_post
    (ii) POST request tp post.php - metabox data available
  3. Using the plugins like Jetpack Email to Post or Scrapping plugins - no metaboxes available
  4. Using WP REST API (non-Gutenberg) - no metaboxes available

The main problem here is that how can I differentiate between 2(i) and 4.
A user may sometimes select not to share the post using the metabox options, but that selected option won't be available to the request 2(i). Is there a way I can add my own field to the REST request sent in 2(i)? That will solve my problem.
Thanks

@n7studios
Copy link

@manzoorwanijk I had the same question for WordPress to Buffer and WordPress to Hootsuite.

Here's the current solution I came up with, abstracted as a class that might help:
https://gist.github.com/n7studios/56fd05f19f5da26f19f6da0ccb57b144

@manzoorwanijk
Copy link
Contributor Author

@n7studios, thank you for that gist.

I already have such a workaround but not something to be relied upon.

Although it should work in most cases but there are certain issues with your solution:

  1. It assumes that the post has content, which may not be always true.
  2. If a user switches to classic editor and leaves those tags there, it will lead to a false conclusion.

Lets see if there is a better solution

@n7studios
Copy link

@manzoorwanijk Agreed.

I notice Posts through the Classic Editor define an _edit_last meta key, which doesn't appear to exist when Posts are edited through Gutenberg (both define the _edit_lock meta key, however).

Wondering if there's some way to use this to 'detect' whether the Post is truly Gutenberg or not?

Also worth inspecting the request object when Gutenberg + non-Gutenberg requests are made through the REST API, to see if there are any differences that distinguish whether the Post was created in Gutenberg or not.

@designsimply
Copy link
Member

I'm afraid I don't know enough to answer your questions directly, but I did want to say that I noticed a recent PR at #13718 which attempts to fix a preview race condition reported at #12617 and the discussion about the two types of requests you mentioned at the start (REST API for the post and post.php for the metaboxes). I am wondering if anything in that discussion or PR might help.

I also know that Jetpack has some social media interactions and that plugin is open source and wondered if it would be a good suggestion to say to check out that code to see if they have done anything similar that could help you figure out your case by chance?

@talldan
Copy link
Contributor

talldan commented Feb 11, 2019

As mentioned by @designsimply, it's worth pointing out the PR at #13718 which proposes to switch the order of the requests (there's no guarantees that will be merged.)

That may cause an issue for code that relies on the existing order, though it might also reduce some of the complexity since I think you'd be able to switch to just relying on the REST hook.

Also worth mentioning the second request only occurs when meta boxes are active.

I'll make sure to make the point that plugins might have some dependencies on the save order in #13718.

@mboynes
Copy link
Contributor

mboynes commented Apr 4, 2019

In encountering this issue in the wild, I believe this to be a fairly significant problem. Recapping the important bits:

  • Gutenberg saves data via one or two requests; the second save request only fires if there are meta boxes, so a plugin cannot depend on this request happening.
  • If there are going to be two requests, the plugin can't necessarily depend on the first request, because the first doesn't contain all the data for the post.
  • A plugin can't determine in the first request that there will ever be a second request.

If a plugin needs to run some process after a post is updated, for instance sync the "final" post data to some external resource, there's no longer a reliable way to do that. It used to be that one could pretty heavily rely on save_post at a very high priority number, or wp_insert_post, to ensure that all post data (with relatively rare exception) has been saved.

@youknowriad
Copy link
Contributor

I believe there's a solution proposed here https://gist.github.com/n7studios/56fd05f19f5da26f19f6da0ccb57b144

And Gutenberg will always use two requests if you have meta boxes but there's no other way to support meta boxes otherwise. The meta boxes API is not deterministic enough to be able to run its saving hooks on the REST API call.

@mboynes
Copy link
Contributor

mboynes commented Jan 15, 2020

This should not be closed. The proposed solution is not a sufficient workaround. Please review my comment above, this is a problem without a viable option as-is.

At the most basic level, gutenberg should include a flag in the first request that there is going to be a second request.

@youknowriad
Copy link
Contributor

is it possible to check for existence of metaboxes on the server to figure that out?

@mboynes
Copy link
Contributor

mboynes commented Jan 15, 2020

That would be a lot of work and I don’t think it would be reliable. Gutenberg knows if it would be sending a follow up request or not, right? Why not announce that intent?

@youknowriad
Copy link
Contributor

Let's reopen to reconsider that.

@janboddez
Copy link
Contributor

janboddez commented May 11, 2023

Running into this as well. (I could copy a bunch of code over to Gutenberg and basically maintain a PHP and a JS codebase for the classic and the block editor, but that would, well, lead to other issues. So I'd much rather be able to detect Gutenberg requests reliably. has_blocks() etc. just doesn't cut it, in this case.)

Could we, in addition to defined( 'REST_REQUEST' ) && REST_REQUEST or empty( $_REQUEST['meta-box-loader'] ) (either would be true for both the first Gutenberg and another type of REST request) not also look for a referrer?

To see if a request came from the admin or an entirely different client? (Even if they can be spoofed and whatnot, it'd still be better than assuming any REST request is a Gutenberg request, no?)

Seems to work (unless of course some hosts unset refer(r)er URLs?):

if (
    defined( 'REST_REQUEST' ) && REST_REQUEST &&
    empty( $_REQUEST['meta-box-loader'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    0 === strpos( wp_get_referer(), admin_url() )
) {
    // This should run only the first time a post is saved from the block editor.
}

Oh, I'm using this inside a transition_post_status callback.

@janboddez
Copy link
Contributor

janboddez commented May 16, 2023

Was also wondering if, in case the referrer is deemed unreliable, we couldn't check against the wp_rest nonce (which we'd need to fetch from the headers or _wp_nonce body params)?

@jordesign jordesign added the [Type] Enhancement A suggestion for improvement. label Aug 2, 2023
@janboddez
Copy link
Contributor

janboddez commented Mar 31, 2024

Was also wondering if, in case the referrer is deemed unreliable, we couldn't check against the wp_rest nonce (which we'd need to fetch from the headers or _wp_nonce body params)?

Case anyone's interested, still, I've been using this bit of code to detect whether a request is coming from Gutenberg (but not, e.g., a mobile app, for which REST_REQUEST would be set as well).

function is_gutenberg() {
	if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
		// Not a REST request.
		return false;
	}

	$nonce = null;

	if ( isset( $_REQUEST['_wpnonce'] ) ) {
		$nonce = $_REQUEST['_wpnonce'];
	} elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
		$nonce = $_SERVER['HTTP_X_WP_NONCE'];
	}

	if ( ! is_string( $nonce ) ) {
		return false;
	}

	// Check the nonce.
	return wp_verify_nonce( $nonce, 'wp_rest' );
}

In combination with the presence of $_REQUEST['meta-box-loader'], this could be used (at least, it's been working for me, for like a year) to tell the difference between the first and a potential second request originating from the block editor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Saving Related to saving functionality REST API Interaction Related to REST API [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests

10 participants