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

895 lines 28.4 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\Utils;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * Search Ordering Feature
22 *
23 * @package ElasticPress\Feature\SearchOrdering
24 */
25 class SearchOrdering extends Feature {
26
27 /**
28 * Internal name of the post type
29 */
30 const POST_TYPE_NAME = 'ep-pointer';
31
32 /**
33 * Internal name of the taxonomy
34 */
35 const TAXONOMY_NAME = 'ep_custom_result';
36
37 /**
38 * Capability required to manage.
39 *
40 * This will be removed in future versions of ElasticPress. Please use `Utils\get_capability()` instead.
41 *
42 * @deprecated 4.5.0
43 */
44 const CAPABILITY = 'elasticpress_manage';
45
46 /**
47 * Initialize feature setting it's config
48 *
49 * @since 3.0
50 */
51 public function __construct() {
52 $this->slug = 'searchordering';
53
54 $this->title = esc_html__( 'Custom Search Results', 'elasticpress' );
55
56 $this->summary = __( 'Insert specific posts into search results for specific search queries.', 'elasticpress' );
57
58 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#custom-search-results', 'elasticpress' );
59
60 $this->requires_install_reindex = false;
61
62 parent::__construct();
63 }
64
65 /**
66 * Setup Feature Functionality
67 */
68 public function setup() {
69 /** Features Class @var Features $features */
70 $features = Features::factory();
71
72 /** Search Feature @var Feature\Search\Search $search */
73 $search = $features->get_registered_feature( 'search' );
74
75 if ( ! $search->is_active() && $this->is_active() ) {
76 $features->deactivate_feature( $this->slug );
77 return false;
78 }
79
80 add_action( 'admin_menu', [ $this, 'admin_menu' ], 50 );
81 add_filter( 'parent_file', [ $this, 'parent_file' ], 50 );
82 add_filter( 'submenu_file', [ $this, 'submenu_file' ], 50 );
83 add_action( 'init', [ $this, 'register_post_type' ] );
84 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
85 add_action( 'save_post_' . self::POST_TYPE_NAME, [ $this, 'save_post' ], 10, 2 );
86 add_action( 'posts_results', [ $this, 'posts_results' ], 20, 2 ); // Runs after core ES is done
87 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
88 add_filter( 'ep_sync_taxonomies', [ $this, 'filter_sync_taxonomies' ] );
89 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'weighting_fields_for_post_type' ], 1, 2 );
90 add_filter( 'ep_weighting_configuration_for_search', [ $this, 'filter_weighting_configuration' ], 10, 2 );
91 add_filter( 'ep_weighting_configuration_for_autosuggest', [ $this, 'filter_weighting_configuration' ], 10, 1 );
92 add_filter( 'ep_weighting_configuration_defaults_for_autosuggest', [ $this, 'filter_weighting_configuration' ], 10, 1 );
93 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'filter_default_post_type_weights' ], 10, 2 );
94 add_filter( 'enter_title_here', [ $this, 'filter_enter_title_here' ] );
95 add_filter( 'manage_' . self::POST_TYPE_NAME . '_posts_columns', [ $this, 'filter_column_names' ] );
96 add_filter( 'post_updated_messages', [ $this, 'filter_updated_messages' ] );
97 add_filter( 'admin_title', [ $this, 'update_page_title' ], 10, 2 );
98
99 // Deals with trashing/untrashing/deleting
100 add_action( 'wp_trash_post', [ $this, 'handle_post_trash' ] );
101 add_action( 'before_delete_post', [ $this, 'handle_post_trash' ] );
102 add_action( 'untrashed_post', [ $this, 'handle_post_untrash' ] );
103 add_filter( 'post_row_actions', [ $this, 'remove_quick_edit' ], 10, 2 );
104 add_filter( 'bulk_actions-edit-' . self::POST_TYPE_NAME, [ $this, 'remove_bulk_edit' ] );
105 }
106
107 /**
108 * Remove bulk edit
109 *
110 * @param array $actions Bulk actions
111 * @since 3.5
112 * @return array
113 */
114 public function remove_bulk_edit( $actions ) {
115 unset( $actions['edit'] );
116 return $actions;
117 }
118
119 /**
120 * Remove quick edit for post type
121 *
122 * @param array $actions Current table row actions
123 * @param WP_Post $post Current post
124 * @since 3.5
125 * @return array
126 */
127 public function remove_quick_edit( $actions, $post ) {
128 if ( self::POST_TYPE_NAME === $post->post_type ) {
129 // Remove "Quick Edit"
130 unset( $actions['inline hide-if-no-js'] );
131 }
132 return $actions;
133 }
134
135 /**
136 * Add updated messages for post type
137 *
138 * @param array $messages Messages array
139 * @since 3.2
140 * @return array
141 */
142 public function filter_updated_messages( $messages ) {
143 $post = get_post();
144 $post_type = get_post_type( $post );
145 $post_type_object = get_post_type_object( $post_type );
146
147 $messages[ self::POST_TYPE_NAME ] = array(
148 0 => '',
149 1 => esc_html__( 'Custom result updated.', 'elasticpress' ),
150 2 => esc_html__( 'Custom field updated.', 'elasticpress' ),
151 3 => esc_html__( 'Custom field deleted.', 'elasticpress' ),
152 4 => esc_html__( 'Custom result updated.', 'elasticpress' ),
153 /* translators: %s: date and time of the revision */
154 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
155 6 => esc_html__( 'Custom result published.', 'elasticpress' ),
156 7 => esc_html__( 'Custom result saved.', 'elasticpress' ),
157 8 => esc_html__( 'Custom result submitted.', 'elasticpress' ),
158 9 => sprintf(
159 // translators: Scheduled date.
160 esc_html__( 'Custom result scheduled for: %1$s.', 'elasticpress' ),
161 // translators: Publish box date format, see https://php.net/date
162 date_i18n( esc_html__( 'M j, Y @ G:i', 'elasticpress' ), strtotime( $post->post_date ) )
163 ),
164 10 => esc_html__( 'Custom result draft updated.', 'elasticpress' ),
165 );
166
167 return $messages;
168 }
169
170 /**
171 * Returns requirements status of feature
172 *
173 * Requires the search feature to be activated
174 *
175 * @return FeatureRequirementsStatus
176 */
177 public function requirements_status() {
178 /** Features Class @var Features $features */
179 $features = Features::factory();
180
181 /** Search Feature @var Feature\Search\Search $search */
182 $search = $features->get_registered_feature( 'search' );
183
184 if ( ! $search->is_active() ) {
185 return new FeatureRequirementsStatus( 2, esc_html__( 'This feature requires the "Post Search" feature to be enabled', 'elasticpress' ) );
186 }
187
188 return parent::requirements_status();
189 }
190
191 /**
192 * Output feature box long
193 */
194 public function output_feature_box_long() {
195 ?>
196 <p><?php esc_html_e( 'Selected posts will be inserted into search results in the specified position.', 'elasticpress' ); ?></p>
197 <?php
198 }
199
200 /**
201 * Adds this taxonomy as one of the taxonomies to index
202 *
203 * @param array $taxonomies Current indexable taxonomies
204 *
205 * @return array
206 */
207 public function filter_sync_taxonomies( $taxonomies ) {
208 $taxonomies[ self::TAXONOMY_NAME ] = get_taxonomy( self::TAXONOMY_NAME );
209
210 return $taxonomies;
211 }
212
213 /**
214 * Adds the search ordering to the admin menu
215 */
216 public function admin_menu() {
217 add_submenu_page(
218 'elasticpress',
219 esc_html__( 'Custom Results', 'elasticpress' ),
220 esc_html__( 'Custom Results', 'elasticpress' ),
221 Utils\get_capability(),
222 'edit.php?post_type=' . self::POST_TYPE_NAME
223 );
224 }
225
226 /**
227 * Sets the parent menu item for the post type submenu
228 *
229 * @param string $parent_file Current parent menu item
230 *
231 * @return string
232 */
233 public function parent_file( $parent_file ) {
234 global $current_screen;
235
236 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
237 return $parent_file;
238 }
239
240 // Set correct active/current menu and submenu in the WordPress Admin menu for the "pointer" CPT Add-New/Edit/List
241 if ( self::POST_TYPE_NAME === $current_screen->post_type ) {
242 $parent_file = 'elasticpress';
243 }
244
245 return $parent_file;
246 }
247
248 /**
249 * Ensures the correct item is highlighted when adding a new post
250 *
251 * @param string $submenu_file Current parent menu item
252 *
253 * @return string
254 */
255 public function submenu_file( $submenu_file ) {
256 global $current_screen;
257
258 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
259 return $submenu_file;
260 }
261
262 // Set correct active/current menu and submenu in the WordPress Admin menu for the "pointer" CPT Add-New/Edit/List
263 if ( self::POST_TYPE_NAME === $current_screen->post_type ) {
264 $submenu_file = 'edit.php?post_type=' . self::POST_TYPE_NAME;
265 }
266
267 return $submenu_file;
268 }
269
270 /**
271 * Registers the pointer post type for the injected results
272 */
273 public function register_post_type() {
274 $is_network = defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK;
275 $menu = $is_network ? null : false;
276
277 $labels = array(
278 'name' => esc_html_x( 'Custom Search Results', 'post type general name', 'elasticpress' ),
279 'singular_name' => esc_html_x( 'Custom Search Result', 'post type singular name', 'elasticpress' ),
280 'menu_name' => esc_html_x( 'Custom Search Results', 'admin menu', 'elasticpress' ),
281 'name_admin_bar' => esc_html_x( 'Custom Search Result', 'add new on admin bar', 'elasticpress' ),
282 'add_new' => esc_html_x( 'Add New', 'book', 'elasticpress' ),
283 'add_new_item' => esc_html__( 'Add New Custom Search Result', 'elasticpress' ),
284 'new_item' => esc_html__( 'New Custom Search Result', 'elasticpress' ),
285 'edit_item' => esc_html__( 'Edit Custom Search Result', 'elasticpress' ),
286 'view_item' => esc_html__( 'View Custom Search Result', 'elasticpress' ),
287 'all_items' => esc_html__( 'All Custom Search Results', 'elasticpress' ),
288 'search_items' => esc_html__( 'Search Custom Search Results', 'elasticpress' ),
289 'parent_item_colon' => esc_html__( 'Parent Custom Search Result:', 'elasticpress' ),
290 'not_found' => esc_html__( 'No results found.', 'elasticpress' ),
291 'not_found_in_trash' => esc_html__( 'No results found in Trash.', 'elasticpress' ),
292 );
293
294 $args = array(
295 'labels' => $labels,
296 'description' => esc_html__( 'Posts to inject into search results', 'elasticpress' ),
297 'public' => false,
298 'publicly_queryable' => false,
299 'show_ui' => true,
300 'show_in_menu' => $menu,
301 'query_var' => true,
302 'rewrite' => array( 'slug' => 'ep-pointer' ),
303 'capabilities' => Utils\get_post_map_capabilities(),
304 'has_archive' => false,
305 'hierarchical' => false,
306 'menu_position' => 100,
307 'supports' => [ 'title' ],
308 'register_meta_box_cb' => [ $this, 'register_meta_box' ],
309 'menu_icon' => 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNzMgNzEuMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNzMgNzEuMzsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik0zNi41LDQuN0MxOS40LDQuNyw1LjYsMTguNiw1LjYsMzUuN2MwLDEwLDQuNywxOC45LDEyLjEsMjQuNWw0LjUtNC41YzAuMS0wLjEsMC4xLTAuMiwwLjItMC4zbDAuNy0wLjdsNi40LTYuNGMyLjEsMS4yLDQuNSwxLjksNy4xLDEuOWM4LDAsMTQuNS02LjUsMTQuNS0xNC41cy02LjUtMTQuNS0xNC41LTE0LjVTMjIsMjcuNiwyMiwzNS42YzAsMi44LDAuOCw1LjMsMi4xLDcuNWwtNi40LDYuNGMtMi45LTMuOS00LjYtOC43LTQuNi0xMy45YzAtMTIuOSwxMC41LTIzLjQsMjMuNC0yMy40czIzLjQsMTAuNSwyMy40LDIzLjRTNDkuNCw1OSwzNi41LDU5Yy0yLjEsMC00LjEtMC4zLTYtMC44bC0wLjYsMC42bC01LjIsNS40YzMuNiwxLjUsNy42LDIuMywxMS44LDIuM2MxNy4xLDAsMzAuOS0xMy45LDMwLjktMzAuOVM1My42LDQuNywzNi41LDQuN3oiLz48L3N2Zz4=',
310 );
311
312 register_post_type( self::POST_TYPE_NAME, $args );
313
314 // Register taxonomy
315 $labels = array(
316 'name' => esc_html_x( 'Custom Results', 'taxonomy general name', 'elasticpress' ),
317 'singular_name' => esc_html_x( 'Custom Result', 'taxonomy singular name', 'elasticpress' ),
318 'search_items' => esc_html__( 'Search Custom Results', 'elasticpress' ),
319 'all_items' => esc_html__( 'All Custom Results', 'elasticpress' ),
320 'parent_item' => esc_html__( 'Parent Custom Result', 'elasticpress' ),
321 'parent_item_colon' => esc_html__( 'Parent Custom Result:', 'elasticpress' ),
322 'edit_item' => esc_html__( 'Edit Custom Result', 'elasticpress' ),
323 'update_item' => esc_html__( 'Update Custom Result', 'elasticpress' ),
324 'add_new_item' => esc_html__( 'Add New Custom Result', 'elasticpress' ),
325 'new_item_name' => esc_html__( 'New Custom Result Name', 'elasticpress' ),
326 'menu_name' => esc_html__( 'Custom Results', 'elasticpress' ),
327 );
328
329 $args = array(
330 'hierarchical' => false,
331 'labels' => $labels,
332 'show_ui' => false,
333 'show_admin_column' => false,
334 'query_var' => false,
335 'rewrite' => false,
336 );
337
338 /** Features Class @var Features $features */
339 $features = Features::factory();
340
341 /** Search Feature @var Feature\Search\Search $search */
342 $search = $features->get_registered_feature( 'search' );
343
344 $post_types = $search->get_searchable_post_types();
345
346 register_taxonomy( 'ep_custom_result', $post_types, $args );
347 }
348
349 /**
350 * Registers meta box for the search pointers
351 */
352 public function register_meta_box() {
353 add_meta_box( 'ep-ordering', esc_html__( 'Manage Results', 'elasticpress' ), [ $this, 'render_meta_box' ], self::POST_TYPE_NAME, 'normal' );
354 }
355
356 /**
357 * Renders the meta box for the injected search results
358 *
359 * @param \WP_Post $post Current post object
360 */
361 public function render_meta_box( $post ) {
362 ?>
363 <div id="ordering-app"></div>
364 <?php
365 }
366
367 /**
368 * Sends initial pointer data to the frontend to reduce API requests required
369 *
370 * @return array
371 */
372 public function get_pointer_data_for_localize() {
373 $post_id = get_the_ID();
374
375 $pointers = get_post_meta( $post_id, 'pointers', true );
376
377 if ( empty( $pointers ) ) {
378 return [
379 'pointers' => [],
380 'posts' => [],
381 ];
382 }
383
384 $post_ids = wp_list_pluck( $pointers, 'ID' );
385
386 $query = new \WP_Query(
387 [
388 'post_type' => 'any',
389 'post__in' => $post_ids,
390 'count' => count( $post_ids ),
391 'orderby' => 'post__in',
392 ]
393 );
394
395 $final_posts = [];
396 $filtered_pointers = [];
397
398 foreach ( $query->posts as $post ) {
399 $final_posts[ $post->ID ] = $post;
400 // Add the post to filtered array. By doing this, we removed the posts that don't exist anymore.
401 $filtered_pointers[] = $pointers[ array_search( $post->ID, $post_ids, true ) ];
402 }
403
404 return [
405 'pointers' => $filtered_pointers,
406 'posts' => $final_posts,
407 ];
408 }
409
410 /**
411 * Enqueues scripts for admin interface
412 */
413 public function admin_enqueue_scripts() {
414 global $pagenow; // post-new.php or post.php
415
416 $screen = get_current_screen();
417
418 if ( in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) && $screen instanceof \WP_Screen && self::POST_TYPE_NAME === $screen->post_type ) {
419 wp_enqueue_script(
420 'ep_ordering_scripts',
421 EP_URL . 'dist/js/ordering-script.js',
422 Utils\get_asset_info( 'ordering-script', 'dependencies' ),
423 Utils\get_asset_info( 'ordering-script', 'version' ),
424 true
425 );
426
427 wp_set_script_translations( 'ep_ordering_scripts', 'elasticpress' );
428
429 wp_enqueue_style(
430 'ep_ordering_styles',
431 EP_URL . 'dist/css/ordering-styles.css',
432 Utils\get_asset_info( 'ordering-styles', 'dependencies' ),
433 Utils\get_asset_info( 'ordering-styles', 'version' )
434 );
435
436 $pointer_data = $this->get_pointer_data_for_localize();
437
438 wp_localize_script(
439 'ep_ordering_scripts',
440 'epOrdering',
441 array_merge(
442 [
443 'searchEndpoint' => rest_url( 'elasticpress/v1/pointer_search' ),
444 'nonce' => wp_create_nonce( 'save-search-ordering' ),
445 'restApiRoot' => rest_url( '/' ),
446 'postsPerPage' => (int) get_option( 'posts_per_page', 10 ),
447 ],
448 $pointer_data
449 )
450 );
451 }
452 }
453
454 /**
455 * Handles saving the injected post settings
456 *
457 * @param int $post_id Post ID of the post being saved
458 * @param \WP_Post $post Post object being saved
459 */
460 public function save_post( $post_id, $post ) {
461 /** Post Indexable @var Post $post_indexable */
462 $post_indexable = Indexables::factory()->get( 'post' );
463
464 if ( ! isset( $_POST['search-ordering-nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['search-ordering-nonce'] ), 'save-search-ordering' ) ) {
465 return;
466 }
467
468 if ( ! current_user_can( 'edit_post', $post_id ) ) {
469 return;
470 }
471
472 $final_order_data = [];
473
474 // Track the old IDs that aren't retained so we can delete the terms later
475 $previous_order_data = get_post_meta( $post_id, 'pointers', true );
476 $previous_post_ids = ! empty( $previous_order_data ) ? array_flip( wp_list_pluck( $previous_order_data, 'ID' ) ) : [];
477
478 $ordered_posts = isset( $_POST['ordered_posts'] ) ? wp_unslash( $_POST['ordered_posts'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
479 $ordered_posts = json_decode( $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( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
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