From b4a25639ad6a25911353bc66da60fddcc103d6b5 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 30 May 2018 13:19:11 +0100 Subject: [PATCH] Add the middlewares API --- .eslintrc.js | 2 +- components/code-editor/index.js | 4 +- .../higher-order/with-api-data/request.js | 7 +- core-blocks/embed/index.js | 3 +- edit-post/index.js | 10 -- edit-post/store/effects.js | 3 +- editor/components/autocompleters/user.js | 7 +- .../post-taxonomies/flat-term-selector.js | 7 +- .../hierarchical-term-selector.js | 7 +- editor/components/provider/index.js | 2 +- editor/components/url-input/index.js | 3 +- editor/store/effects.js | 15 +-- lib/client-assets.php | 35 ++++-- lib/compat.php | 117 ------------------ packages/api-request/README.md | 19 +++ .../api-request/src/http-v1-middleware.js | 19 +++ packages/api-request/src/index.js | 103 +++++---------- .../src/namespace-endpoint-middleware.js | 27 ++++ packages/api-request/src/nonce-middleware.js | 46 +++++++ .../api-request/src/preloading-middleware.js | 49 ++++++++ .../api-request/src/root-url-middleware.js | 34 +++++ utils/mediaupload.js | 7 +- webpack.config.js | 2 +- 23 files changed, 295 insertions(+), 233 deletions(-) create mode 100644 packages/api-request/src/http-v1-middleware.js create mode 100644 packages/api-request/src/namespace-endpoint-middleware.js create mode 100644 packages/api-request/src/nonce-middleware.js create mode 100644 packages/api-request/src/preloading-middleware.js create mode 100644 packages/api-request/src/root-url-middleware.js diff --git a/.eslintrc.js b/.eslintrc.js index ce01183611968..eaf9372817d18 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,7 +26,7 @@ module.exports = { 'jest/globals': true, }, globals: { - wpApiSettings: true, + wpApiSchema: true, }, plugins: [ 'jest', diff --git a/components/code-editor/index.js b/components/code-editor/index.js index 546f0afe67329..bac7580e58065 100644 --- a/components/code-editor/index.js +++ b/components/code-editor/index.js @@ -21,7 +21,7 @@ function loadScript() { } const script = document.createElement( 'script' ); - script.src = `${ wpApiSettings.schema.url }/wp-admin/load-scripts.php?load=${ handles.join( ',' ) }`; + script.src = `${ wpApiSchema.url }/wp-admin/load-scripts.php?load=${ handles.join( ',' ) }`; script.onload = resolve; script.onerror = reject; @@ -35,7 +35,7 @@ function loadStyle() { const style = document.createElement( 'link' ); style.rel = 'stylesheet'; - style.href = `${ wpApiSettings.schema.url }/wp-admin/load-styles.php?load=${ handles.join( ',' ) }`; + style.href = `${ wpApiSchema.url }/wp-admin/load-styles.php?load=${ handles.join( ',' ) }`; style.onload = resolve; style.onerror = reject; diff --git a/components/higher-order/with-api-data/request.js b/components/higher-order/with-api-data/request.js index c37aa016a6e6e..49c04a7fe3e9a 100644 --- a/components/higher-order/with-api-data/request.js +++ b/components/higher-order/with-api-data/request.js @@ -4,6 +4,11 @@ import memoize from 'memize'; import { mapKeys } from 'lodash'; +/** + * WordPress dependencies + */ +import apiRequest from '@wordpress/api-request'; + export const getStablePath = memoize( ( path ) => { const [ base, query ] = path.split( '?' ); if ( ! query ) { @@ -75,7 +80,7 @@ export function getCachedResponse( request ) { } export function getResponseFromNetwork( request ) { - const promise = wp.apiRequest( request ) + const promise = apiRequest( request ) .then( ( body, status, xhr ) => { return { body, diff --git a/core-blocks/embed/index.js b/core-blocks/embed/index.js index b9596ea6dd258..91fb017f4415f 100644 --- a/core-blocks/embed/index.js +++ b/core-blocks/embed/index.js @@ -19,6 +19,7 @@ import { BlockAlignmentToolbar, RichText, } from '@wordpress/editor'; +import apiRequest from '@wordpress/api-request'; /** * Internal dependencies @@ -30,7 +31,7 @@ import './editor.scss'; const HOSTS_NO_PREVIEWS = [ 'facebook.com' ]; // Caches the embed API calls, so if blocks get transformed, or deleted and added again, we don't spam the API. -const wpEmbedAPI = memoize( ( url ) => wp.apiRequest( { path: `/oembed/1.0/proxy?${ stringify( { url } ) }` } ) ); +const wpEmbedAPI = memoize( ( url ) => apiRequest( { path: `/oembed/1.0/proxy?${ stringify( { url } ) }` } ) ); const matchesPatterns = ( url, patterns = [] ) => { return patterns.some( ( pattern ) => { diff --git a/edit-post/index.js b/edit-post/index.js index 6e137825b07ba..b84647925ffc7 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -13,16 +13,6 @@ import store from './store'; import { initializeMetaBoxState } from './store/actions'; import Editor from './editor'; -/** - * Configure heartbeat to refresh the wp-api nonce, keeping the editor - * authorization intact. - */ -window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => { - if ( response[ 'rest-nonce' ] ) { - window.wpApiSettings.nonce = response[ 'rest-nonce' ]; - } -} ); - /** * Reinitializes the editor after the user chooses to reboot the editor after * an unhandled error occurs, replacing previously mounted editor element using diff --git a/edit-post/store/effects.js b/edit-post/store/effects.js index 1347614e4cd1b..2143abc2018ce 100644 --- a/edit-post/store/effects.js +++ b/edit-post/store/effects.js @@ -9,6 +9,7 @@ import { reduce, some } from 'lodash'; import { select, subscribe } from '@wordpress/data'; import { speak } from '@wordpress/a11y'; import { __ } from '@wordpress/i18n'; +import apiRequest from '@wordpress/api-request'; /** * Internal dependencies @@ -98,7 +99,7 @@ const effects = { additionalData.forEach( ( [ key, value ] ) => formData.append( key, value ) ); // Save the metaboxes - wp.apiRequest( { + apiRequest( { url: window._wpMetaBoxUrl, method: 'POST', processData: false, diff --git a/editor/components/autocompleters/user.js b/editor/components/autocompleters/user.js index 36a89249b6b6e..2b655f4b3ef6c 100644 --- a/editor/components/autocompleters/user.js +++ b/editor/components/autocompleters/user.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import apiRequest from '@wordpress/api-request'; + /** * A user mentions completer. * @@ -12,7 +17,7 @@ export default { if ( search ) { payload = '?search=' + encodeURIComponent( search ); } - return wp.apiRequest( { path: '/wp/v2/users' + payload } ); + return apiRequest( { path: '/wp/v2/users' + payload } ); }, isDebounced: true, getOptionKeywords( user ) { diff --git a/editor/components/post-taxonomies/flat-term-selector.js b/editor/components/post-taxonomies/flat-term-selector.js index 3357b6c5f2ed2..4931ca7fb5f2d 100644 --- a/editor/components/post-taxonomies/flat-term-selector.js +++ b/editor/components/post-taxonomies/flat-term-selector.js @@ -11,6 +11,7 @@ import { __, _x, sprintf } from '@wordpress/i18n'; import { Component, compose } from '@wordpress/element'; import { FormTokenField, withAPIData } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; +import apiRequest from '@wordpress/api-request'; /** * Module constants @@ -75,7 +76,7 @@ class FlatTermSelector extends Component { fetchTerms( params = {} ) { const query = { ...DEFAULT_QUERY, ...params }; const basePath = wp.api.getTaxonomyRoute( this.props.slug ); - const request = wp.apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( query ) }` } ); + const request = apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( query ) }` } ); request.then( ( terms ) => { this.setState( ( state ) => ( { availableTerms: state.availableTerms.concat( @@ -106,7 +107,7 @@ class FlatTermSelector extends Component { return new Promise( ( resolve, reject ) => { // Tries to create a term or fetch it if it already exists const basePath = wp.api.getTaxonomyRoute( this.props.slug ); - wp.apiRequest( { + apiRequest( { path: `/wp/v2/${ basePath }`, method: 'POST', data: { name: termName }, @@ -114,7 +115,7 @@ class FlatTermSelector extends Component { const errorCode = xhr.responseJSON && xhr.responseJSON.code; if ( errorCode === 'term_exists' ) { // search the new category created since last fetch - this.addRequest = wp.apiRequest( { + this.addRequest = apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( { ...DEFAULT_QUERY, search: termName } ) }`, } ); return this.addRequest.then( ( searchResult ) => { diff --git a/editor/components/post-taxonomies/hierarchical-term-selector.js b/editor/components/post-taxonomies/hierarchical-term-selector.js index 8686a28356292..7933f83b2a564 100644 --- a/editor/components/post-taxonomies/hierarchical-term-selector.js +++ b/editor/components/post-taxonomies/hierarchical-term-selector.js @@ -12,6 +12,7 @@ import { Component, compose } from '@wordpress/element'; import { TreeSelect, withAPIData, withInstanceId, withSpokenMessages, Button } from '@wordpress/components'; import { buildTermsTree } from '@wordpress/utils'; import { withSelect, withDispatch } from '@wordpress/data'; +import apiRequest from '@wordpress/api-request'; /** * Module Constants @@ -103,7 +104,7 @@ class HierarchicalTermSelector extends Component { } ); // Tries to create a term or fetch it if it already exists const basePath = wp.api.getTaxonomyRoute( this.props.slug ); - this.addRequest = wp.apiRequest( { + this.addRequest = apiRequest( { path: `/wp/v2/${ basePath }`, method: 'POST', data: { @@ -116,7 +117,7 @@ class HierarchicalTermSelector extends Component { const errorCode = xhr.responseJSON && xhr.responseJSON.code; if ( errorCode === 'term_exists' ) { // search the new category created since last fetch - this.addRequest = wp.apiRequest( { + this.addRequest = apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( { ...DEFAULT_QUERY, parent: formParent || 0, search: formName } ) }`, } ); return this.addRequest.then( ( searchResult ) => { @@ -161,7 +162,7 @@ class HierarchicalTermSelector extends Component { componentDidMount() { const basePath = wp.api.getTaxonomyRoute( this.props.slug ); - this.fetchRequest = wp.apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( DEFAULT_QUERY ) }` } ); + this.fetchRequest = apiRequest( { path: `/wp/v2/${ basePath }?${ stringify( DEFAULT_QUERY ) }` } ); this.fetchRequest.then( ( terms ) => { // resolve const availableTermsTree = buildTermsTree( terms ); diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 6067b90712cd8..b0df5adf751f6 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -75,7 +75,7 @@ class EditorProvider extends Component { [ APIProvider, { - ...wpApiSettings, + schema: wpApiSchema, ...pick( wp.api, [ 'postTypeRestBaseMapping', 'taxonomyRestBaseMapping', diff --git a/editor/components/url-input/index.js b/editor/components/url-input/index.js index 21d1e98ef5578..8f5887bca3cd1 100644 --- a/editor/components/url-input/index.js +++ b/editor/components/url-input/index.js @@ -13,6 +13,7 @@ import { __, sprintf, _n } from '@wordpress/i18n'; import { Component, Fragment } from '@wordpress/element'; import { keycodes, decodeEntities } from '@wordpress/utils'; import { Spinner, withInstanceId, withSpokenMessages, Popover } from '@wordpress/components'; +import apiRequest from '@wordpress/api-request'; const { UP, DOWN, ENTER } = keycodes; @@ -68,7 +69,7 @@ class UrlInput extends Component { selectedSuggestion: null, loading: true, } ); - this.suggestionsRequest = wp.apiRequest( { + this.suggestionsRequest = apiRequest( { path: `/wp/v2/posts?${ stringify( { search: value, per_page: 20, diff --git a/editor/store/effects.js b/editor/store/effects.js index 3570b8fedf64d..09749eca54a84 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -20,6 +20,7 @@ import { } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; +import apiRequest from '@wordpress/api-request'; /** * Internal dependencies @@ -110,7 +111,7 @@ export default { } ); dispatch( removeNotice( SAVE_POST_NOTICE_ID ) ); const basePath = wp.api.getPostTypeRoute( getCurrentPostType( state ) ); - wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, method: 'PUT', data: toSend } ).then( + apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, method: 'PUT', data: toSend } ).then( ( newPost ) => { dispatch( resetPost( newPost ) ); dispatch( { @@ -207,7 +208,7 @@ export default { const { postId } = action; const basePath = wp.api.getPostTypeRoute( getCurrentPostType( getState() ) ); dispatch( removeNotice( TRASH_POST_NOTICE_ID ) ); - wp.apiRequest( { path: `/wp/v2/${ basePath }/${ postId }`, method: 'DELETE' } ).then( + apiRequest( { path: `/wp/v2/${ basePath }/${ postId }`, method: 'DELETE' } ).then( () => { dispatch( { ...action, @@ -250,7 +251,7 @@ export default { context: 'edit', }; - wp.apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, data } ).then( + apiRequest( { path: `/wp/v2/${ basePath }/${ post.id }`, data } ).then( ( newPost ) => { dispatch( resetPost( newPost ) ); } @@ -399,9 +400,9 @@ export default { let result; if ( id ) { - result = wp.apiRequest( { path: `/wp/v2/${ basePath }/${ id }` } ); + result = apiRequest( { path: `/wp/v2/${ basePath }/${ id }` } ); } else { - result = wp.apiRequest( { path: `/wp/v2/${ basePath }?per_page=-1` } ); + result = apiRequest( { path: `/wp/v2/${ basePath }?per_page=-1` } ); } result.then( @@ -454,7 +455,7 @@ export default { const path = isTemporary ? `/wp/v2/${ basePath }` : `/wp/v2/${ basePath }/${ id }`; const method = isTemporary ? 'POST' : 'PUT'; - wp.apiRequest( { path, data, method } ).then( + apiRequest( { path, data, method } ).then( ( updatedSharedBlock ) => { dispatch( { type: 'SAVE_SHARED_BLOCK_SUCCESS', @@ -510,7 +511,7 @@ export default { sharedBlock.uid, ] ) ); - wp.apiRequest( { path: `/wp/v2/${ basePath }/${ id }`, method: 'DELETE' } ).then( + apiRequest( { path: `/wp/v2/${ basePath }/${ id }`, method: 'DELETE' } ).then( () => { dispatch( { type: 'DELETE_SHARED_BLOCK_SUCCESS', diff --git a/lib/client-assets.php b/lib/client-assets.php index 7afeffad4da25..0a0c2e4f4e635 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -123,11 +123,23 @@ function gutenberg_register_scripts_and_styles() { filemtime( gutenberg_dir_path() . 'build/api-request/index.js' ), true ); - wp_localize_script( 'wp-api-request', 'wpApiSettings', array( - 'root' => esc_url_raw( get_rest_url() ), - 'nonce' => ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ), - 'versionString' => 'wp/v2/', - ) ); + wp_add_inline_script( + 'wp-api-request', + sprintf( + 'wp.apiRequest.use( wp.apiRequest.createNonceMiddleware( "%s" ) );', + ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ) + ), + 'after' + ); + wp_add_inline_script( + 'wp-api-request', + sprintf( + 'wp.apiRequest.use( wp.apiRequest.createRootURLMiddleware( "%s" ) );', + esc_url_raw( get_rest_url() ) + ), + 'after' + ); + wp_register_script( 'wp-deprecated', gutenberg_url( 'build/deprecated/index.js' ), @@ -166,7 +178,7 @@ function gutenberg_register_scripts_and_styles() { wp_register_script( 'wp-utils', gutenberg_url( 'build/utils/index.js' ), - array( 'lodash', 'wp-blob', 'wp-deprecated', 'wp-dom' ), + array( 'lodash', 'wp-blob', 'wp-deprecated', 'wp-dom', 'wp-api-request' ), filemtime( gutenberg_dir_path() . 'build/utils/index.js' ), true ); @@ -268,6 +280,7 @@ function gutenberg_register_scripts_and_styles() { 'wp-i18n', 'wp-utils', 'wp-viewport', + 'wp-api-request', ), filemtime( gutenberg_dir_path() . 'build/core-blocks/index.js' ), true @@ -351,6 +364,7 @@ function gutenberg_register_scripts_and_styles() { 'postbox', 'wp-a11y', 'wp-api', + 'wp-api-request', 'wp-blob', 'wp-blocks', 'wp-components', @@ -381,6 +395,7 @@ function gutenberg_register_scripts_and_styles() { 'media-models', 'media-views', 'wp-a11y', + 'wp-api-request', 'wp-components', 'wp-core-blocks', 'wp-date', @@ -747,7 +762,7 @@ function gutenberg_extend_wp_api_backbone_client() { $schema_response = rest_do_request( new WP_REST_Request( 'GET', '/' ) ); if ( ! $schema_response->is_error() ) { wp_add_inline_script( 'wp-api', sprintf( - 'wpApiSettings.cacheSchema = true; wpApiSettings.schema = %s;', + 'wpApiSchema = %s;', wp_json_encode( $schema_response->get_data() ) ), 'before' ); } @@ -977,9 +992,9 @@ function gutenberg_editor_scripts_and_styles( $hook ) { ); wp_add_inline_script( - 'wp-components', - sprintf( 'window._wpAPIDataPreload = %s', wp_json_encode( $preload_data ) ), - 'before' + 'wp-api-request', + sprintf( 'wp.apiRequest.use( wp.apiRequest.createPreloadingMiddleware( %s ) );', wp_json_encode( $preload_data ) ), + 'after' ); // Initialize the post data. diff --git a/lib/compat.php b/lib/compat.php index ad72ef60d3349..44b2c8d3f94b4 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -69,123 +69,6 @@ function _gutenberg_utf8_split( $str ) { return $chars; } -/** - * Shims fix for apiRequest on sites configured to use plain permalinks and add Preloading support. - * - * @see https://core.trac.wordpress.org/ticket/42382 - * - * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). - */ -function gutenberg_shim_api_request( $scripts ) { - $api_request_fix = <<add_inline_script( 'wp-api-request', $api_request_fix, 'after' ); -} -add_action( 'wp_default_scripts', 'gutenberg_shim_api_request' ); - -/** - * Shims support for emulating HTTP/1.0 requests in wp.apiRequest - * - * @see https://core.trac.wordpress.org/ticket/43605 - * - * @param WP_Scripts $scripts WP_Scripts instance (passed by reference). - */ -function gutenberg_shim_api_request_emulate_http( $scripts ) { - $api_request_fix = <<= 0 ) { - if ( ! options.headers ) { - options.headers = {}; - } - options.headers['X-HTTP-Method-Override'] = options.method; - options.method = 'POST'; - - options.contentType = 'application/json'; - options.data = JSON.stringify( options.data ); - } - } - - return oldApiRequest( options ); - } -} )( window.wp ); -JS; - - $scripts->add_inline_script( 'wp-api-request', $api_request_fix, 'after' ); -} -add_action( 'wp_default_scripts', 'gutenberg_shim_api_request_emulate_http' ); - /** * Disables wpautop behavior in classic editor when post contains blocks, to * prevent removep from invalidating paragraph blocks. diff --git a/packages/api-request/README.md b/packages/api-request/README.md index 6e6a708834e8d..220096dfe84fa 100644 --- a/packages/api-request/README.md +++ b/packages/api-request/README.md @@ -20,4 +20,23 @@ apiRequest( { path: '/wp/v2/posts' } ).then( posts => { } ); ``` +### Middlewares + +the `api-request` package supports middlewares. Middlewares are functions you can use to wrap the `wp.apiRequest` calls to perform any pre/post process to the API requests. + +**Example** + +```js +wp.apiRequest.use( ( options, next ) => { + const start = Date.now(); + const result = next( options ); + result.then( () => { + console.log( 'The request took ' + Date.now() - start ); + } ); + return result; +} ); +``` + +The apiRequest package provides built-in middlewares you can use to provide a `nonce` and a custom `rootURL`. +

Code is Poetry.

diff --git a/packages/api-request/src/http-v1-middleware.js b/packages/api-request/src/http-v1-middleware.js new file mode 100644 index 0000000000000..b9552bafa8210 --- /dev/null +++ b/packages/api-request/src/http-v1-middleware.js @@ -0,0 +1,19 @@ +function httpV1Middleware( options, next ) { + const newOptions = { ...options }; + if ( newOptions.method ) { + if ( [ 'PATCH', 'PUT', 'DELETE' ].indexOf( newOptions.method.toUpperCase() ) >= 0 ) { + if ( ! newOptions.headers ) { + options.headers = {}; + } + newOptions.headers[ 'X-HTTP-Method-Override' ] = newOptions.method; + newOptions.method = 'POST'; + + newOptions.contentType = 'application/json'; + newOptions.data = JSON.stringify( newOptions.data ); + } + } + + return next( newOptions, next ); +} + +export default httpV1Middleware; diff --git a/packages/api-request/src/index.js b/packages/api-request/src/index.js index 2641359d9b08b..1695910741336 100644 --- a/packages/api-request/src/index.js +++ b/packages/api-request/src/index.js @@ -3,82 +3,41 @@ */ import jQuery from 'jquery'; -const wpApiSettings = window.wpApiSettings; - -function apiRequest( options ) { - options = apiRequest.buildAjaxOptions( options ); - return apiRequest.transport( options ); -} - -apiRequest.buildAjaxOptions = function( options ) { - let url = options.url; - let path = options.path; - let namespaceTrimmed, endpointTrimmed, apiRoot; - let headers, addNonceHeader, headerName; - - if ( - typeof options.namespace === 'string' && - typeof options.endpoint === 'string' - ) { - namespaceTrimmed = options.namespace.replace( /^\/|\/$/g, '' ); - endpointTrimmed = options.endpoint.replace( /^\//, '' ); - if ( endpointTrimmed ) { - path = namespaceTrimmed + '/' + endpointTrimmed; - } else { - path = namespaceTrimmed; - } - } - if ( typeof path === 'string' ) { - apiRoot = wpApiSettings.root; - path = path.replace( /^\//, '' ); - - // API root may already include query parameter prefix if site is - // configured to use plain permalinks. - if ( 'string' === typeof apiRoot && -1 !== apiRoot.indexOf( '?' ) ) { - path = path.replace( '?', '&' ); - } - - url = apiRoot + path; - } - - // If ?_wpnonce=... is present, no need to add a nonce header. - addNonceHeader = ! ( options.data && options.data._wpnonce ); - - headers = options.headers || {}; - - // If an 'X-WP-Nonce' header (or any case-insensitive variation - // thereof) was specified, no need to add a nonce header. - if ( addNonceHeader ) { - for ( headerName in headers ) { - if ( headers.hasOwnProperty( headerName ) ) { - if ( headerName.toLowerCase() === 'x-wp-nonce' ) { - addNonceHeader = false; - break; - } - } - } - } +/** + * Internal dependencies + */ +import createNonceMiddleware from './nonce-middleware'; +import createRootURLMiddleware from './root-url-middleware'; +import createPreloadingMiddleware from './preloading-middleware'; +import namespaceEndpointMiddleware from './namespace-endpoint-middleware'; +import httpV1Middleware from './http-v1-middleware'; - if ( addNonceHeader ) { - // Do not mutate the original headers object, if any. - headers = jQuery.extend( { - 'X-WP-Nonce': wpApiSettings.nonce, - }, headers ); - } +const middlewares = []; - // Do not mutate the original options object. - options = jQuery.extend( {}, options, { - headers: headers, - url: url, - } ); +function registerMiddleware( middleware ) { + middlewares.push( middleware ); +} - delete options.path; - delete options.namespace; - delete options.endpoint; +function apiRequest( options ) { + const raw = ( nextOptions ) => jQuery.ajax( nextOptions ); + const steps = [ + ...middlewares, + namespaceEndpointMiddleware, + httpV1Middleware, + raw, + ].reverse(); + const next = ( nextOptions ) => { + const nextMiddleware = steps.pop(); + return nextMiddleware( nextOptions, next ); + }; + + return next( options ); +} - return options; -}; +apiRequest.use = registerMiddleware; -apiRequest.transport = jQuery.ajax; +apiRequest.createNonceMiddleware = createNonceMiddleware; +apiRequest.createPreloadingMiddleware = createPreloadingMiddleware; +apiRequest.createRootURLMiddleware = createRootURLMiddleware; export default apiRequest; diff --git a/packages/api-request/src/namespace-endpoint-middleware.js b/packages/api-request/src/namespace-endpoint-middleware.js new file mode 100644 index 0000000000000..c6ee6c67b4afb --- /dev/null +++ b/packages/api-request/src/namespace-endpoint-middleware.js @@ -0,0 +1,27 @@ +const namespaceAndEndpointMiddleware = ( options, next ) => { + let path = options.path; + let namespaceTrimmed, endpointTrimmed; + + if ( + typeof options.namespace === 'string' && + typeof options.endpoint === 'string' + ) { + namespaceTrimmed = options.namespace.replace( /^\/|\/$/g, '' ); + endpointTrimmed = options.endpoint.replace( /^\//, '' ); + if ( endpointTrimmed ) { + path = namespaceTrimmed + '/' + endpointTrimmed; + } else { + path = namespaceTrimmed; + } + } + + delete options.namespace; + delete options.endpoint; + + return next( { + ...options, + path, + } ); +}; + +export default namespaceAndEndpointMiddleware; diff --git a/packages/api-request/src/nonce-middleware.js b/packages/api-request/src/nonce-middleware.js new file mode 100644 index 0000000000000..372881f8ffd3f --- /dev/null +++ b/packages/api-request/src/nonce-middleware.js @@ -0,0 +1,46 @@ +const createNonceMiddleware = ( nonce ) => ( options, next ) => { + let usedNonce = nonce; + /** + * This is not ideal but it's fine for now. + * + * Configure heartbeat to refresh the wp-api nonce, keeping the editor + * authorization intact. + */ + window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => { + if ( response[ 'rest-nonce' ] ) { + usedNonce = response[ 'rest-nonce' ]; + } + } ); + + // If ?_wpnonce=... is present, no need to add a nonce header. + let addNonceHeader = ! ( options.data && options.data._wpnonce ); + let headers = options.headers || {}; + + // If an 'X-WP-Nonce' header (or any case-insensitive variation + // thereof) was specified, no need to add a nonce header. + if ( addNonceHeader ) { + for ( const headerName in headers ) { + if ( headers.hasOwnProperty( headerName ) ) { + if ( headerName.toLowerCase() === 'x-wp-nonce' ) { + addNonceHeader = false; + break; + } + } + } + } + + if ( addNonceHeader ) { + // Do not mutate the original headers object, if any. + headers = { + ...headers, + 'X-WP-Nonce': usedNonce, + }; + } + + return next( { + ...options, + headers, + } ); +}; + +export default createNonceMiddleware; diff --git a/packages/api-request/src/preloading-middleware.js b/packages/api-request/src/preloading-middleware.js new file mode 100644 index 0000000000000..73491fa8fe609 --- /dev/null +++ b/packages/api-request/src/preloading-middleware.js @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import jQuery from 'jquery'; + +const createPreloadingMiddleware = ( preloadedData ) => ( options, next ) => { + function getStablePath( path ) { + const splitted = path.split( '?' ); + const query = splitted[ 1 ]; + const base = splitted[ 0 ]; + if ( ! query ) { + return base; + } + + // 'b=1&c=2&a=5' + return base + '?' + query + // [ 'b=1', 'c=2', 'a=5' ] + .split( '&' ) + // [ [ 'b, '1' ], [ 'c', '2' ], [ 'a', '5' ] ] + .map( function( entry ) { + return entry.split( '=' ); + } ) + // [ [ 'a', '5' ], [ 'b, '1' ], [ 'c', '2' ] ] + .sort( function( a, b ) { + return a[ 0 ].localeCompare( b[ 0 ] ); + } ) + // [ 'a=5', 'b=1', 'c=2' ] + .map( function( pair ) { + return pair.join( '=' ); + } ) + // 'a=5&b=1&c=2' + .join( '&' ); + } + + if ( typeof options.path === 'string' ) { + const method = options.method || 'GET'; + const path = getStablePath( options.path ); + + if ( 'GET' === method && preloadedData[ path ] ) { + const deferred = jQuery.Deferred(); + deferred.resolve( preloadedData[ path ].body ); + return deferred.promise(); + } + } + + return next( options ); +}; + +export default createPreloadingMiddleware; diff --git a/packages/api-request/src/root-url-middleware.js b/packages/api-request/src/root-url-middleware.js new file mode 100644 index 0000000000000..8d48e798525ee --- /dev/null +++ b/packages/api-request/src/root-url-middleware.js @@ -0,0 +1,34 @@ +import namespaceAndEndpointMiddleware from './namespace-endpoint-middleware'; + +const createRootURLMiddleware = ( rootURL ) => ( options, next ) => { + return namespaceAndEndpointMiddleware( options, ( optionsWithPath ) => { + let url = optionsWithPath.url; + let path = optionsWithPath.path; + let apiRoot; + + if ( typeof path === 'string' ) { + apiRoot = rootURL; + + if ( -1 !== rootURL.indexOf( '?' ) ) { + path = path.replace( '?', '&' ); + } + + path = path.replace( /^\//, '' ); + + // API root may already include query parameter prefix if site is + // configured to use plain permalinks. + if ( 'string' === typeof apiRoot && -1 !== apiRoot.indexOf( '?' ) ) { + path = path.replace( '?', '&' ); + } + + url = apiRoot + path; + } + + return next( { + ...optionsWithPath, + url, + } ); + } ); +}; + +export default createRootURLMiddleware; diff --git a/utils/mediaupload.js b/utils/mediaupload.js index 29b3863bb5ea0..c42a4996a1c4a 100644 --- a/utils/mediaupload.js +++ b/utils/mediaupload.js @@ -3,6 +3,11 @@ */ import { compact, forEach, get, noop, startsWith } from 'lodash'; +/** + * WordPress dependencies + */ +import apiRequest from '@wordpress/api-request'; + /** * Media Upload is used by audio, image, gallery and video blocks to handle uploading a media file * when a file upload button is activated. @@ -81,7 +86,7 @@ function createMediaFromFile( file, additionalData ) { const data = new window.FormData(); data.append( 'file', file, file.name || file.type.replace( '/', '.' ) ); forEach( additionalData, ( ( value, key ) => data.append( key, value ) ) ); - return wp.apiRequest( { + return apiRequest( { path: '/wp/v2/media', data, contentType: false, diff --git a/webpack.config.js b/webpack.config.js index c7f497475ab45..41fb332fda795 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -273,7 +273,7 @@ const config = { return path; }, } ), - new LibraryExportDefaultPlugin( [ 'deprecated', 'dom-ready' ].map( camelCaseDash ) ), + new LibraryExportDefaultPlugin( [ 'deprecated', 'dom-ready', 'api-request' ].map( camelCaseDash ) ), ], stats: { children: false,