diff --git a/gutenberg.php b/gutenberg.php index 7ab481538e32cc..3706a64b6ef741 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -128,3 +128,11 @@ function gutenberg_pre_init() { require_once dirname( __FILE__ ) . '/lib/load.php'; } + +/** + * Outputs a WP REST API nonce. + */ +function gutenberg_rest_nonce() { + exit( wp_create_nonce( 'wp_rest' ) ); +} +add_action( 'wp_ajax_gutenberg_rest_nonce', 'gutenberg_rest_nonce' ); diff --git a/lib/client-assets.php b/lib/client-assets.php index 4d07d1c51ac97e..8e6b1d688cb471 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -246,25 +246,11 @@ function gutenberg_register_scripts_and_styles() { wp_add_inline_script( 'wp-api-fetch', sprintf( - implode( - "\n", - array( - '( function() {', - ' var nonceMiddleware = wp.apiFetch.createNonceMiddleware( "%s" );', - ' wp.apiFetch.use( nonceMiddleware );', - ' wp.hooks.addAction(', - ' "heartbeat.tick",', - ' "core/api-fetch/create-nonce-middleware",', - ' function( response ) {', - ' if ( response[ "rest_nonce" ] ) {', - ' nonceMiddleware.nonce = response[ "rest_nonce" ];', - ' }', - ' }', - ' )', - '} )();', - ) - ), - ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ) + 'wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "%s" );' . + 'wp.apiFetch.use( wp.apiFetch.nonceMiddleware );' . + 'wp.apiFetch.nonceEndpoint = "%s";', + ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ), + admin_url( 'admin-ajax.php?action=gutenberg_rest_nonce' ) ), 'after' ); diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index 27fb225fe94eb8..7535d2117b2f24 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -49,6 +49,14 @@ function registerMiddleware( middleware ) { middlewares.unshift( middleware ); } +const checkStatus = ( response ) => { + if ( response.status >= 200 && response.status < 300 ) { + return response; + } + + throw response; +}; + const defaultFetchHandler = ( nextOptions ) => { const { url, path, data, parse = true, ...remainingOptions } = nextOptions; let { body, headers } = nextOptions; @@ -71,13 +79,6 @@ const defaultFetchHandler = ( nextOptions ) => { headers, } ); - const checkStatus = ( response ) => { - if ( response.status >= 200 && response.status < 300 ) { - return response; - } - - throw response; - }; const parseResponse = ( response ) => { if ( parse ) { @@ -148,7 +149,27 @@ function apiFetch( options ) { return step( workingOptions, next ); }; - return createRunStep( 0 )( options ); + return new Promise( function( resolve, reject ) { + createRunStep( 0 )( options ) + .then( resolve ) + .catch( ( error ) => { + if ( error.code !== 'rest_cookie_invalid_nonce' ) { + return reject( error ); + } + + // If the nonce is invalid, refresh it and try again. + window.fetch( apiFetch.nonceEndpoint ) + .then( checkStatus ) + .then( ( data ) => data.text() ) + .then( ( text ) => { + apiFetch.nonceMiddleware.nonce = text; + apiFetch( options ) + .then( resolve ) + .catch( reject ); + } ) + .catch( reject ); + } ); + } ); } apiFetch.use = registerMiddleware; diff --git a/packages/e2e-tests/plugins/nonce.php b/packages/e2e-tests/plugins/nonce.php new file mode 100644 index 00000000000000..e28ff07ac48ad1 --- /dev/null +++ b/packages/e2e-tests/plugins/nonce.php @@ -0,0 +1,16 @@ + { + beforeAll( async () => { + await activatePlugin( 'gutenberg-test-plugin-nonce' ); + } ); + + afterAll( async () => { + await deactivatePlugin( 'gutenberg-test-plugin-nonce' ); + } ); + + beforeEach( async () => { + await createNewPost(); + } ); + + it( 'should refresh when expired', async () => { + await page.keyboard.press( 'Enter' ); + // eslint-disable-next-line no-restricted-syntax + await page.waitFor( 5000 ); + await page.keyboard.type( 'test' ); + // `saveDraft` waits for saving to be successful, so this test would + // timeout if it's not. + await saveDraft(); + // We expect a 403 status once. + expect( console ).toHaveErrored(); + } ); +} );