PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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 5.3.5, at includes/classes/Feature/SearchOrdering/SearchOrdering.php

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