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

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