PluginProbe
ElasticPress / 4.4.1
ElasticPress v4.4.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.4.1, at includes/classes/Feature/SearchOrdering/SearchOrdering.php

891 lines 28.0 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 https://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.js',
419 Utils\get_asset_info( 'ordering-script', 'dependencies' ),
420 Utils\get_asset_info( 'ordering-script', 'version' ),
421 true
422 );
423
424 wp_set_script_translations( 'ep_ordering_scripts', 'elasticpress' );
425
426 wp_enqueue_style(
427 'ep_ordering_styles',
428 EP_URL . 'dist/css/ordering-styles.css',
429 Utils\get_asset_info( 'ordering-styles', 'dependencies' ),
430 Utils\get_asset_info( 'ordering-styles', 'version' )
431 );
432
433 $pointer_data = $this->get_pointer_data_for_localize();
434
435 wp_localize_script(
436 'ep_ordering_scripts',
437 'epOrdering',
438 array_merge(
439 [
440 'searchEndpoint' => rest_url( 'elasticpress/v1/pointer_search' ),
441 'nonce' => wp_create_nonce( 'save-search-ordering' ),
442 'restApiRoot' => rest_url( '/' ),
443 'postsPerPage' => (int) get_option( 'posts_per_page', 10 ),
444 ],
445 $pointer_data
446 )
447 );
448 }
449 }
450
451 /**
452 * Handles saving the injected post settings
453 *
454 * @param int $post_id Post ID of the post being saved
455 * @param \WP_Post $post Post object being saved
456 */
457 public function save_post( $post_id, $post ) {
458 /** Post Indexable @var Post $post_indexable */
459 $post_indexable = Indexables::factory()->get( 'post' );
460
461 if ( ! isset( $_POST['search-ordering-nonce'] ) || ! wp_verify_nonce( $_POST['search-ordering-nonce'], 'save-search-ordering' ) ) {
462 return;
463 }
464
465 if ( ! current_user_can( 'edit_post', $post_id ) ) {
466 return;
467 }
468
469 $final_order_data = [];
470
471 // Track the old IDs that aren't retained so we can delete the terms later
472 $previous_order_data = get_post_meta( $post_id, 'pointers', true );
473 $previous_post_ids = ! empty( $previous_order_data ) ? array_flip( wp_list_pluck( $previous_order_data, 'ID' ) ) : [];
474
475 $ordered_posts = json_decode( wp_unslash( $_POST['ordered_posts'] ), true );
476
477 $posts_per_page = (int) get_option( 'posts_per_page', 10 );
478
479 $old_search_term = get_post_meta( $post->ID, 'search_term', true );
480
481 // Search term changed, so remove it from all of the posts it was assigned to
482 if ( ! empty( $old_search_term ) && $old_search_term !== $post->post_title ) {
483 $old_term = $this->create_or_return_custom_result_term( $old_search_term );
484
485 foreach ( array_flip( $previous_post_ids ) as $previous_post_id ) {
486 wp_remove_object_terms( $previous_post_id, $old_term->term_id, self::TAXONOMY_NAME );
487 $post_indexable->sync_manager->action_sync_on_update( $previous_post_id );
488 }
489 }
490
491 foreach ( $ordered_posts as $order_data ) {
492 if ( intval( $order_data['order'] ) <= $posts_per_page ) {
493 $final_order_data[] = [
494 'ID' => intval( $order_data['ID'] ),
495 'order' => intval( $order_data['order'] ),
496 ];
497 } else {
498 $previous_post_ids[ intval( $order_data['ID'] ) ] = true;
499 }
500
501 // If the post is still assigned, no need to delete the terms later
502 if ( isset( $previous_post_ids[ $order_data['ID'] ] ) ) {
503 unset( $previous_post_ids[ $order_data['ID'] ] );
504 }
505 }
506
507 $custom_result_term = $this->create_or_return_custom_result_term( $post->post_title );
508 if ( $custom_result_term ) {
509 foreach ( $final_order_data as $final_order_datum ) {
510
511 if ( 'publish' === $post->post_status ) {
512 $this->assign_term_to_post( $final_order_datum['ID'], $custom_result_term->term_taxonomy_id, $final_order_datum['order'] );
513 } else {
514 // If not published, we need to ensure that the term is _not_ present on the target posts
515 wp_remove_object_terms( $final_order_datum['ID'], (int) $custom_result_term->term_id, self::TAXONOMY_NAME );
516 }
517
518 $post_indexable->sync_manager->action_sync_on_update( $final_order_datum['ID'] );
519 }
520 }
521
522 // Remove terms for any that were deleted
523 if ( ! empty( $previous_post_ids ) ) {
524 foreach ( array_flip( $previous_post_ids ) as $old_post_id ) {
525 wp_remove_object_terms( $old_post_id, (int) $custom_result_term->term_id, self::TAXONOMY_NAME );
526
527 $post_indexable->sync_manager->action_sync_on_update( $old_post_id );
528 }
529 }
530
531 update_post_meta( $post_id, 'pointers', $final_order_data );
532 update_post_meta( $post_id, 'search_term', $post->post_title );
533 }
534
535 /**
536 * Creates a term in the taxonomy for tracking ordered results or returns the existing term
537 *
538 * @param string $term_name Term name to fetch or create
539 *
540 * @return false|\WP_Term
541 */
542 public function create_or_return_custom_result_term( $term_name ) {
543 $term = get_term_by( 'name', $term_name, self::TAXONOMY_NAME );
544
545 if ( ! $term ) {
546 $term_ids = wp_insert_term( $term_name, self::TAXONOMY_NAME );
547
548 if ( is_wp_error( $term_ids ) ) {
549 return false;
550 }
551
552 $term = get_term( $term_ids['term_id'], self::TAXONOMY_NAME );
553 }
554
555 return $term;
556 }
557
558 /**
559 * Filters available fields for weighting to exclude the custom results taxonomy
560 *
561 * @param array $fields Current weightable fields
562 * @param string $post_type Current post type
563 *
564 * @return array Final weightable fields
565 */
566 public function weighting_fields_for_post_type( $fields, $post_type ) {
567 if ( isset( $fields['taxonomies'] ) && isset( $fields['taxonomies']['children'] ) && isset( $fields['taxonomies']['children'][ 'terms.' . self::TAXONOMY_NAME . '.name' ] ) ) {
568 unset( $fields['taxonomies']['children'][ 'terms.' . self::TAXONOMY_NAME . '.name' ] );
569 }
570
571 return $fields;
572 }
573
574 /**
575 * Filters the weighting configuration to insert our weighting config when we're searching
576 *
577 * @param array $weighting_configuration Current weighting configuration
578 * @param array $args WP Query Args
579 *
580 * @return array Final weighting configuration
581 */
582 public function filter_weighting_configuration( $weighting_configuration, $args = array() ) {
583 if ( ! isset( $args['exclude_pointers'] ) || true !== $args['exclude_pointers'] ) {
584 foreach ( $weighting_configuration as $post_type => $config ) {
585 $weighting_configuration[ $post_type ]['terms.ep_custom_result.name'] = [
586 'enabled' => true,
587 'weight' => 9999,
588 'fuzziness' => false,
589 ];
590 }
591 }
592
593 return $weighting_configuration;
594 }
595
596 /**
597 * Filters default weights for server side searches
598 *
599 * @param array $post_type_defaults Current default weight settings
600 * @param string $post_type Post type
601 *
602 * @return array Final weight settings
603 */
604 public function filter_default_post_type_weights( $post_type_defaults, $post_type ) {
605 $post_type_defaults['terms.ep_custom_result.name'] = [
606 'enabled' => true,
607 'weight' => 9999,
608 'fuzziness' => false,
609 ];
610
611 return $post_type_defaults;
612 }
613
614 /**
615 * Changes the title to show "Enter Search Query" on the CPT edit screen
616 *
617 * @param string $text Current text for the input label
618 *
619 * @return string Final label
620 */
621 public function filter_enter_title_here( $text ) {
622 if ( self::POST_TYPE_NAME === get_post_type() ) {
623 $text = esc_html__( 'Enter Search Query', 'elasticpress' );
624 }
625
626 return $text;
627 }
628
629 /**
630 * Filters the title column to show "Search Query"
631 *
632 * @param array $columns Current columns
633 *
634 * @return array Final Columns
635 */
636 public function filter_column_names( $columns ) {
637 $columns['title'] = esc_html__( 'Search Query', 'elasticpress' );
638
639 return $columns;
640 }
641
642 /**
643 * Finds and pointer post types in the result set and replaces them with the posts to be injected in the proper positions
644 *
645 * @param array $posts Current array of post results
646 * @param \WP_Query $query The current query
647 *
648 * @return array Final modified posts array
649 */
650 public function posts_results( $posts, $query ) {
651 if ( is_array( $posts ) && $query->is_search() ) {
652 $search_query = strtolower( $query->get( 's' ) );
653
654 $to_inject = array();
655
656 foreach ( $posts as $key => &$post ) {
657 if ( isset( $post->terms ) && isset( $post->terms[ self::TAXONOMY_NAME ] ) ) {
658 foreach ( $post->terms[ self::TAXONOMY_NAME ] as $current_term ) {
659 if ( strtolower( $current_term['name'] ) === $search_query ) {
660 $to_inject[ $current_term['term_order'] ] = $post;
661
662 unset( $posts[ $key ] );
663
664 break;
665 }
666 }
667 }
668 }
669
670 // Remove the null values
671 $posts = array_filter( $posts );
672
673 // Sort by key so they get injected in order and remain in the proper positions
674 ksort( $to_inject );
675
676 if ( ! empty( $to_inject ) ) {
677 foreach ( $to_inject as $position => $newpost ) {
678 array_splice( $posts, $position - 1, 0, array( $newpost ) );
679 }
680 }
681
682 // reindex just in case we got out of order keys
683 $posts = array_values( $posts );
684 }
685
686 return $posts;
687 }
688
689 /**
690 * Registers the API endpoint for searching from the admin interface
691 */
692 public function rest_api_init() {
693 register_rest_route(
694 'elasticpress/v1',
695 'pointer_search',
696 [
697 'methods' => 'GET',
698 'callback' => [ $this, 'handle_pointer_search' ],
699 'permission_callback' => function() {
700 return current_user_can( self::CAPABILITY );
701 },
702 'args' => [
703 's' => [
704 'validate_callback' => function ( $param ) {
705 return ! empty( $param );
706 },
707 'required' => true,
708 ],
709 ],
710 ]
711 );
712
713 register_rest_route(
714 'elasticpress/v1',
715 'pointer_preview',
716 [
717 'methods' => 'GET',
718 'callback' => [ $this, 'handle_pointer_preview' ],
719 'permission_callback' => function() {
720 return current_user_can( self::CAPABILITY );
721 },
722 'args' => [
723 's' => [
724 'validate_callback' => function ( $param ) {
725 return ! empty( $param );
726 },
727 'required' => true,
728 ],
729 ],
730 ]
731 );
732 }
733
734 /**
735 * Handles the search for posts from the admin interface for the post type
736 *
737 * @param \WP_REST_Request $request Rest request
738 *
739 * @return array
740 */
741 public function handle_pointer_search( $request ) {
742 $search = $request->get_param( 's' );
743
744 /** Features Class @var Features $features */
745 $features = Features::factory();
746
747 /** Search Feature @var Feature\Search\Search $search */
748 $search_feature = $features->get_registered_feature( 'search' );
749
750 $post_types = $search_feature->get_searchable_post_types();
751
752 $query = new \WP_Query(
753 [
754 'post_type' => $post_types,
755 'post_status' => 'publish',
756 's' => $search,
757 ]
758 );
759
760 return $query->posts;
761 }
762
763 /**
764 * Handles the search preview on the pointer edit screen
765 *
766 * @param \WP_REST_Request $request Rest request
767 *
768 * @return array
769 */
770 public function handle_pointer_preview( $request ) {
771 remove_filter( 'ep_searchable_post_types', [ $this, 'searchable_post_types' ] );
772
773 $search = $request->get_param( 's' );
774
775 $query = new \WP_Query(
776 [
777 's' => $search,
778 'exclude_pointers' => true,
779 ]
780 );
781
782 add_filter( 'ep_searchable_post_types', [ $this, 'searchable_post_types' ] );
783
784 return $query->posts;
785 }
786
787 /**
788 * Removes taxonomy terms from the references posts when a pointer is deleted or trashed
789 *
790 * @param int $post_id Post ID that is being deleted
791 */
792 public function handle_post_trash( $post_id ) {
793 $post = get_post( $post_id );
794
795 if ( self::POST_TYPE_NAME !== $post->post_type ) {
796 return;
797 }
798
799 /** Post Indexable @var Post $post_indexable */
800 $post_indexable = Indexables::factory()->get( 'post' );
801
802 $pointers = get_post_meta( $post_id, 'pointers', true );
803 $term = $this->create_or_return_custom_result_term( $post->post_title );
804
805 if ( empty( $pointers ) ) {
806 return;
807 }
808
809 foreach ( $pointers as $pointer ) {
810 $ref_id = $pointer['ID'];
811 wp_remove_object_terms( $ref_id, (int) $term->term_id, self::TAXONOMY_NAME );
812
813 $post_indexable->sync_manager->action_sync_on_update( $ref_id );
814 }
815 }
816
817 /**
818 * Handles reassigning terms to the posts when a pointer post is restored from trash
819 *
820 * @param int $post_id Post ID
821 */
822 public function handle_post_untrash( $post_id ) {
823 $post = get_post( $post_id );
824
825 if ( self::POST_TYPE_NAME !== $post->post_type ) {
826 return;
827 }
828
829 /** Post Indexable @var Post $post_indexable */
830 $post_indexable = Indexables::factory()->get( 'post' );
831
832 $pointers = get_post_meta( $post_id, 'pointers', true );
833 $term = $this->create_or_return_custom_result_term( $post->post_title );
834
835 if ( 'publish' === $post->post_status ) {
836 foreach ( $pointers as $pointer ) {
837 $this->assign_term_to_post( $pointer['ID'], $term->term_taxonomy_id, $pointer['order'] );
838
839 $post_indexable->sync_manager->action_sync_on_update( $pointer['ID'] );
840 }
841 }
842 }
843
844 /**
845 * Assigns the term to the post with the proper term_order value
846 *
847 * @param int $post_id The Post ID
848 * @param int $term_taxonomy_id Term Taxonomy ID
849 * @param int $order Term order to assign
850 *
851 * @return bool|int
852 */
853 protected function assign_term_to_post( $post_id, $term_taxonomy_id, $order ) {
854 global $wpdb;
855
856 $result = $wpdb->query(
857 $wpdb->prepare(
858 "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)",
859 $post_id,
860 $term_taxonomy_id,
861 $order
862 )
863 );
864
865 // Delete the term order cache
866 wp_cache_delete( "{$post_id}_term_order" );
867
868 // Clears the core cache
869 wp_cache_delete( $post_id, self::TAXONOMY_NAME . '_relationships' );
870
871 return $result;
872 }
873
874 /**
875 * Update the page title to keep the consistency through the plugin
876 *
877 * @param string $admin_title The page title, with extra context added
878 * @param string $title The original page title
879 *
880 * @return string Updated the page title
881 */
882 public function update_page_title( $admin_title, $title ) {
883 if ( $this->title === $title ) {
884 return __( 'ElasticPress Custom Search Results', 'elasticpress' );
885 }
886
887 return $admin_title;
888 }
889
890 }
891