diff --git a/assets/js/sync-ui/config.js b/assets/js/sync-ui/config.js index b667ebdfd0..5420001bbc 100644 --- a/assets/js/sync-ui/config.js +++ b/assets/js/sync-ui/config.js @@ -3,7 +3,7 @@ */ const { auto_start_index: autoIndex, - ajax_url: ajaxUrl, + api_url: apiUrl, index_meta: indexMeta = null, is_epio: isEpio, ep_last_sync_date: lastSyncDateTime = null, @@ -11,4 +11,4 @@ const { nonce, } = window.epDash; -export { autoIndex, ajaxUrl, indexMeta, isEpio, lastSyncDateTime, lastSyncFailed, nonce }; +export { autoIndex, apiUrl, indexMeta, isEpio, lastSyncDateTime, lastSyncFailed, nonce }; diff --git a/assets/js/sync-ui/index.js b/assets/js/sync-ui/index.js index ebcb5a251f..9a4421a6c8 100644 --- a/assets/js/sync-ui/index.js +++ b/assets/js/sync-ui/index.js @@ -8,7 +8,7 @@ import { SyncProvider } from '../sync'; * Internal dependencies. */ import { - ajaxUrl, + apiUrl, autoIndex, lastSyncDateTime, lastSyncFailed, @@ -25,7 +25,7 @@ import SettingsPage from './apps/settings-page'; */ const App = () => ( { diff --git a/assets/js/sync/src/hooks.js b/assets/js/sync/src/hooks.js index 114fd36f66..4304609a8e 100644 --- a/assets/js/sync/src/hooks.js +++ b/assets/js/sync/src/hooks.js @@ -12,11 +12,11 @@ import { __ } from '@wordpress/i18n'; * interrupt eachother to avoid multiple sync requests causing race conditions * or duplicate output, such as by rapidly pausing and unpausing indexing. * - * @param {string} ajaxUrl AJAX endpoint URL. + * @param {string} apiUrl AJAX endpoint URL. * @param {string} nonce WordPress nonce. * @returns {object} Sync, sync status, and cancel functions. */ -export const useIndex = (ajaxUrl, nonce) => { +export const useIndex = (apiUrl, nonce) => { const abort = useRef(new AbortController()); const request = useRef(null); @@ -119,16 +119,17 @@ export const useIndex = (ajaxUrl, nonce) => { * Silently catches abort errors and clears the current request on * completion. * + * @param {URL} url API URL. * @param {object} options Request options. * @throws {Error} Any non-abort errors. * @returns {Promise} Current request promise. */ - (options) => { - request.current = fetch(ajaxUrl, options).then(onResponse).finally(onComplete); + (url, options) => { + request.current = fetch(url, options).then(onResponse).finally(onComplete); return request.current; }, - [ajaxUrl, onComplete, onResponse], + [onComplete, onResponse], ); const cancelIndex = useCallback( @@ -141,20 +142,19 @@ export const useIndex = (ajaxUrl, nonce) => { abort.current.abort(); abort.current = new AbortController(); - const body = new FormData(); - - body.append('action', 'ep_cancel_index'); - body.append('nonce', nonce); + const url = new URL(apiUrl); const options = { - method: 'POST', - body, + headers: { + 'X-WP-Nonce': nonce, + }, + method: 'DELETE', signal: abort.current.signal, }; - return sendRequest(options); + return sendRequest(url, options); }, - [nonce, sendRequest], + [apiUrl, nonce, sendRequest], ); const index = useCallback( @@ -168,21 +168,21 @@ export const useIndex = (ajaxUrl, nonce) => { abort.current.abort(); abort.current = new AbortController(); - const body = new FormData(); + const url = new URL(apiUrl); - body.append('action', 'ep_index'); - body.append('put_mapping', putMapping ? 1 : 0); - body.append('nonce', nonce); + url.searchParams.append('put_mapping', putMapping); const options = { + headers: { + 'X-WP-Nonce': nonce, + }, method: 'POST', - body, signal: abort.current.signal, }; - return sendRequest(options); + return sendRequest(url, options); }, - [nonce, sendRequest], + [apiUrl, nonce, sendRequest], ); const indexStatus = useCallback( @@ -195,20 +195,19 @@ export const useIndex = (ajaxUrl, nonce) => { abort.current.abort(); abort.current = new AbortController(); - const body = new FormData(); - - body.append('action', 'ep_index_status'); - body.append('nonce', nonce); + const url = new URL(apiUrl); const options = { - method: 'POST', - body, + headers: { + 'X-WP-Nonce': nonce, + }, + method: 'GET', signal: abort.current.signal, }; - return sendRequest(options); + return sendRequest(url, options); }, - [nonce, sendRequest], + [apiUrl, nonce, sendRequest], ); return { cancelIndex, index, indexStatus }; diff --git a/includes/classes/REST/Sync.php b/includes/classes/REST/Sync.php new file mode 100644 index 0000000000..e36db136cd --- /dev/null +++ b/includes/classes/REST/Sync.php @@ -0,0 +1,215 @@ + $this->get_args_schema(), + 'callback' => [ $this, 'sync' ], + 'methods' => 'POST', + 'permission_callback' => [ $this, 'sync_permissions_check' ], + ] + ); + + register_rest_route( + 'elasticpress/v1', + 'sync', + [ + 'callback' => [ $this, 'get_sync_status' ], + 'methods' => 'GET', + 'permission_callback' => [ $this, 'sync_permissions_check' ], + ] + ); + + register_rest_route( + 'elasticpress/v1', + 'sync', + [ + 'callback' => [ $this, 'cancel_sync' ], + 'methods' => 'DELETE', + 'permission_callback' => [ $this, 'sync_permissions_check' ], + ] + ); + } + + /** + * Get args schema. + * + * @return array + */ + public function get_args_schema() { + return [ + 'include' => [ + 'items' => [ + 'type' => 'integer', + ], + 'type' => 'array', + ], + 'indexables' => [ + 'items' => [ + 'type' => 'string', + ], + 'required' => false, + 'type' => 'array', + ], + 'lower_limit_object_id' => [ + 'type' => 'integer', + 'required' => false, + ], + 'offset' => [ + 'required' => false, + 'type' => 'integer', + ], + 'post_type' => [ + 'items' => [ + 'type' => 'string', + ], + 'type' => 'array', + ], + 'put_mapping' => [ + 'default' => false, + 'type' => 'boolean', + 'required' => false, + ], + 'upper_limit_object_id' => [ + 'type' => 'integer', + 'required' => false, + ], + ]; + } + + /** + * Check that the request has permission to sync. + * + * @return boolean + */ + public function sync_permissions_check() { + $capability = Utils\get_capability(); + + return current_user_can( $capability ); + } + + /** + * Start or continue a sync. + * + * @param \WP_REST_Request $request Full details about the request. + * @return void + */ + public function sync( \WP_REST_Request $request ) { + $index_meta = Utils\get_indexing_status(); + + if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { + $this->get_sync_status( $request ); + exit; + } + + $args = array_merge( + [ + 'method' => 'dashboard', + 'network_wide' => 0, + 'output_method' => [ $this, 'output' ], + 'show_errors' => true, + ], + $request->get_params() + ); + + IndexHelper::factory()->full_index( $args ); + } + + /** + * Output the result of indexing. + * + * @param array $message Index details. + * @return void + */ + public function output( array $message ) { + switch ( $message['status'] ) { + case 'success': + wp_send_json_success( $message ); + break; + case 'error': + wp_send_json_error( $message ); + break; + default: + wp_send_json( [ 'data' => $message ] ); + break; + } + } + + /** + * Get the status of a sync in progress. + * + * @param \WP_REST_Request $request Full details about the request. + * @return void + */ + public function get_sync_status( \WP_REST_Request $request ) { + $index_meta = Utils\get_indexing_status(); + + if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { + wp_send_json_success( + [ + 'message' => sprintf( + /* translators: 1. Number of objects indexed, 2. Total number of objects, 3. Last object ID. */ + esc_html__( 'Processed %1$d/%2$d. Last Object ID: %3$d', 'elasticpress' ), + $index_meta['offset'], + $index_meta['found_items'], + $index_meta['current_sync_item']['last_processed_object_id'] + ), + 'index_meta' => $index_meta, + ] + ); + } + + wp_send_json_success( + [ + 'is_finished' => true, + 'totals' => Utils\get_option( 'ep_last_index' ), + ] + ); + } + + /** + * Cancel a sync in progress. + * + * @param \WP_REST_Request $request Full details about the request. + * @return void + */ + public function cancel_sync( \WP_REST_Request $request ) { + $index_meta = Utils\get_indexing_status(); + + if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { + set_transient( 'ep_wpcli_sync_interrupted', true, MINUTE_IN_SECONDS ); + wp_send_json_success(); + exit; + } + + Utils\delete_option( 'ep_index_meta' ); + + wp_send_json_success(); + } +} diff --git a/includes/classes/Screen/Sync.php b/includes/classes/Screen/Sync.php index ea8dab96fa..bbaa852d40 100644 --- a/includes/classes/Screen/Sync.php +++ b/includes/classes/Screen/Sync.php @@ -8,11 +8,12 @@ namespace ElasticPress\Screen; +use ElasticPress\Elasticsearch; use ElasticPress\IndexHelper; +use ElasticPress\REST; use ElasticPress\Screen; -use ElasticPress\Utils; use ElasticPress\Stats; -use ElasticPress\Elasticsearch; +use ElasticPress\Utils; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -29,100 +30,8 @@ class Sync { * Initialize class */ public function setup() { - add_action( 'wp_ajax_ep_index', [ $this, 'action_wp_ajax_ep_index' ] ); - add_action( 'wp_ajax_ep_index_status', [ $this, 'action_wp_ajax_ep_index_status' ] ); - add_action( 'wp_ajax_ep_cancel_index', [ $this, 'action_wp_ajax_ep_cancel_index' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); - } - - /** - * Getting the status of ongoing index fired by WP CLI - * - * @since 3.6.0 - */ - public function action_wp_ajax_ep_index_status() { - if ( ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) || ! EP_DASHBOARD_SYNC ) { - wp_send_json_error( null, 403 ); - exit; - } - - $index_meta = Utils\get_indexing_status(); - - if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { - wp_send_json_success( - [ - 'message' => sprintf( - /* translators: 1. Number of objects indexed, 2. Total number of objects, 3. Last object ID */ - esc_html__( 'Processed %1$d/%2$d. Last Object ID: %3$d', 'elasticpress' ), - $index_meta['offset'], - $index_meta['found_items'], - $index_meta['current_sync_item']['last_processed_object_id'] - ), - 'index_meta' => $index_meta, - ] - ); - } - - wp_send_json_success( - [ - 'is_finished' => true, - 'totals' => Utils\get_option( 'ep_last_index' ), - ] - ); - } - - /** - * Perform index - * - * @since 3.6.0 - */ - public function action_wp_ajax_ep_index() { - if ( ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) || ! EP_DASHBOARD_SYNC ) { - wp_send_json_error( null, 403 ); - exit; - } - - $index_meta = Utils\get_indexing_status(); - - if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { - $this->action_wp_ajax_ep_index_status(); - exit; - } - - IndexHelper::factory()->full_index( - [ - 'method' => 'dashboard', - 'put_mapping' => ! empty( $_REQUEST['put_mapping'] ), - 'output_method' => [ $this, 'index_output' ], - 'show_errors' => true, - 'network_wide' => 0, - ] - ); - } - - /** - * Cancel index - * - * @since 3.6.0 - */ - public function action_wp_ajax_ep_cancel_index() { - if ( ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) || ! EP_DASHBOARD_SYNC ) { - wp_send_json_error( null, 403 ); - exit; - } - - $index_meta = Utils\get_indexing_status(); - - if ( isset( $index_meta['method'] ) && 'cli' === $index_meta['method'] ) { - set_transient( 'ep_wpcli_sync_interrupted', true, MINUTE_IN_SECONDS ); - wp_send_json_success(); - exit; - } - - Utils\delete_option( 'ep_index_meta' ); - - wp_send_json_success(); + add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); } /** @@ -156,7 +65,7 @@ public function admin_enqueue_scripts() { Utils\get_asset_info( 'sync-styles', 'version' ) ); - $data = array( 'nonce' => wp_create_nonce( 'ep_dashboard_nonce' ) ); + $data = array( 'nonce' => wp_create_nonce( 'wp_rest' ) ); $index_meta = Utils\get_indexing_status(); $last_sync = Utils\get_option( 'ep_last_sync', false ); @@ -214,7 +123,7 @@ public function admin_enqueue_scripts() { ] ); - $data['ajax_url'] = admin_url( 'admin-ajax.php' ); + $data['api_url'] = rest_url( 'elasticpress/v1/sync' ); $data['install_sync'] = empty( $last_sync ); $data['install_complete_url'] = esc_url( $install_complete_url ); $data['sync_complete'] = esc_html__( 'Sync complete', 'elasticpress' ); @@ -230,24 +139,13 @@ public function admin_enqueue_scripts() { } /** - * Output information received from the index helper class. + * Register REST API routes. * - * @param array $message Message to be outputted with its status and additional info, if needed. + * @since 5.0.0 + * @return void */ - public static function index_output( $message ) { - switch ( $message['status'] ) { - case 'success': - wp_send_json_success( $message ); - break; - - case 'error': - wp_send_json_error( $message ); - break; - - default: - wp_send_json( [ 'data' => $message ] ); - break; - } - exit; + public function register_rest_routes() { + $controller = new REST\Sync(); + $controller->register_routes(); } } diff --git a/tests/cypress/integration/dashboard-sync.cy.js b/tests/cypress/integration/dashboard-sync.cy.js index 93d56942f0..2cd0e11555 100644 --- a/tests/cypress/integration/dashboard-sync.cy.js +++ b/tests/cypress/integration/dashboard-sync.cy.js @@ -145,9 +145,9 @@ describe('Dashboard Sync', () => { cy.visitAdminPage('admin.php?page=elasticpress-sync'); // Start sync via dashboard and pause it - cy.intercept('POST', '/wp-admin/admin-ajax.php*').as('ajaxRequest'); + cy.intercept('POST', '/wp-json/elasticpress/v1/sync*').as('apiRequest'); cy.get('.ep-sync-button--sync').click(); - cy.wait('@ajaxRequest').its('response.statusCode').should('eq', 200); + cy.wait('@apiRequest').its('response.statusCode').should('eq', 200); cy.get('.ep-sync-button--pause').should('be.visible'); // Can not activate a feature.