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

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