diff --git a/.gitignore b/.gitignore index eaa2b07..78dfeb5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ tests/wordpress Thumbs.db /tests/compatibility/*.txt .DS_Store +.vscode inc/sandbox.inc *.sh diff --git a/src/Plugin.php b/src/Plugin.php index 3d531a6..65136da 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -8,6 +8,10 @@ class Plugin { // ElasticPrtess loads its features at plugins_loaded:10. const INIT_PRIORITY = 11; + // Before ElasticPress 5.0.0, the Dashboard sync was run over AJAX. + // After ElasticPress 5.0.0, the Dashboard sync is run over the REST API. + const DASHBOARD_SYNC_API_CHANGE_V1 = '5.0.0'; + public static function init() { add_action( 'plugins_loaded', function () { if ( ! defined( 'EP_VERSION' ) || version_compare( EP_VERSION, '3.0.0', '<' ) ) { @@ -18,6 +22,8 @@ public static function init() { return; } + $elasticPressVersion = EP_VERSION; + $activeLanguagesData = apply_filters( 'wpml_active_languages', [] ); $activeLanguages = array_keys( $activeLanguagesData ); $defaultLanguage = apply_filters( 'wpml_default_language', '' ); @@ -37,6 +43,26 @@ public static function init() { ); $indicesManager->addHooks(); + $syncDashboard = version_compare( $elasticPressVersion, self::DASHBOARD_SYNC_API_CHANGE_V1, '<' ) + ? new Sync\DashboardAjax( + $indexables, + $indicesManager, + new Manager\DashboardStatus( + $activeLanguages + ), + $activeLanguages, + $defaultLanguage + ) + : new Sync\DashboardRest( + $indexables, + $indicesManager, + new Manager\DashboardStatus( + $activeLanguages + ), + $activeLanguages, + $defaultLanguage + ); + $feature = new Feature( new Field\Search( $elasticsearchVersion, @@ -50,20 +76,13 @@ public static function init() { $defaultLanguage, $currentLanguage ), - new Sync\Dashboard( - $indexables, - $indicesManager, - new Manager\DashboardStatus( - $activeLanguages - ), - $activeLanguages, - $defaultLanguage - ), + $syncDashboard, new Sync\Singular( $indexables, $indicesManager, $activeLanguages, - $defaultLanguage + $defaultLanguage, + $elasticPressVersion ), new Sync\CLI( $indexables, diff --git a/src/Sync/Dashboard.php b/src/Sync/Dashboard.php index 5659411..70a68e5 100644 --- a/src/Sync/Dashboard.php +++ b/src/Sync/Dashboard.php @@ -11,7 +11,7 @@ use WPML\ElasticPress\Traits\ManageIndexables; -class Dashboard { +abstract class Dashboard { use ManageIndexables; @@ -19,13 +19,13 @@ class Dashboard { private $indexables; /** @var Indices */ - private $indicesManager; + protected $indicesManager; /** @var DashboardStatus */ - private $status; + protected $status; /** @var array */ - private $activeLanguages; + protected $activeLanguages; /** @var string */ private $defaultLanguage; @@ -33,6 +33,9 @@ class Dashboard { /** @var string */ private $currentLanguage = ''; + /** @var array */ + private $fullIndexArgs = []; + /** * @param Indexables $indexables * @param Indices $indicesManager @@ -54,20 +57,30 @@ public function __construct( $this->defaultLanguage = $defaultLanguage; } - public function addHooks() { - if ( 0 === count( $this->activeLanguages ) ) { + abstract public function addHooks(); + + /** + * @param array $args + */ + protected function setFullIndexArgs( $args ) { + $this->fullIndexArgs = $args; + } + + protected function setUpAndRun() { + $this->prepare(); + if ( empty( $this->status->get('currentLanguage') ) ) { return; } + $this->maybePutMapping(); + $this->beforeFullIndex(); - add_action( 'wp_ajax_ep_index', [ $this, 'action_wp_ajax_ep_index' ], 9 ); - add_action( 'wp_ajax_ep_cancel_index', [ $this, 'action_wp_ajax_ep_cancel_index' ], 9 ); - } + // This happens on an AJAX call, hence on admin: force the display-as-translated snippet in queries + add_filter( 'wpml_should_use_display_as_translated_snippet', '__return_true' ); - private function setUp() { - $this->status->prepare(); + $this->runFullIndex( $this->fullIndexArgs ); } - private function tearDown() { + protected function tearDown() { $this->indicesManager->clearCurrentIndexLanguage(); $this->status->delete(); } @@ -82,25 +95,14 @@ private function clearCurrentLanguage() { $this->indicesManager->clearCurrentIndexLanguage(); } - private function isDashboardSync() { - 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'] ) { - return false; - } - - return true; + private function prepare() { + $this->status->prepare(); } private function maybePutMapping() { $putMapping = ! empty( $_REQUEST['put_mapping'] ); - if ( $putMapping && false === $this->status->get('putMapping') ) { + if ( ( $putMapping || $forcePutMapping ) && false === $this->status->get('putMapping') ) { $this->indicesManager->clearAllIndices(); $this->status->set('putMapping', true); } @@ -116,44 +118,31 @@ private function beforeFullIndex() { $this->status->logIndexablesToReset( $this->deactivateIndexables() ); } - public function action_wp_ajax_ep_cancel_index() { - if ( false === $this->isDashboardSync() ) { - return; - } - $this->tearDown(); - } - - public function action_wp_ajax_ep_index() { - if ( false === $this->isDashboardSync() ) { - return; - } - $this->setUp(); - if ( empty( $this->status->get('currentLanguage') ) ) { - return; - } - $this->maybePutMapping(); - $this->beforeFullIndex(); - - // This happens on an AJAX call, hence on admin: force the display-as-translated snippet in queries - add_filter( 'wpml_should_use_display_as_translated_snippet', '__return_true' ); - - IndexHelper::factory()->full_index( + /** + * @param array $forcedArgs + */ + private function runFullIndex( $forcedArgs = [] ) { + $args = array_merge( [ 'method' => 'dashboard', - 'put_mapping' => false, - 'output_method' => [ $this, 'indexOutput' ], - 'show_errors' => true, 'network_wide' => 0, - ] + 'show_errors' => true, + ], + $forcedArgs ); + + $args['put_mapping'] = false; + $args['output_method'] = [ $this, 'indexOutput' ]; + + IndexHelper::factory()->full_index( $args ); } private function syncComplete() { $message = [ - 'message' => 'Sync complete', + 'message' => 'Sync complete', 'index_meta' => null, - 'totals' => $this->status->get('totals'), - 'status' => 'success' + 'totals' => $this->status->get('totals'), + 'status' => 'success' ]; $this->tearDown(); wp_send_json_success( $message ); @@ -200,9 +189,9 @@ private function setLanguageCompletedResponse( $message ) { // Hijack the message data so the next language gets processed $message['totals'] = []; $message['index_meta'] = [ - 'method' => 'web', - 'totals' => $this->status->get('totals'), - 'sync_stack' => [], + 'method' => 'web', + 'totals' => $this->status->get('totals'), + 'sync_stack' => [], 'put_mapping' => $this->status->get('putMapping'), ]; return $message; diff --git a/src/Sync/DashboardAjax.php b/src/Sync/DashboardAjax.php new file mode 100644 index 0000000..be10658 --- /dev/null +++ b/src/Sync/DashboardAjax.php @@ -0,0 +1,59 @@ +activeLanguages ) ) { + return; + } + + add_action( 'wp_ajax_ep_index', [ $this, 'action_wp_ajax_ep_index' ], 9 ); + add_action( 'wp_ajax_ep_cancel_index', [ $this, 'action_wp_ajax_ep_cancel_index' ], 9 ); + } + + private function isDashboardSync() { + 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'] ) { + return false; + } + + return true; + } + + public function action_wp_ajax_ep_index() { + if ( false === $this->isDashboardSync() ) { + return; + } + $this->setUpAndRun(); + } + + public function action_wp_ajax_ep_cancel_index() { + if ( false === $this->isDashboardSync() ) { + return; + } + $this->tearDown(); + } + +} diff --git a/src/Sync/DashboardRest.php b/src/Sync/DashboardRest.php new file mode 100644 index 0000000..73b5ab0 --- /dev/null +++ b/src/Sync/DashboardRest.php @@ -0,0 +1,69 @@ +activeLanguages ) ) { + return; + } + + add_filter( 'rest_request_before_callbacks', [ $this, 'rest_request_before_callbacks' ], 10, 3 ) ; + } + + private function isSyncMethod( $method, $request ) { + if ( $method !== $request->get_method() ) { + return false; + } + + $route = $request->get_route(); + if ( preg_match( "/^\/elasticpress\/v\d+\/sync$/", $route ) ) { + return true; + } + + return false; + } + + /** + * @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response + * @param array $handler + * @param \WP_REST_Request $request + * + * @return \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed + */ + public function rest_request_before_callbacks( $response, $handler, $request ) { + $capability = Utils\get_capability(); + + if ( ! current_user_can( $capability ) ) { + return $response; + } + + if ( $this->isSyncMethod( 'POST', $request ) ) { + $this->setFullIndexArgs( $request->get_params() ); + $this->setUpAndRun(); + } + + if ( $this->isSyncMethod( 'DELETE', $request ) ) { + $this->tearDown(); + } + + return $response; + + } + +} \ No newline at end of file diff --git a/src/Sync/Singular.php b/src/Sync/Singular.php index 81fba41..066cc06 100644 --- a/src/Sync/Singular.php +++ b/src/Sync/Singular.php @@ -13,6 +13,18 @@ class Singular { use CrudPropagation; + /** + * In ElasticPress 5.0.0, the \ElasticPress\SyncManager::sync_queue attribute mutated: + * - Before, it was an array indexed by post IDs with a TRUE value for those modified. + * - After, those same entries got spread in parent arrays per site ID. + * + * For example, [ 123 => true ] became [ 1 => [ 123 => true ] ], + * where 1 is the a block ID and 123 is a post ID for a post in the blog with ID equal 1. + * + * Nice for them to change some public data structure, right? Well, life! + */ + const SYNC_QUEUE_API_CHANGE_V1 = '5.0.0'; + /** @var Indexables */ private $indexables; @@ -25,22 +37,28 @@ class Singular { /** @var string */ private $defaultLanguage; + /** @var string */ + private $elasticPressVersion; + /** * @param Indexables $indexables * @param Indices $indicesManager * @param array $activeLanguages * @param string $defaultLanguage + * @param string $elasticPressVersion */ public function __construct( Indexables $indexables, Indices $indicesManager, $activeLanguages, - $defaultLanguage + $defaultLanguage, + $elasticPressVersion ) { - $this->indexables = $indexables; - $this->indicesManager = $indicesManager; - $this->activeLanguages = $activeLanguages; - $this->defaultLanguage = $defaultLanguage; + $this->indexables = $indexables; + $this->indicesManager = $indicesManager; + $this->activeLanguages = $activeLanguages; + $this->defaultLanguage = $defaultLanguage; + $this->elasticPressVersion = $elasticPressVersion; } public function addHooks() { @@ -63,6 +81,22 @@ function( $hook ) { ); } + /** + * @param \ElasticPress\SyncManager $syncManager + * + * @return int[] + */ + private function getIdsInSyncQueue( $syncManager ) { + $syncQueue = $syncManager->sync_queue; + + if ( version_compare( $this->elasticPressVersion, self::SYNC_QUEUE_API_CHANGE_V1, '<' ) ) { + return array_keys( $syncQueue ); + } + $currentBlogId = get_current_blog_id(); + $syncQueueForCurrentBlog = $syncQueue[ $currentBlogId ] ?? []; + return array_keys( $syncQueueForCurrentBlog ) ; + } + /** * @param bool $halt * @param \ElasticPress\SyncManager $syncManager @@ -94,7 +128,7 @@ public function manageSyncQueue( $halt, $syncManager, $indexableSlug ) { // - Maybe remove the ID for the default language post from the current language index // - Skip those which do not exist in the current language index (when updating an existing translation, for example) // - Maybe sync the default language post in the default language index to sync language field values - $this->propagateIds( array_keys( $syncManager->sync_queue ) ); + $this->propagateIds( $this->getIdsInSyncQueue( $syncManager ) ); $this->manageIds( 'sync', 'main' ); $this->manageIds( 'delete', 'related' );