PluginProbe
ElasticPress / 4.3.1
ElasticPress v4.3.1
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / SearchOrdering / SearchOrdering.php

SearchOrdering.php in ElasticPress 4.3.1, at includes/classes/Feature/SearchOrdering/SearchOrdering.php

889 lines 27.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Search Ordering Feature
4 *
5 * @package elasticpress
6 */
7
8 namespace ElasticPress\Feature\SearchOrdering;
9
10 use ElasticPress\Feature;
11 use ElasticPress\FeatureRequirementsStatus as FeatureRequirementsStatus;
12 use ElasticPress\Features;
13 use ElasticPress\Indexable\Post\Post;
14 use ElasticPress\Indexables;
15 use ElasticPress\Utils;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Search Ordering Feature
23 *
24 * @package ElasticPress\Feature\SearchOrdering
25 */
26 class SearchOrdering extends Feature {
27
28 /**
29 * Internal name of the post type
30 */
31 const POST_TYPE_NAME = 'ep-pointer';
32
33 /**
34 * Internal name of the taxonomy
35 */
36 const TAXONOMY_NAME = 'ep_custom_result';
37
38 /**
39 * Capability required to manage.
40 */
41 const CAPABILITY = 'manage_options';
42
43 /**
44 * Initialize feature setting it's config
45 *
46 * @since 3.0
47 */
48 public function __construct() {
49 $this->slug = 'searchordering';
50
51 $this->title = esc_html__( 'Custom Search Results', 'elasticpress' );
52
53 $this->summary = __( 'Insert specific posts into search results for specific search queries.', 'elasticpress' );
54
55 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#custom-search-results', 'elasticpress' );
56
57 $this->requires_install_reindex = false;
58
59 parent::__construct();
60 }
61
62 /**
63 * Setup Feature Functionality
64 */
65 public function setup() {
66 /** Features Class @var Features $features */
67 $features = Features::factory();
68
69 /** Search Feature @var Feature\Search\Search $search */
70 $search = $features->get_registered_feature( 'search' );
71
72 if ( ! $search->is_active() && $this->is_active() ) {
73 $features->deactivate_feature( $this->slug );
74 return false;
75 }
76
77 add_action( 'admin_menu', [ $this, 'admin_menu' ], 50 );
78 add_filter( 'parent_file', [ $this, 'parent_file' ], 50 );
79 add_filter( 'submenu_file', [ $this, 'submenu_file' ], 50 );
80 add_action( 'init', [ $this, 'register_post_type' ] );
81 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
82 add_action( 'save_post_' . self::POST_TYPE_NAME, [ $this, 'save_post' ], 10, 2 );
83 add_action( 'posts_results', [ $this, 'posts_results' ], 20, 2 ); // Runs after core ES is done
84 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
85 add_filter( 'ep_sync_taxonomies', [ $this, 'filter_sync_taxonomies' ] );
86 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'weighting_fields_for_post_type' ], 1, 2 );
87 add_filter( 'ep_weighting_configuration_for_search', [ $this, 'filter_weighting_configuration' ], 10, 2 );
88 add_filter( 'ep_weighting_configuration_for_autosuggest', [ $this, 'filter_weighting_configuration' ], 10, 1 );
89 add_filter( 'ep_weighting_configuration_defaults_for_autosuggest', [ $this, 'filter_weighting_configuration' ], 10, 1 );
90 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'filter_default_post_type_weights' ], 10, 2 );
91 add_filter( 'enter_title_here', [ $this, 'filter_enter_title_here' ] );
92 add_filter( 'manage_' . self::POST_TYPE_NAME . '_posts_columns', [ $this, 'filter_column_names' ] );
93 add_filter( 'post_updated_messages', [ $this, 'filter_updated_messages' ] );
94 add_filter( 'admin_title', [ $this, 'update_page_title' ], 10, 2 );
95
96 // Deals with trashing/untrashing/deleting
97 add_action( 'wp_trash_post', [ $this, 'handle_post_trash' ] );
98 add_action( 'before_delete_post', [ $this, 'handle_post_trash' ] );
99 add_action( 'untrashed_post', [ $this, 'handle_post_untrash' ] );
100 add_filter( 'post_row_actions', [ $this, 'remove_quick_edit' ], 10, 2 );
101 add_filter( 'bulk_actions-edit-' . self::POST_TYPE_NAME, [ $this, 'remove_bulk_edit' ] );
102 }
103
104 /**
105 * Remove bulk edit
106 *
107 * @param array $actions Bulk actions
108 * @since 3.5
109 * @return array
110 */
111 public function remove_bulk_edit( $actions ) {
112 unset( $actions['edit'] );
113 return $actions;
114 }
115
116 /**
117 * Remove quick edit for post type
118 *
119 * @param array $actions Current table row actions
120 * @param WP_Post $post Current post
121 * @since 3.5
122 * @return array
123 */
124 public function remove_quick_edit( $actions, $post ) {
125 if ( self::POST_TYPE_NAME === $post->post_type ) {
126 // Remove "Quick Edit"
127 unset( $actions['inline hide-if-no-js'] );
128 }
129 return $actions;
130 }
131
132 /**
133 * Add updated messages for post type
134 *
135 * @param array $messages Messages array
136 * @since 3.2
137 * @return array
138 */
139 public function filter_updated_messages( $messages ) {
140 $post = get_post();
141 $post_type = get_post_type( $post );
142 $post_type_object = get_post_type_object( $post_type );
143
144 $messages[ self::POST_TYPE_NAME ] = array(
145 0 => '',
146 1 => esc_html__( 'Custom result updated.', 'elasticpress' ),
147 2 => esc_html__( 'Custom field updated.', 'elasticpress' ),
148 3 => esc_html__( 'Custom field deleted.', 'elasticpress' ),
149 4 => esc_html__( 'Custom result updated.', 'elasticpress' ),
150 /* translators: %s: date and time of the revision */
151 5 => isset( $_GET['revision'] ) ? sprintf( __( 'Custom result restored to revision from %s', 'elasticpress' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, // phpcs:ignore WordPress.Security.NonceVerification
152 6 => esc_html__( 'Custom result published.', 'elasticpress' ),
153 7 => esc_html__( 'Custom result saved.', 'elasticpress' ),
154 8 => esc_html__( 'Custom result submitted.', 'elasticpress' ),
155 9 => sprintf(
156 // translators: Scheduled date.
157 esc_html__( 'Custom result scheduled for: %1$s.', 'elasticpress' ),
158 // translators: Publish box date format, see http://php.net/date
159 date_i18n( esc_html__( 'M j, Y @ G:i', 'elasticpress' ), strtotime( $post->post_date ) )
160 ),
161 10 => esc_html__( 'Custom result draft updated.', 'elasticpress' ),
162 );
163
164 return $messages;
165 }
166
167 /**
168 * Returns requirements status of feature
169 *
170 * Requires the search feature to be activated
171 *
172 * @return FeatureRequirementsStatus
173 */
174 public function requirements_status() {
175 /** Features Class @var Features $features */
176 $features = Features::factory();
177
178 /** Search Feature @var Feature\Search\Search $search */
179 $search = $features->get_registered_feature( 'search' );
180
181 if ( ! $search->is_active() ) {
182 return new FeatureRequirementsStatus( 2, esc_html__( 'This feature requires the "Post Search" feature to be enabled', 'elasticpress' ) );
183 }
184
185 return parent::requirements_status();
186 }
187
188 /**
189 * Output feature box long
190 */
191 public function output_feature_box_long() {
192 ?>
193 <p><?php esc_html_e( 'Selected posts will be inserted into search results in the specified position.', 'elasticpress' ); ?></p>
194 <?php
195 }
196
197 /**
198 * Adds this taxonomy as one of the taxonomies to index
199 *
200 * @param array $taxonomies Current indexable taxonomies
201 *
202 * @return array
203 */
204 public function filter_sync_taxonomies( $taxonomies ) {
205 $taxonomies[ self::TAXONOMY_NAME ] = get_taxonomy( self::TAXONOMY_NAME );
206
207 return $taxonomies;
208 }
209
210 /**
211 * Adds the search ordering to the admin menu
212 */
213 public function admin_menu() {
214 add_submenu_page(
215 'elasticpress',
216 esc_html__( 'Custom Results', 'elasticpress' ),
217 esc_html__( 'Custom Results', 'elasticpress' ),
218 self::CAPABILITY,
219 'edit.php?post_type=' . self::POST_TYPE_NAME
220 );
221 }
222
223 /**
224 * Sets the parent menu item for the post type submenu
225 *
226 * @param string $parent_file Current parent menu item
227 *
228 * @return string
229 */
230 public function parent_file( $parent_file ) {
231 global $current_screen;
232
233 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
234 return $parent_file;
235 }
236
237 // Set correct active/current menu and submenu in the WordPress Admin menu for the "pointer" CPT Add-New/Edit/List
238 if ( self::POST_TYPE_NAME === $current_screen->post_type ) {
239 $parent_file = 'elasticpress';
240 }
241
242 return $parent_file;
243 }
244
245 /**
246 * Ensures the correct item is highlighted when adding a new post
247 *
248 * @param string $submenu_file Current parent menu item
249 *
250 * @return string
251 */
252 public function submenu_file( $submenu_file ) {
253 global $current_screen;
254
255 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
256 return $submenu_file;
257 }
258
259 // Set correct active/current menu and submenu in the WordPress Admin menu for the "pointer" CPT Add-New/Edit/List
260 if ( self::POST_TYPE_NAME === $current_screen->post_type ) {
261 $submenu_file = 'edit.php?post_type=' . self::POST_TYPE_NAME;
262 }
263
264 return $submenu_file;
265 }
266
267 /**
268 * Registers the pointer post type for the injected results
269 */
270 public function register_post_type() {
271 $is_network = defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK;
272 $menu = $is_network ? null : false;
273
274 $labels = array(
275 'name' => esc_html_x( 'Custom Search Results', 'post type general name', 'elasticpress' ),
276 'singular_name' => esc_html_x( 'Custom Search Result', 'post type singular name', 'elasticpress' ),
277 'menu_name' => esc_html_x( 'Custom Search Results', 'admin menu', 'elasticpress' ),
278 'name_admin_bar' => esc_html_x( 'Custom Search Result', 'add new on admin bar', 'elasticpress' ),
279 'add_new' => esc_html_x( 'Add New', 'book', 'elasticpress' ),
280 'add_new_item' => esc_html__( 'Add New Custom Search Result', 'elasticpress' ),
281 'new_item' => esc_html__( 'New Custom Search Result', 'elasticpress' ),
282 'edit_item' => esc_html__( 'Edit Custom Search Result', 'elasticpress' ),
283 'view_item' => esc_html__( 'View Custom Search Result', 'elasticpress' ),
284 'all_items' => esc_html__( 'All Custom Search Results', 'elasticpress' ),
285 'search_items' => esc_html__( 'Search Custom Search Results', 'elasticpress' ),
286 'parent_item_colon' => esc_html__( 'Parent Custom Search Result:', 'elasticpress' ),
287 'not_found' => esc_html__( 'No results found.', 'elasticpress' ),
288 'not_found_in_trash' => esc_html__( 'No results found in Trash.', 'elasticpress' ),
289 );
290
291 $args = array(
292 'labels' => $labels,
293 'description' => esc_html__( 'Posts to inject into search results', 'elasticpress' ),
294 'public' => false,
295 'publicly_queryable' => false,
296 'show_ui' => true,
297 'show_in_menu' => $menu,
298 'query_var' => true,
299 'rewrite' => array( 'slug' => 'ep-pointer' ),
300 'capability_type' => 'post',
301 'has_archive' => false,
302 'hierarchical' => false,
303 'menu_position' => 100,
304 'supports' => [ 'title' ],
305 'register_meta_box_cb' => [ $this, 'register_meta_box' ],
306 'menu_icon' => 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNzMgNzEuMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNzMgNzEuMzsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik0zNi41LDQuN0MxOS40LDQuNyw1LjYsMTguNiw1LjYsMzUuN2MwLDEwLDQuNywxOC45LDEyLjEsMjQuNWw0LjUtNC41YzAuMS0wLjEsMC4xLTAuMiwwLjItMC4zbDAuNy0wLjdsNi40LTYuNGMyLjEsMS4yLDQuNSwxLjksNy4xLDEuOWM4LDAsMTQuNS02LjUsMTQuNS0xNC41cy02LjUtMTQuNS0xNC41LTE0LjVTMjIsMjcuNiwyMiwzNS42YzAsMi44LDAuOCw1LjMsMi4xLDcuNWwtNi40LDYuNGMtMi45LTMuOS00LjYtOC43LTQuNi0xMy45YzAtMTIuOSwxMC41LTIzLjQsMjMuNC0yMy40czIzLjQsMTAuNSwyMy40LDIzLjRTNDkuNCw1OSwzNi41LDU5Yy0yLjEsMC00LjEtMC4zLTYtMC44bC0wLjYsMC42bC01LjIsNS40YzMuNiwxLjUsNy42LDIuMywxMS44LDIuM2MxNy4xLDAsMzAuOS0xMy45LDMwLjktMzAuOVM1My42LDQuNywzNi41LDQuN3oiLz48L3N2Zz4=',
307 );
308
309 register_post_type( self::POST_TYPE_NAME, $args );
310
311 // Register taxonomy
312 $labels = array(
313 'name' => esc_html_x( 'Custom Results', 'taxonomy general name', 'elasticpress' ),
314 'singular_name' => esc_html_x( 'Custom Result', 'taxonomy singular name', 'elasticpress' ),
315 'search_items' => esc_html__( 'Search Custom Results', 'elasticpress' ),
316 'all_items' => esc_html__( 'All Custom Results', 'elasticpress' ),
317 'parent_item' => esc_html__( 'Parent Custom Result', 'elasticpress' ),
318 'parent_item_colon' => esc_html__( 'Parent Custom Result:', 'elasticpress' ),
319 'edit_item' => esc_html__( 'Edit Custom Result', 'elasticpress' ),
320 'update_item' => esc_html__( 'Update Custom Result', 'elasticpress' ),
321 'add_new_item' => esc_html__( 'Add New Custom Result', 'elasticpress' ),
322 'new_item_name' => esc_html__( 'New Custom Result Name', 'elasticpress' ),
323 'menu_name' => esc_html__( 'Custom Results', 'elasticpress' ),
324 );
325
326 $args = array(
327 'hierarchical' => false,
328 'labels' => $labels,
329 'show_ui' => false,
330 'show_admin_column' => false,
331 'query_var' => false,
332 'rewrite' => false,
333 );
334
335 /** Features Class @var Features $features */
336 $features = Features::factory();
337
338 /** Search Feature @var Feature\Search\Search $search */
339 $search = $features->get_registered_feature( 'search' );
340
341 $post_types = $search->get_searchable_post_types();
342
343 register_taxonomy( 'ep_custom_result', $post_types, $args );
344 }
345
346 /**
347 * Registers meta box for the search pointers
348 */
349 public function register_meta_box() {
350 add_meta_box( 'ep-ordering', esc_html__( 'Manage Results', 'elasticpress' ), [ $this, 'render_meta_box' ], self::POST_TYPE_NAME, 'normal' );
351 }
352
353 /**
354 * Renders the meta box for the injected search results
355 *
356 * @param \WP_Post $post Current post object
357 */
358 public function render_meta_box( $post ) {
359 ?>
360 <div id="ordering-app"></div>
361 <?php
362 }
363
364 /**
365 * Sends initial pointer data to the frontend to reduce API requests required
366 *
367 * @return array
368 */
369 public function get_pointer_data_for_localize() {
370 $post_id = get_the_ID();
371
372 $pointers = get_post_meta( $post_id, 'pointers', true );
373
374 if ( empty( $pointers ) ) {
375 return [
376 'pointers' => [],
377 'posts' => [],
378 ];
379 }
380
381 $post_ids = wp_list_pluck( $pointers, 'ID' );
382
383 $query = new \WP_Query(
384 [
385 'post_type' => 'any',
386 'post__in' => $post_ids,
387 'count' => count( $post_ids ),
388 'orderby' => 'post__in',
389 ]
390 );
391
392 $final_posts = [];
393 $filtered_pointers = [];
394
395 foreach ( $query->posts as $post ) {
396 $final_posts[ $post->ID ] = $post;
397 // Add the post to filtered array. By doing this, we removed the posts that don't exist anymore.
398 $filtered_pointers[] = $pointers[ array_search( $post->ID, $post_ids, true ) ];
399 }
400
401 return [
402 'pointers' => $filtered_pointers,
403 'posts' => $final_posts,
404 ];
405 }
406
407 /**
408 * Enqueues scripts for admin interface
409 */
410 public function admin_enqueue_scripts() {
411 global $pagenow; // post-new.php or post.php
412
413 $screen = get_current_screen();
414
415 if ( in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) && $screen instanceof \WP_Screen && self::POST_TYPE_NAME === $screen->post_type ) {
416 wp_enqueue_script(
417 'ep_ordering_scripts',
418 EP_URL . 'dist/js/ordering-script.min.js',
419 Utils\get_asset_info( 'ordering-script', 'dependencies' ),
420 Utils\get_asset_info( 'ordering-script', 'version' ),
421 true
422 );
423
424 wp_enqueue_style(
425 'ep_ordering_styles',
426 EP_URL . 'dist/css/ordering-styles.min.css',
427 Utils\get_asset_info( 'ordering-styles', 'dependencies' ),
428 Utils\get_asset_info( 'ordering-styles', 'version' )
429 );
430
431 $pointer_data = $this->get_pointer_data_for_localize();
432
433 wp_localize_script(
434 'ep_ordering_scripts',
435 'epOrdering',
436 array_merge(
437 [
438 'searchEndpoint' => rest_url( 'elasticpress/v1/pointer_search' ),
439 'nonce' => wp_create_nonce( 'save-search-ordering' ),
440 'restApiRoot' => rest_url( '/' ),
441 'postsPerPage' => (int) get_option( 'posts_per_page', 10 ),
442 ],
443 $pointer_data
444 )
445 );
446 }
447 }
448
449 /**
450 * Handles saving the injected post settings
451 *
452 * @param int $post_id Post ID of the post being saved
453 * @param \WP_Post $post Post object being saved
454 */
455 public function save_post( $post_id, $post ) {
456 /** Post Indexable @var Post $post_indexable */
457 $post_indexable = Indexables::factory()->get( 'post' );
458
459 if ( ! isset( $_POST['search-ordering-nonce'] ) || ! wp_verify_nonce( $_POST['search-ordering-nonce'], 'save-search-ordering' ) ) {
460 return;
461 }
462
463 if ( ! current_user_can( 'edit_post', $post_id ) ) {
464 return;
465 }
466
467 $final_order_data = [];
468
469 // Track the old IDs that aren't retained so we can delete the terms later
470 $previous_order_data = get_post_meta( $post_id, 'pointers', true );
471 $previous_post_ids = ! empty( $previous_order_data ) ? array_flip( wp_list_pluck( $previous_order_data, 'ID' ) ) : [];
472
473 $ordered_posts = json_decode( wp_unslash( $_POST['ordered_posts'] ), true );
474
475 $posts_per_page = (int) get_option( 'posts_per_page', 10 );
476
477 $old_search_term = get_post_meta( $post->ID, 'search_term', true );
478
479 // Search term changed, so remove it from all of the posts it was assigned to
480 if ( ! empty( $old_search_term ) && $old_search_term !== $post->post_title ) {
481 $old_term = $this->create_or_return_custom_result_term( $old_search_term );
482
483 foreach ( array_flip( $previous_post_ids ) as $previous_post_id ) {
484 wp_remove_object_terms( $previous_post_id, $old_term->term_id, self::TAXONOMY_NAME );
485 $post_indexable->sync_manager->action_sync_on_update( $previous_post_id );
486 }
487 }
488
489 foreach ( $ordered_posts as $order_data ) {
490 if ( intval( $order_data['order'] ) <= $posts_per_page ) {
491 $final_order_data[] = [
492 'ID' => intval( $order_data['ID'] ),
493 'order' => intval( $order_data['order'] ),
494 ];
495 } else {
496 $previous_post_ids[ intval( $order_data['ID'] ) ] = true;
497 }
498
499 // If the post is still assigned, no need to delete the terms later
500 if ( isset( $previous_post_ids[ $order_data['ID'] ] ) ) {
501 unset( $previous_post_ids[ $order_data['ID'] ] );
502 }
503 }
504
505 $custom_result_term = $this->create_or_return_custom_result_term( $post->post_title );
506 if ( $custom_result_term ) {
507 foreach ( $final_order_data as $final_order_datum ) {
508
509 if ( 'publish' === $post->post_status ) {
510 $this->assign_term_to_post( $final_order_datum['ID'], $custom_result_term->term_taxonomy_id, $final_order_datum['order'] );
511 } else {
512 // If not published, we need to ensure that the term is _not_ present on the target posts
513 wp_remove_object_terms( $final_order_datum['ID'], (int) $custom_result_term->term_id, self::TAXONOMY_NAME );
514 }
515
516 $post_indexable->sync_manager->action_sync_on_update( $final_order_datum['ID'] );
517 }
518 }
519
520 // Remove terms for any that were deleted
521 if ( ! empty( $previous_post_ids ) ) {
522 foreach ( array_flip( $previous_post_ids ) as $old_post_id ) {
523 wp_remove_object_terms( $old_post_id, (int) $custom_result_term->term_id, self::TAXONOMY_NAME );
524
525 $post_indexable->sync_manager->action_sync_on_update( $old_post_id );
526 }
527 }
528
529 update_post_meta( $post_id, 'pointers', $final_order_data );
530 update_post_meta( $post_id, 'search_term', $post->post_title );
531 }
532
533 /**
534 * Creates a term in the taxonomy for tracking ordered results or returns the existing term
535 *
536 * @param string $term_name Term name to fetch or create
537 *
538 * @return false|\WP_Term
539 */
540 public function create_or_return_custom_result_term( $term_name ) {
541 $term = get_term_by( 'name', $term_name, self::TAXONOMY_NAME );
542
543 if ( ! $term ) {
544 $term_ids = wp_insert_term( $term_name, self::TAXONOMY_NAME );
545
546 if ( is_wp_error( $term_ids ) ) {
547 return false;
548 }
549
550 $term = get_term( $term_ids['term_id'], self::TAXONOMY_NAME );
551 }
552
553 return $term;
554 }
555
556 /**
557 * Filters available fields for weighting to exclude the custom results taxonomy
558 *
559 * @param array $fields Current weightable fields
560 * @param string $post_type Current post type
561 *
562 * @return array Final weightable fields
563 */
564 public function weighting_fields_for_post_type( $fields, $post_type ) {
565 if ( isset( $fields['taxonomies'] ) && isset( $fields['taxonomies']['children'] ) && isset( $fields['taxonomies']['children'][ 'terms.' . self::TAXONOMY_NAME . '.name' ] ) ) {
566 unset( $fields['taxonomies']['children'][ 'terms.' . self::TAXONOMY_NAME . '.name' ] );
567 }
568
569 return $fields;
570 }
571
572 /**
573 * Filters the weighting configuration to insert our weighting config when we're searching
574 *
575 * @param array $weighting_configuration Current weighting configuration
576 * @param array $args WP Query Args
577 *
578 * @return array Final weighting configuration
579 */
580 public function filter_weighting_configuration( $weighting_configuration, $args = array() ) {
581 if ( ! isset( $args['exclude_pointers'] ) || true !== $args['exclude_pointers'] ) {
582 foreach ( $weighting_configuration as $post_type => $config ) {
583 $weighting_configuration[ $post_type ]['terms.ep_custom_result.name'] = [
584 'enabled' => true,
585 'weight' => 9999,
586 'fuzziness' => false,
587 ];
588 }
589 }
590
591 return $weighting_configuration;
592 }
593
594 /**
595 * Filters default weights for server side searches
596 *
597 * @param array $post_type_defaults Current default weight settings
598 * @param string $post_type Post type
599 *
600 * @return array Final weight settings
601 */
602 public function filter_default_post_type_weights( $post_type_defaults, $post_type ) {
603 $post_type_defaults['terms.ep_custom_result.name'] = [
604 'enabled' => true,
605 'weight' => 9999,
606 'fuzziness' => false,
607 ];
608
609 return $post_type_defaults;
610 }
611
612 /**
613 * Changes the title to show "Enter Search Query" on the CPT edit screen
614 *
615 * @param string $text Current text for the input label
616 *
617 * @return string Final label
618 */
619 public function filter_enter_title_here( $text ) {
620 if ( self::POST_TYPE_NAME === get_post_type() ) {
621 $text = esc_html__( 'Enter Search Query', 'elasticpress' );
622 }
623
624 return $text;
625 }
626
627 /**
628 * Filters the title column to show "Search Query"
629 *
630 * @param array $columns Current columns
631 *
632 * @return array Final Columns
633 */
634 public function filter_column_names( $columns ) {
635 $columns['title'] = esc_html__( 'Search Query', 'elasticpress' );
636
637 return $columns;
638 }
639
640 /**
641 * Finds and pointer post types in the result set and replaces them with the posts to be injected in the proper positions
642 *
643 * @param array $posts Current array of post results
644 * @param \WP_Query $query The current query
645 *
646 * @return array Final modified posts array
647 */
648 public function posts_results( $posts, $query ) {
649 if ( is_array( $posts ) && $query->is_search() ) {
650 $search_query = strtolower( $query->get( 's' ) );
651
652 $to_inject = array();
653
654 foreach ( $posts as $key => &$post ) {
655 if ( isset( $post->terms ) && isset( $post->terms[ self::TAXONOMY_NAME ] ) ) {
656 foreach ( $post->terms[ self::TAXONOMY_NAME ] as $current_term ) {
657 if ( strtolower( $current_term['name'] ) === $search_query ) {
658 $to_inject[ $current_term['term_order'] ] = $post->ID;
659
660 unset( $posts[ $key ] );
661
662 break;
663 }
664 }
665 }
666 }
667
668 // Remove the null values
669 $posts = array_filter( $posts );
670
671 // Sort by key so they get injected in order and remain in the proper positions
672 ksort( $to_inject );
673
674 if ( ! empty( $to_inject ) ) {
675 foreach ( $to_inject as $position => $newpost ) {
676 array_splice( $posts, $position - 1, 0, $newpost );
677 }
678 }
679
680 // reindex just in case we got out of order keys
681 $posts = array_values( $posts );
682 }
683
684 return $posts;
685 }
686
687 /**
688 * Registers the API endpoint for searching from the admin interface
689 */
690 public function rest_api_init() {
691 register_rest_route(
692 'elasticpress/v1',
693 'pointer_search',
694 [
695 'methods' => 'GET',
696 'callback' => [ $this, 'handle_pointer_search' ],
697 'permission_callback' => function() {
698 return current_user_can( self::CAPABILITY );
699 },
700 'args' => [
701 's' => [
702 'validate_callback' => function ( $param ) {
703 return ! empty( $param );
704 },
705 'required' => true,
706 ],
707 ],
708 ]
709 );
710
711 register_rest_route(
712 'elasticpress/v1',
713 'pointer_preview',
714 [
715 'methods' => 'GET',
716 'callback' => [ $this, 'handle_pointer_preview' ],
717 'permission_callback' => function() {
718 return current_user_can( self::CAPABILITY );
719 },
720 'args' => [
721 's' => [
722 'validate_callback' => function ( $param ) {
723 return ! empty( $param );
724 },
725 'required' => true,
726 ],
727 ],
728 ]
729 );
730 }
731
732 /**
733 * Handles the search for posts from the admin interface for the post type
734 *
735 * @param \WP_REST_Request $request Rest request
736 *
737 * @return array
738 */
739 public function handle_pointer_search( $request ) {
740 $search = $request->get_param( 's' );
741
742 /** Features Class @var Features $features */
743 $features = Features::factory();
744
745 /** Search Feature @var Feature\Search\Search $search */
746 $search_feature = $features->get_registered_feature( 'search' );
747
748 $post_types = $search_feature->get_searchable_post_types();
749
750 $query = new \WP_Query(
751 [
752 'post_type' => $post_types,
753 'post_status' => 'publish',
754 's' => $search,
755 ]
756 );
757
758 return $query->posts;
759 }
760
761 /**
762 * Handles the search preview on the pointer edit screen
763 *
764 * @param \WP_REST_Request $request Rest request
765 *
766 * @return array
767 */
768 public function handle_pointer_preview( $request ) {
769 remove_filter( 'ep_searchable_post_types', [ $this, 'searchable_post_types' ] );
770
771 $search = $request->get_param( 's' );
772
773 $query = new \WP_Query(
774 [
775 's' => $search,
776 'exclude_pointers' => true,
777 ]
778 );
779
780 add_filter( 'ep_searchable_post_types', [ $this, 'searchable_post_types' ] );
781
782 return $query->posts;
783 }
784
785 /**
786 * Removes taxonomy terms from the references posts when a pointer is deleted or trashed
787 *
788 * @param int $post_id Post ID that is being deleted
789 */
790 public function handle_post_trash( $post_id ) {
791 $post = get_post( $post_id );
792
793 if ( self::POST_TYPE_NAME !== $post->post_type ) {
794 return;
795 }
796
797 /** Post Indexable @var Post $post_indexable */
798 $post_indexable = Indexables::factory()->get( 'post' );
799
800 $pointers = get_post_meta( $post_id, 'pointers', true );
801 $term = $this->create_or_return_custom_result_term( $post->post_title );
802
803 if ( empty( $pointers ) ) {
804 return;
805 }
806
807 foreach ( $pointers as $pointer ) {
808 $ref_id = $pointer['ID'];
809 wp_remove_object_terms( $ref_id, (int) $term->term_id, self::TAXONOMY_NAME );
810
811 $post_indexable->sync_manager->action_sync_on_update( $ref_id );
812 }
813 }
814
815 /**
816 * Handles reassigning terms to the posts when a pointer post is restored from trash
817 *
818 * @param int $post_id Post ID
819 */
820 public function handle_post_untrash( $post_id ) {
821 $post = get_post( $post_id );
822
823 if ( self::POST_TYPE_NAME !== $post->post_type ) {
824 return;
825 }
826
827 /** Post Indexable @var Post $post_indexable */
828 $post_indexable = Indexables::factory()->get( 'post' );
829
830 $pointers = get_post_meta( $post_id, 'pointers', true );
831 $term = $this->create_or_return_custom_result_term( $post->post_title );
832
833 if ( 'publish' === $post->post_status ) {
834 foreach ( $pointers as $pointer ) {
835 $this->assign_term_to_post( $pointer['ID'], $term->term_taxonomy_id, $pointer['order'] );
836
837 $post_indexable->sync_manager->action_sync_on_update( $pointer['ID'] );
838 }
839 }
840 }
841
842 /**
843 * Assigns the term to the post with the proper term_order value
844 *
845 * @param int $post_id The Post ID
846 * @param int $term_taxonomy_id Term Taxonomy ID
847 * @param int $order Term order to assign
848 *
849 * @return bool|int
850 */
851 protected function assign_term_to_post( $post_id, $term_taxonomy_id, $order ) {
852 global $wpdb;
853
854 $result = $wpdb->query(
855 $wpdb->prepare(
856 "INSERT INTO $wpdb->term_relationships (object_id, term_taxonomy_id, term_order) VALUES ( %d, %d, %d ) ON DUPLICATE KEY UPDATE term_order = VALUES(term_order)",
857 $post_id,
858 $term_taxonomy_id,
859 $order
860 )
861 );
862
863 // Delete the term order cache
864 wp_cache_delete( "{$post_id}_term_order" );
865
866 // Clears the core cache
867 wp_cache_delete( $post_id, self::TAXONOMY_NAME . '_relationships' );
868
869 return $result;
870 }
871
872 /**
873 * Update the page title to keep the consistency through the plugin
874 *
875 * @param string $admin_title The page title, with extra context added
876 * @param string $title The original page title
877 *
878 * @return string Updated the page title
879 */
880 public function update_page_title( $admin_title, $title ) {
881 if ( $this->title === $title ) {
882 return __( 'ElasticPress Custom Search Results', 'elasticpress' );
883 }
884
885 return $admin_title;
886 }
887
888 }
889