WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.51k stars 4.2k forks source link

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

Open manzoorwanijk opened 5 years ago

manzoorwanijk commented 5 years ago

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.

manzoorwanijk commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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 https://github.com/WordPress/gutenberg/pull/13718 which attempts to fix a preview race condition reported at https://github.com/WordPress/gutenberg/issues/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 commented 5 years ago

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 commented 5 years ago

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

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

mboynes commented 4 years ago

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 commented 4 years ago

Let's reopen to reconsider that.

janboddez commented 1 year ago

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 commented 1 year ago

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)?

janboddez commented 7 months ago

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.