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

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