Implementing `is_gutenberg()`

So, I wanted to be able to detect whether a certain request was initiated by the block editor.

TL;DR: When the presence of the global REST_REQUEST constant isn’t enough.

Some Context

My Share on Mastodon plugin used to rely on (only) the transition_post_status hook to kick off a request to Mastodon’s API. (Good times!)

Before we do so, however, we look for a “checkbox”—$_POST['share_on_mastodon'], to be exact. If it’s there, great, let’s share! If not, don’t do anything.

That’s how it worked.

Then Gutenberg came.

In the block editor, when you save a post, it sends a JSON object to WordPress’ REST API. The post gets stored, the transition_post_status hook runs. No $_POST variables. None.

No worries, though. The block editor, whenever a meta box is present, will submit a second request with these variables. Yay!

Except, we don’t really look for these variables to decide what to do. We look at previously stored values. This allows us to set them upfront, even programmatically, and then they’ll be there even when you schedule posts.

Right?

So now, when you’ve previously enabled sharing (e.g., when your post was still “draft”) and then, for whatever reason, you uncheck the “Share on Mastodon” checkbox right before clicking “Publish,” guess what happens.

Right. Goes right through, because the updated checkbox value isn’t known until after the post is actually published.

OK, we can deal with that. Just gotta detect whether a post was saved through the block editor, and if so, ignore the first (of two, hopefully) requests. (Since the plugin relies, or rather, relied, on a “classic” meta box, we knew there would always be a second request.)

That will allow us to, during that second request, process and store the latest and greatest values, and, subsequently, fire off our request to Mastodon’s API.

(But: if a post is saved from the classic editor, that first request must not be ignored; there will not be a second one.)

Searching for Block Editor Clues

So. No is_gutenberg().

But. There’s a use_block_editor_for_post_type(). Awesome.

Except, it doesn’t … It … It checks whether a post type was registered with REST API support. And most post types are. I mean, it’s been proven to be unreliable.

use_block_editor_for_post()? Uses the same underlying logic—with one exception: it returns false during that second request mentioned above.

has_blocks()? Only looks for something vaguely similar to blocks inside a post’s content. Useless for new posts.

Well, we can look for the global REST_REQUEST constant. Except it’ll be true for any REST request. (How is that an issue? Well, if you enabled the “Share Always” option and posted from a mobile app, and we subsequently ignored that request, nothing would be shared. That’s an issue.)

So, to exclude 3rd-party apps, we can look at the referrer URL. (Which I have at one point suggested as part of the solution to this issue.)

After all, a REST request coming from WP Admin must mean it came from the block editor. Well, not necessarily. (But inside a transition_post_status callback? Highly likely.) Yet, privacy-minded users might have disabled referrer headers altogether.

get_current_screen()

I also came across a similar question on GitHub. Folks there suggested using get_current_screen()->is_block_editor(), which I’m sure works great when an actual admin page is being loaded. You can’t use it in a REST controller, though. “Call to undefined function get_current_screen()” and such.

Nonce

So, what’s left? A nonce? Each REST request is expected to come with a wp_rest nonce. (Unless, of course, we’re authenticated using an auth token from an app or something, i.e., not logged into the web interface.)

Ah! So if the current request is, in fact, a REST request and there happens to be a valid wp_rest nonce either as a GET or POST variable or through a request header: good chance this is a Gutenberg request!

After all, if it was a classic editor request, the REST_REQUEST constant would be undefined. If it was a REST request initiated from a mobile app, there wouldn’t be a nonce.

By the Way

I have since added a “true” Gutenberg sidebar panel which doesn’t suffer from all of this. (Suffers from different issues, though. Plus, “duplicate” code. Lots of it, in fact.)

A Code Example

Uhm, so, this is what I’ve settled on for the time being. If you know of a more elegant solution, hit me up.

function my_is_gutenberg() {
  if ( ! defined( 'REST_REQUEST' ) || ! 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 ( null === $nonce ) {
    return false;
  }

  return wp_verify_nonce( $nonce, 'wp_rest' );
}

3 responses to “Implementing `is_gutenberg()`”

  1. Jan Boddez

    Yes, I can, in my specific code, use the `rest_after_insert_{$post->post_type}` hook. And I do (in one particular scenario)!

    But it’d still run for non-Gutenberg API requests, and it’d still need to be ignored when the meta box’s `$_POST` variables haven’t yet been processed.

    The second one’s rather easy; one can check against `empty( $_REQUEST[‘meta-box-loader’] )`. But it must be during that hook, or it could run for classic editor users, too.

  2. Jan Boddez

    > But it’d still run for non-Gutenberg API requests[.]

    You’re always going to need a nonce or referrer check for this one, I’m afraid. Unless core has something available that I somehow haven’t encountered, yet.

  3. Jan Boddez

    Moreover, I’d still need a different hook for classic editor sites. So, doesn’t solve all that much.

    (I only use `rest_after_insert_*` when I’m dealing with a “pure” Gutenberg panel, the one I’ve recently added to my plugin. Because it runs after metadata is saved, and also works when there’s only ever one request.)

Leave a Reply

Your email address will not be published. Required fields are marked *