PluginProbe
ElasticPress / 5.0.0
ElasticPress v5.0.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / SearchOrdering / SearchOrdering.php

SearchOrdering.php in ElasticPress 5.0.0, at includes/classes/Feature/SearchOrdering/SearchOrdering.php

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