PluginProbe
ElasticPress / 4.3.0
ElasticPress v4.3.0
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.0, at includes/classes/Feature/SearchOrdering/SearchOrdering.php

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