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 / Search / Synonyms.php

Synonyms.php in ElasticPress 5.3.5, at includes/classes/Feature/Search/Synonyms.php

906 lines 23.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Synonyms Feature
4 *
5 * @package elasticpress
6 */
7
8 namespace ElasticPress\Feature\Search;
9
10 use ElasticPress\Elasticsearch;
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 * Synonyms Feature
23 *
24 * @since 3.4
25 * @package ElasticPress\Feature\Synonyms
26 */
27 class Synonyms {
28
29 /**
30 * Internal name of the post type
31 */
32 const POST_TYPE_NAME = 'ep-synonym';
33
34 /**
35 * Indices that should receive the synonym filter.
36 *
37 * @var array
38 */
39 public $affected_indices;
40
41 /**
42 * Elasticsearch Synonym Filter Name
43 *
44 * @var string
45 */
46 public $filter_name;
47
48 /**
49 * Synonym post id.
50 *
51 * @var int
52 */
53 protected $synonym_post_id;
54
55 /**
56 * Initialize feature setting it's config
57 *
58 * @since 3.4
59 */
60 public function __construct() {
61 $this->filter_name = 'ep_synonyms_filter';
62 $this->affected_indices = [ 'post' ];
63 }
64
65 /**
66 * Get search feature.
67 *
68 * @return Search
69 */
70 public function get_search_feature() {
71 /** Features Class @var Features $features */
72 $features = Features::factory();
73
74 /** Search Feature @var Feature\Search\Search $search */
75 return $features->get_registered_feature( 'search' );
76 }
77
78 /**
79 * Returns requirements status of feature
80 *
81 * Requires the search feature to be activated
82 *
83 * @return FeatureRequirementsStatus
84 */
85 public function requirements_status() {
86 $status = new FeatureRequirementsStatus( 0, null, $this );
87 $search = $this->get_search_feature();
88
89 if ( ! $search->is_active() ) {
90 $status->code = 2;
91 $status->message = esc_html__( 'This feature requires the "Post Search" feature to be enabled', 'elasticpress' );
92 }
93
94 return $status;
95 }
96
97 /**
98 * Setup Feature Functionality
99 *
100 * @return bool
101 */
102 public function setup() {
103 if ( (bool) $this->requirements_status()->get_code() ) {
104 return false;
105 }
106
107 // Register a post type to hold the synonyms post.
108 add_action( 'init', [ $this, 'register_post_type' ] );
109
110 // Setup the UI.
111 add_action( 'admin_menu', [ $this, 'admin_menu' ], 50 );
112 add_action( 'admin_enqueue_scripts', [ $this, 'scripts' ] );
113
114 // Add the synonyms to the elasticsearch query.
115 add_filter( 'ep_config_mapping', [ $this, 'add_search_synonyms' ], 20, 2 );
116
117 // Register REST routes.
118 add_action( 'rest_api_init', [ $this, 'setup_endpoint' ] );
119
120 return true;
121 }
122
123 /**
124 * Enqueues scripts and styles.
125 *
126 * @return void
127 */
128 public function scripts() {
129 if ( ! $this->is_synonym_page() ) {
130 return;
131 }
132
133 wp_enqueue_script(
134 'ep_synonyms_scripts',
135 EP_URL . 'dist/js/synonyms-script.js',
136 Utils\get_asset_info( 'synonyms-script', 'dependencies' ),
137 Utils\get_asset_info( 'synonyms-script', 'version' ),
138 true
139 );
140
141 wp_set_script_translations( 'ep_synonyms_scripts', 'elasticpress' );
142
143 wp_enqueue_style( 'wp-edit-post' );
144
145 wp_enqueue_style(
146 'ep_synonyms_scripts',
147 EP_URL . 'dist/css/synonyms-script.css',
148 [ 'wp-components', 'wp-edit-post' ],
149 Utils\get_asset_info( 'synonyms-styles', 'version' ),
150 'all'
151 );
152
153 wp_enqueue_style(
154 'ep_synonyms_styles',
155 EP_URL . 'dist/css/synonyms-styles.css',
156 Utils\get_asset_info( 'synonyms-styles', 'dependencies' ),
157 Utils\get_asset_info( 'synonyms-styles', 'version' ),
158 'all'
159 );
160
161 $api_url = rest_url( 'elasticpress/v1/synonyms' );
162 $sync_url = Utils\get_sync_url();
163
164 wp_localize_script(
165 'ep_synonyms_scripts',
166 'epSynonyms',
167 [
168 'apiUrl' => esc_url_raw( $api_url ),
169 'defaultIsSolr' => $this->synonyms_editor_mode() === 'advanced',
170 'defaultSolr' => $this->get_synonyms_raw(),
171 'syncUrl' => esc_url_raw( $sync_url ),
172 ]
173 );
174 }
175
176 /**
177 * Adds the synonyms settings page to the admin menu.
178 *
179 * @return void
180 */
181 public function admin_menu() {
182 add_submenu_page(
183 'elasticpress',
184 esc_html__( 'ElasticPress Synonyms', 'elasticpress' ),
185 esc_html__( 'Synonyms', 'elasticpress' ),
186 Utils\get_capability( 'synonyms' ),
187 'elasticpress-synonyms',
188 [ $this, 'admin_page' ]
189 );
190 }
191
192 /**
193 * Renders the synonyms settings page.
194 *
195 * @return void
196 */
197 public function admin_page() {
198 include EP_PATH . '/includes/partials/header.php';
199
200 ?>
201 <div class="wrap">
202 <div id="ep-synonyms"></div>
203 </div>
204 <?php
205 }
206
207 /**
208 * Admin notices.
209 *
210 * @return void
211 * @deprecated 5.1.0
212 */
213 public function admin_notices() {
214 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::admin_notices', '5.1.0' );
215
216 if ( ! $this->is_synonym_page() ) {
217 return;
218 }
219
220 $update = filter_input( INPUT_GET, 'ep_synonym_update', FILTER_SANITIZE_SPECIAL_CHARS );
221
222 if ( ! in_array( $update, [ 'success', 'error-update-post', 'error-update-index' ], true ) ) {
223 return;
224 }
225
226 $class = ( 'success' === $update ? 'notice-success' : 'notice-error' ) . ' notice';
227 $message = '';
228
229 switch ( $update ) {
230 case 'success':
231 $message = __( 'Successfully updated synonym filter.', 'elasticpress' );
232 break;
233 case 'error-update-post':
234 $message = __( 'There was an error storing your synonyms.', 'elasticpress' );
235 break;
236 case 'error-update-index':
237 $message = __( 'There was a problem updating the index with your synonyms. If you have not indexed your data, please run an index.', 'elasticpress' );
238 break;
239 default:
240 $message = __( 'There was an error updating the synonym list.', 'elasticpress' );
241 }
242
243 printf(
244 '<div class="%1$s"><p>%2$s</p></div>',
245 esc_attr( $class ),
246 esc_html( $message )
247 );
248 }
249
250 /**
251 * Registers a post type for our synonyms post storage.
252 *
253 * @return void
254 */
255 public function register_post_type() {
256 $args = [
257 'description' => esc_html__( 'Elasticsearch Synonyms', 'elasticpress' ),
258 'public' => false,
259 'publicly_queryable' => false,
260 'show_ui' => false,
261 'show_in_menu' => false,
262 'query_var' => true,
263 'capabilities' => Utils\get_post_map_capabilities( 'synonyms' ),
264 'has_archive' => false,
265 'hierarchical' => false,
266 'menu_position' => 100,
267 'supports' => [ 'title' ],
268 ];
269
270 register_post_type( self::POST_TYPE_NAME, $args );
271 }
272
273 /**
274 * Get the post id of the post holding our synonyms.
275 *
276 * @return int The synonym post ID.
277 */
278 public function get_synonym_post_id() {
279 if ( ! $this->synonym_post_id ) {
280 $query_synonym_post = new \WP_Query(
281 array(
282 'fields' => 'ids',
283 'post_type' => self::POST_TYPE_NAME,
284 'posts_per_page' => 1,
285 'orderby' => 'modified',
286 'post_status' => 'any',
287 )
288 );
289
290 $this->synonym_post_id = ( $query_synonym_post->post_count >= 1 ) ? $query_synonym_post->posts[0] : false;
291
292 if ( ! $this->synonym_post_id ) {
293 $this->synonym_post_id = $this->insert_default_synonym_post();
294 }
295 }
296
297 return $this->synonym_post_id;
298 }
299
300 /**
301 * Get synonyms in their raw format.
302 *
303 * @return string
304 */
305 public function get_synonyms_raw() {
306 $post = get_post( $this->get_synonym_post_id() );
307
308 if ( ! $post ) {
309 return '';
310 }
311
312 return $post->post_content;
313 }
314
315 /**
316 * Get an array of user defined synonyms.
317 *
318 * @return array
319 */
320 public function get_synonyms() {
321 $synonyms_raw = $this->get_synonyms_raw();
322 $synonyms = array_values(
323 array_filter(
324 array_map( [ $this, 'validate_synonym' ], preg_split( '/\r\n|\r|\n/', $synonyms_raw ) )
325 )
326 );
327
328 /**
329 * Filter array of synonyms to add to a custom synonym filter.
330 *
331 * @hook ep_synonyms
332 * @return {array} The new array of search synonyms.
333 */
334 return apply_filters( 'ep_synonyms', $synonyms );
335 }
336
337 /**
338 * Validate a synonym.
339 *
340 * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html#_solr_synonyms
341 * @param string $synonym The synonym.
342 * @return string|boolean String synonym if valid, boolean false if validation failed.
343 */
344 public function validate_synonym( $synonym ) {
345 // Don't use empty lines.
346 if ( empty( trim( $synonym ) ) ) {
347 return false;
348 }
349
350 // Don't use lines that start with "#", those are comments.
351 if ( 0 === strpos( $synonym, '#' ) ) {
352 return false;
353 }
354
355 // Don't use lines that start with "//" though not in Solr spec.
356 if ( 0 === strpos( $synonym, '//' ) ) {
357 return false;
358 }
359
360 return sanitize_text_field( $synonym, true );
361 }
362
363 /**
364 * Add search synonyms.
365 *
366 * @param array $mapping Elasticsearch mapping.
367 * @param string $index Index name.
368 * @return array
369 */
370 public function add_search_synonyms( $mapping, $index ) {
371 $synonyms = $this->get_synonyms();
372 $indices = $this->get_affected_indices();
373 $filter_name = $this->get_synonym_filter_name();
374
375 // Ensure we should affect this mapping.
376 if ( ! in_array( $index, $indices, true ) ) {
377 return $mapping;
378 }
379
380 // Ensure we have synonyms to add.
381 if ( ! is_array( $synonyms ) || empty( $synonyms ) ) {
382 return $mapping;
383 }
384
385 // Ensure we have filters and that it is an array.
386 if ( ! isset( $mapping['settings']['analysis']['filter'] )
387 || ! is_array( $mapping['settings']['analysis']['filter'] )
388 ) {
389 return $mapping;
390 }
391
392 // Ensure we have analyzers and that it is an array.
393 if ( ! isset( $mapping['settings']['analysis']['analyzer']['default_search']['filter'] )
394 || ! is_array( $mapping['settings']['analysis']['analyzer']['default_search']['filter'] )
395 ) {
396 return $mapping;
397 }
398
399 // Create a custom synonym filter for EP.
400 $mapping['settings']['analysis']['filter'][ $filter_name ] = $this->get_synonym_filter();
401
402 // Tell the analyzer to use our newly created filter.
403 $mapping['settings']['analysis']['analyzer']['default_search']['filter'] = $this->maybe_change_filter_position(
404 array_values(
405 array_merge(
406 [ $filter_name ],
407 $mapping['settings']['analysis']['analyzer']['default_search']['filter'],
408 )
409 )
410 );
411
412 return $mapping;
413 }
414
415 /**
416 * Handles updating the synonym list.
417 *
418 * @return void
419 * @deprecated 5.1.0
420 */
421 public function handle_update_synonyms() {
422 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::handle_update_synonyms', '5.1.0' );
423
424 $nonce = filter_input( INPUT_POST, $this->get_nonce_field(), FILTER_SANITIZE_SPECIAL_CHARS );
425 $referer = filter_input( INPUT_POST, '_wp_http_referer', FILTER_SANITIZE_URL );
426 $post_id = false;
427 $update = false;
428
429 if ( wp_verify_nonce( $nonce, $this->get_nonce_action() ) ) {
430 $synonyms = filter_input( INPUT_POST, $this->get_synonym_field(), FILTER_CALLBACK, [ 'options' => 'wp_strip_all_tags' ] );
431 $mode = filter_input( INPUT_POST, 'synonyms_editor_mode', FILTER_SANITIZE_SPECIAL_CHARS );
432 $content = trim( sanitize_textarea_field( $synonyms ) );
433
434 // Content can't be empty.
435 if ( empty( $content ) ) {
436 $lines = $this->example_synonym_list( true );
437 $content = implode( PHP_EOL, [ $lines[0], $lines[2], $lines[3] ] );
438 }
439
440 $post_id = $this->update_synonym_post( $content );
441
442 // Update Elasticsearch
443 $update = $this->update_synonyms();
444
445 // Save editor mode.
446 if ( in_array( $mode, [ 'advanced', 'simple' ], true ) ) {
447 $this->save_editor_mode( $mode );
448 }
449 }
450
451 $result = 'success';
452
453 if ( ! $post_id || is_wp_error( $post_id ) ) {
454 $result = 'error-update-post';
455 }
456
457 if ( ! $update ) {
458 $result = 'error-update-index';
459 }
460
461 wp_safe_redirect(
462 add_query_arg(
463 [
464 'ep_synonym_update' => $result,
465 ],
466 esc_url_raw( $referer )
467 )
468 );
469 exit;
470 }
471
472 /**
473 * Update synonyms.
474 *
475 * @return boolean
476 */
477 public function update_synonyms() {
478 return array_reduce(
479 $this->get_affected_indices(),
480 function ( $success, $index ) {
481 $filter = $this->get_synonym_filter();
482 $mapping = Elasticsearch::factory()->get_mapping( $index );
483
484 if ( empty( $mapping ) || empty( $mapping[ $index ] ) ) {
485 return false;
486 }
487
488 $filters = (array) $mapping[ $index ]['settings']['index']['analysis']['analyzer']['default_search']['filter'];
489
490 /*
491 * Due to limitations in Elasticsearch, we can't remove the filter and analyzer
492 * once set on the index settings and synonyms array can't be empty. So we set a
493 * fallback synonyms array here if the user supplied synonym array is empty.
494 */
495 if ( empty( $filter['synonyms'] ) ) {
496 $filter['synonyms'] = [ 'odd,unusual' ];
497 }
498
499 // Construct the synonym filter.
500 $setting['index']['analysis']['filter']['ep_synonyms_filter'] = $filter;
501
502 // Add the analyzer.
503 $setting['index']['analysis']['analyzer']['default_search']['filter'] = $this->maybe_change_filter_position(
504 array_values(
505 array_unique(
506 array_merge(
507 [ $this->get_synonym_filter_name() ],
508 $filters
509 )
510 )
511 )
512 );
513
514 // Put it to Elasticsearch.
515 $update = Elasticsearch::factory()->update_index_settings( $index, $setting, true );
516 return $success ? $update : false;
517 },
518 true
519 );
520 }
521
522 /**
523 * Get affected indices.
524 *
525 * @return array
526 */
527 public function get_affected_indices() {
528 /**
529 * Filter the indices that use the synonym filter.
530 *
531 * @return array Array of index names.
532 */
533 $indices = apply_filters( 'ep_synonyms_affected_indices', $this->affected_indices );
534
535 return array_filter(
536 array_map(
537 function ( $index ) {
538 $indexable = Indexables::factory()->get( $index );
539 return $indexable ? $indexable->get_index_name() : false;
540 },
541 $indices
542 )
543 );
544 }
545
546 /**
547 * Get synonym filter name.
548 *
549 * @return string
550 */
551 public function get_synonym_filter_name() {
552 /**
553 * Filter name of the synonym filter set in elasticsearch.
554 *
555 * @hook ep_synonyms_filter_name
556 * @return {string} The name of the synonyms filter.
557 */
558 return apply_filters( 'ep_synonyms_filter_name', $this->filter_name );
559 }
560
561 /**
562 * Get synonym filter.
563 *
564 * @return array
565 */
566 public function get_synonym_filter() {
567 /**
568 * Filter the synonym filter set in elasticsearch.
569 *
570 * @hook ep_synonyms_filter
571 * @return {array} The synonym search filter.
572 */
573 return apply_filters(
574 'ep_synonyms_filter',
575 [
576 'type' => 'synonym_graph',
577 'lenient' => true,
578 'synonyms' => $this->get_synonyms(),
579 ]
580 );
581 }
582
583 /**
584 * Get form action for admin page.
585 *
586 * @access protected
587 * @return string The admin post form action url.
588 * @deprecated 5.1.0
589 */
590 public function get_form_action() {
591 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_form_action', '5.1.0' );
592
593 return esc_url_raw( admin_url( 'admin-post.php' ) );
594 }
595
596 /**
597 * Render admin page form hidden fields.
598 *
599 * @return void
600 * @deprecated 5.1.0
601 */
602 public function form_hidden_fields() {
603 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_form_action', '5.1.0', );
604
605 wp_nonce_field( $this->get_nonce_action(), $this->get_nonce_field() );
606 ?>
607 <input type="hidden" name="action" value="<?php echo esc_attr( $this->get_action() ); ?>" />
608 <?php
609 }
610
611 /**
612 * Get nonce action for admin page form.
613 *
614 * @return string
615 * @deprecated 5.1.0
616 */
617 public function get_nonce_action() {
618 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_form_action', '5.1.0', );
619
620 return $this->get_action();
621 }
622
623 /**
624 * Get nonce field for admin page form.
625 *
626 * @return string
627 * @deprecated 5.1.0
628 */
629 public function get_nonce_field() {
630 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_nonce_field', '5.1.0', );
631
632 return 'ep_synonyms_nonce';
633 }
634
635 /**
636 * Get synonym field name for admin page form.
637 *
638 * @return string
639 * @deprecated 5.1.0
640 */
641 public function get_synonym_field() {
642 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_synonym_field', '5.1.0', );
643
644 return 'ep_synonyms';
645 }
646
647 /**
648 * Get the action slug for admin page form.
649 *
650 * @return string
651 * @deprecated 5.1.0
652 */
653 public function get_action() {
654 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_action', '5.1.0', );
655
656 return 'ep_synonyms_update';
657 }
658
659 /**
660 * Is this our synonym page.
661 *
662 * @return boolean
663 */
664 public function is_synonym_page() {
665 if ( ! function_exists( '\get_current_screen' ) ) {
666 return false;
667 }
668
669 $screen = get_current_screen();
670 return ( 'elasticpress_page_elasticpress-synonyms' === $screen->base );
671 }
672
673 /**
674 * An example synonym that we initialize new synonyms lists with.
675 *
676 * @param bool $as_array Optional. Return an array of synonym lines. Default false.
677 * @return string
678 */
679 public function example_synonym_list( $as_array = false ) {
680 $lines = [
681 __( '# Defined synonyms.', 'elasticpress' ),
682 'runner, running shoe, sneaker, tennis shoe, trainer',
683 '',
684 __( '# Defined hyponyms.', 'elasticpress' ),
685 'blue => blue, aqua, azure, cerulean, cyan, ultramarine',
686 '',
687 __( '# Defined replacements.', 'elasticpress' ),
688 'supposably => supposedly',
689 'flustrated => flustered, frustrated',
690 'intensive purposes => intents and purposes',
691 ];
692
693 return $as_array ? $lines : implode( PHP_EOL, $lines );
694 }
695
696 /**
697 * Gets localized strings for use on the front end.
698 *
699 * @return array
700 * @deprecated 5.1.0
701 */
702 public function get_localized_strings() {
703 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_localized_strings', '5.1.0' );
704
705 return array(
706 'pageHeading' => __( 'Manage Synonyms', 'elasticpress' ),
707 'pageDescription' => __( 'Synonyms enable more flexible search results that show relevant results even without an exact match. Synonyms can be defined as a sets where all words are synonyms for each other, or as alternatives where searches for the primary word will also match the rest, but no vice versa.', 'elasticpress' ),
708 'pageToggleAdvanceText' => __( 'Switch to Advanced Text Editor', 'elasticpress' ),
709 'pageToggleSimpleText' => __( 'Switch to Visual Editor', 'elasticpress' ),
710
711 'setsTitle' => __( 'Sets', 'elasticpress' ),
712 'setsDescription' => __( 'Sets are terms that will all match each other for search results. This is useful where all words are considered equivalent, such as product renaming or regional variations like sneakers, tennis shoes, trainers, and runners.', 'elasticpress' ),
713 'setsInputHeading' => __( 'Comma separated list of terms', 'elasticpress' ),
714 'setsAddButtonText' => __( 'Add Set', 'elasticpress' ),
715 'setsErrorMessage' => __( 'This set must contain at least 2 terms.', 'elasticpress' ),
716
717 'alternativesTitle' => __( 'Alternatives', 'elasticpress' ),
718 'alternativesDescription' => __( 'Alternatives are terms that will also be matched when you search for the primary term. For instance, a search for shoes can also include results for sneaker, sandals, boots, and high heels.', 'elasticpress' ),
719 'alternativesPrimaryHeading' => __( 'Primary term', 'elasticpress' ),
720 'alternativesInputHeading' => __( 'Comma separated list of alternatives', 'elasticpress' ),
721 'alternativesAddButtonText' => __( 'Add Alternative', 'elasticpress' ),
722 'alternativesErrorMessage' => __( 'You must enter both a primary term and at least one alternative term.', 'elasticpress' ),
723
724 'solrTitle' => __( 'Advanced Synonym Editor', 'elasticpress' ),
725 'solrDescription' => __( 'When you add Sets and Alternatives above, we reduce them to SolrSynonyms which Elasticsearch can understand. If you are an advanced user, you can edit synonyms directly using Solr synonym formatting. This is beneficial if you want to import a large dictionary of synonyms, or want to export this site\'s synonyms for use on another site.', 'elasticpress' ),
726 'solrInputHeading' => __( 'SolrSynonym Text', 'elasticpress' ),
727 'solrAlternativesErrorMessage' => __( 'Alternatives must have both a primary term and at least one alternative term.', 'elasticpress' ),
728 'solrSetsErrorMessage' => __( 'Sets must contain at least 2 terms.', 'elasticpress' ),
729
730 'removeItemText' => __( 'Remove', 'elasticpress' ),
731 'submitText' => __( 'Update Synonyms', 'elasticpress' ),
732
733 'synonymsTextareaInputName' => $this->get_synonym_field(),
734 );
735 }
736
737 /**
738 * Get data to export to the frontend with localization strings.
739 *
740 * @return array
741 * @deprecated 5.1.0
742 */
743 public function get_localized_data() {
744 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_localized_strings', '5.1.0' );
745
746 $data = array(
747 'sets' => array(),
748 'alternatives' => array(),
749 'initialMode' => $this->synonyms_editor_mode(),
750 );
751 $synonyms = $this->get_synonyms();
752
753 foreach ( $synonyms as $line ) {
754 $synonym = array();
755 if ( strpos( $line, '=>' ) ) {
756 $tokens = explode( '=>', $line );
757 array_push( $synonym, self::prepare_localized_token( $tokens[0], true ) );
758 array_push(
759 $synonym,
760 ...array_map(
761 array( __CLASS__, 'prepare_localized_token' ),
762 explode( ',', $tokens[1] )
763 )
764 );
765 array_push( $data['alternatives'], $synonym );
766 } else {
767 array_push(
768 $synonym,
769 ...array_map(
770 array( __CLASS__, 'prepare_localized_token' ),
771 explode( ',', $line )
772 )
773 );
774 array_push( $data['sets'], $synonym );
775 }
776 }
777
778 return $data;
779 }
780
781 /**
782 * Saves the editor mode.
783 *
784 * @param string $mode The mode, one of "advanced" or "simple".
785 * @return void
786 */
787 public function save_editor_mode( $mode ) {
788 $search = $this->get_search_feature();
789 $settings = $search->get_settings();
790
791 if ( isset( $settings['synonyms_editor_mode'] ) && $settings['synonyms_editor_mode'] === $mode ) {
792 return;
793 }
794
795 $settings['synonyms_editor_mode'] = $mode;
796 $features = Features::factory();
797 $features->update_feature( $search->slug, $settings, false );
798 }
799
800 /**
801 * Get the stored editor mode. Default simple.
802 *
803 * @return boolean
804 */
805 public function synonyms_editor_mode() {
806 $search = $this->get_search_feature();
807 $settings = $search->get_settings();
808 $settings = wp_parse_args( is_array( $settings ) ? $settings : [], $search->default_settings );
809 $mode = $settings['synonyms_editor_mode'];
810
811 /**
812 * Filter the default synonyms editor mode.
813 *
814 * @hook ep_synonyms_editor_mode
815 * @return {string} One of 'simple' or 'advanced'.
816 */
817 $filtered = apply_filters( 'ep_synonyms_editor_mode', $mode );
818
819 return in_array( $filtered, [ 'simple', 'advanced' ], true ) ? $filtered : 'simple';
820 }
821
822 /**
823 * Prepare localized token.
824 *
825 * @param string $token The synonym token to prepare.
826 * @param boolean $primary Whether this string is the primary term of an alternative.
827 * @return array
828 * @deprecated 5.1.0
829 */
830 public static function prepare_localized_token( $token, $primary = false ) {
831 _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::prepare_localized_token', '5.1.0' );
832
833 return array(
834 'label' => trim( sanitize_text_field( $token ) ),
835 'value' => trim( sanitize_text_field( $token ) ),
836 'primary' => $primary,
837 );
838 }
839
840 /**
841 * Insert default synonym post
842 *
843 * @return int|WP_Error
844 */
845 private function insert_default_synonym_post() {
846 return wp_insert_post(
847 [
848 'post_content' => $this->example_synonym_list(),
849 'post_type' => self::POST_TYPE_NAME,
850 ],
851 true
852 );
853 }
854
855 /**
856 * Update synonym post
857 *
858 * @param string $content The content post.
859 * @return int|WP_Error
860 */
861 public function update_synonym_post( $content ) {
862 $synonym_post_id = $this->get_synonym_post_id();
863
864 if ( ! $synonym_post_id ) {
865 return $synonym_post_id;
866 }
867
868 return wp_insert_post(
869 [
870 'ID' => $synonym_post_id,
871 'post_content' => $content,
872 'post_type' => self::POST_TYPE_NAME,
873 ],
874 true
875 );
876 }
877
878 /**
879 * Setup REST endpoints
880 *
881 * @since 5.1.0
882 */
883 public function setup_endpoint() {
884 $controller = new REST\Synonyms();
885 $controller->register_routes();
886 }
887
888 /**
889 * Change the position of the lowercase filter to the beginning of the array.
890 *
891 * @since 5.1.0
892 * @param array $filters Array of filters.
893 * @return array
894 */
895 protected function maybe_change_filter_position( array $filters ): array {
896 $lowercase_filter = array_search( 'lowercase', $filters, true );
897
898 if ( false !== $lowercase_filter ) {
899 unset( $filters[ $lowercase_filter ] );
900 array_unshift( $filters, 'lowercase' );
901 }
902
903 return $filters;
904 }
905 }
906