Relevanssi – A Better Search - Version 4.10.0

Version Description

  • New feature: Relevanssi now supports multilingual synonyms and stopwords. Relevanssi now has a different set of synonyms and stopwords for each language. This feature is compatible with WPML and Polylang.
  • New feature: SEO by Rank Math compatibility is added: posts marked as 'noindex' with Rank Math are not indexed by Relevanssi.
  • Minor fix: With keyword matching set to 'whole words' and the 'expand highlights' disabled, words that ended with an 's' weren't highlighted correctly.
  • Minor fix: The 'Post exclusion' setting didn't work correctly. It has been fixed.
  • Minor fix: It's now impossible to set negative weights in searching settings. They did not work as expected anyway.
  • Minor fix: Relevanssi had an unnecessary index on the doc column in the wp_relevanssi database table. It is now removed to save space. Thanks to Matthew Wang.
  • Minor fix: Improved Oxygen Builder support makes sure ct_builder_shortcodes custom field is always indexed.
Download this release

Release Info

Developer msaari
Plugin Icon 128x128 Relevanssi – A Better Search
Version 4.10.0
Comparing to
See all releases

Code changes from version 4.9.1 to 4.10.0

changelog.txt CHANGED
@@ -1,3 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  = 4.5.0 =
2
  * New feature: New filter hook `relevanssi_disable_stopwords` can be used to disable stopwords completely. Just add a filter function that returns `true`.
3
  * Changed behaviour: Stopwords are no longer automatically restored if emptied. It's now possible to empty the stopword list. If you want to restore the stopwords from the file (or from the database, if you're upgrading from an earlier version of Relevanssi and find your stopwords missing), just click the button on the stopwords settings page that restores the stopwords.
1
+ = 4.7.2 =
2
+ * Minor fix: Media Library searches failed if Relevanssi was enabled in the WP admin, but the `attachment` post type wasn't indexed. Relevanssi will no longer block the default Media Library search in these cases.
3
+ * Minor fix: Adds more backwards compatibility for the `relevanssi_indexing_restriction` change, there's now an alert on indexing tab if there's a problem.
4
+
5
+ = 4.7.1 =
6
+ * New feature: New filter hook `relevanssi_post_content_after_shortcodes` filters the post content after shortcodes have been processed but before the HTML tags are stripped.
7
+ * Minor fix: Adds more backwards compatibility for the `relevanssi_indexing_restriction` change.
8
+
9
+ = 4.7.0 =
10
+ * New feature: New filter hook `relevanssi_admin_search_blocked_post_types` makes it easy to block Relevanssi from searching a specific post type in the admin dashboard. There's built-in support for Reusable Content Blocks `rc_blocks` post type, for example.
11
+ * New feature: The reason why a post is not indexed is now stored in the `_relevanssi_noindex_reason` custom field.
12
+ * Changed behaviour: The `relevanssi_indexing_restriction` filter hook has a changed format. Instead of a string value, the filter now expects an array with the MySQL query in the index 'mysql' and a reason in string format in 'reason'. There's some temporary backwards compatibility for this.
13
+ * Changed behaviour: Relevanssi now applies minimum word length when tokenizing search query terms.
14
+ * Changed behaviour: Content stopwords are removed from the search queries when doing excerpts and highlights. When Relevanssi uses the untokenized search terms for excerpt-building, stopwords are removed from those words. This should lead to better excerpts.
15
+ * Minor fix: Improves handling of emoji in indexing. If the database supports emoji, they are allowed, otherwise they are encoded.
16
+
17
+ = 4.6.0 =
18
+ * Changed behaviour: Phrases in OR search are now less restrictive. A search for 'foo "bar baz"' used to only return posts with the "bar baz" phrase, but now also posts with just the word 'foo' will be returned.
19
+ * Minor fix: User Access Manager showed drafts in search results for all users. This is now fixed.
20
+
21
  = 4.5.0 =
22
  * New feature: New filter hook `relevanssi_disable_stopwords` can be used to disable stopwords completely. Just add a filter function that returns `true`.
23
  * Changed behaviour: Stopwords are no longer automatically restored if emptied. It's now possible to empty the stopword list. If you want to restore the stopwords from the file (or from the database, if you're upgrading from an earlier version of Relevanssi and find your stopwords missing), just click the button on the stopwords settings page that restores the stopwords.
lib/common.php CHANGED
@@ -8,72 +8,6 @@
8
  * @see https://www.relevanssi.com/
9
  */
10
 
11
- /**
12
- * Multibyte friendly case-insensitive string comparison.
13
- *
14
- * If multibyte string functions are available, do strcmp() after using
15
- * mb_strtoupper() to both strings. Otherwise use strcasecmp().
16
- *
17
- * @param string $str1 First string to compare.
18
- * @param string $str2 Second string to compare.
19
- * @param string $encoding The encoding to use. Defaults to mb_internal_encoding().
20
- *
21
- * @return int $val Returns < 0 if str1 is less than str2; > 0 if str1 is greater
22
- * than str2, and 0 if they are equal.
23
- */
24
- function relevanssi_mb_strcasecmp( $str1, $str2, $encoding = null ) {
25
- if ( ! function_exists( 'mb_internal_encoding' ) ) {
26
- return strnatcasecmp( $str1, $str2 );
27
- } else {
28
- if ( null === $encoding ) {
29
- $encoding = mb_internal_encoding();
30
- }
31
- return strnatcmp( mb_strtoupper( $str1, $encoding ), mb_strtoupper( $str2, $encoding ) );
32
- }
33
- }
34
-
35
- /**
36
- * Multibyte friendly strtolower.
37
- *
38
- * If multibyte string functions are available, returns mb_strtolower() and falls
39
- * back to strtolower() if multibyte functions are not available.
40
- *
41
- * @param string $string The string to lowercase.
42
- *
43
- * @return string $string The string in lowercase.
44
- */
45
- function relevanssi_strtolower( $string ) {
46
- if ( ! function_exists( 'mb_strtolower' ) ) {
47
- return strtolower( $string );
48
- } else {
49
- return mb_strtolower( $string );
50
- }
51
- }
52
-
53
- /**
54
- * Multibyte friendly substr.
55
- *
56
- * If multibyte string functions are available, returns mb_substr() and falls
57
- * back to substr() if multibyte functions are not available.
58
- *
59
- * @param string $string The source string.
60
- * @param int $start If start is non-negative, the returned string will
61
- * start at the start'th position in str, counting from zero. If start is
62
- * negative, the returned string will start at the start'th character from the
63
- * end of string.
64
- * @param int $length Maximum number of characters to use from string. If
65
- * omitted or null is passed, extract all characters to the end of the string.
66
- *
67
- * @return string $string The string in lowercase.
68
- */
69
- function relevanssi_substr( $string, $start, $length = null ) {
70
- if ( ! function_exists( 'mb_substr' ) ) {
71
- return substr( $string, $start, $length );
72
- } else {
73
- return mb_substr( $string, $start, $length );
74
- }
75
- }
76
-
77
  /**
78
  * Adds the search result match breakdown to the post object.
79
  *
@@ -318,319 +252,6 @@ function relevanssi_populate_array( $matches ) {
318
  wp_suspend_cache_addition( false );
319
  }
320
 
321
- /**
322
- * Fetches the taxonomy based on term ID.
323
- *
324
- * Fetches the taxonomy from wp_term_taxonomy based on term_id.
325
- *
326
- * @global object $wpdb The WordPress database interface.
327
- * @param int $term_id The term ID.
328
- * @deprecated Will be removed in future versions.
329
- * @return string $taxonomy The term taxonomy.
330
- */
331
- function relevanssi_get_term_taxonomy( $term_id ) {
332
- global $wpdb;
333
-
334
- $taxonomy = $wpdb->get_var( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
335
- return $taxonomy;
336
- }
337
-
338
- /**
339
- * Extracts phrases from the search query.
340
- *
341
- * Finds all phrases wrapped in quotes from the search query.
342
- *
343
- * @param string $query The query.
344
- *
345
- * @return array An array of phrases (strings).
346
- */
347
- function relevanssi_extract_phrases( $query ) {
348
- $strpos_function = 'strpos';
349
- if ( function_exists( 'mb_strpos' ) ) {
350
- $strpos_function = 'mb_strpos';
351
- }
352
- $substr_function = 'substr';
353
- if ( function_exists( 'mb_substr' ) ) {
354
- $substr_function = 'mb_substr';
355
- }
356
-
357
- // iOS uses “” as the default quotes, so Relevanssi needs to understand that as
358
- // well.
359
- $normalized_query = str_replace( array( '”', '“' ), '"', $query );
360
- $pos = call_user_func( $strpos_function, $normalized_query, '"' );
361
-
362
- $phrases = array();
363
- while ( false !== $pos ) {
364
- if ( $pos + 2 > relevanssi_strlen( $normalized_query ) ) {
365
- $pos = false;
366
- continue;
367
- }
368
- $start = call_user_func( $strpos_function, $normalized_query, '"', $pos );
369
- $end = false;
370
- if ( false !== $start ) {
371
- $end = call_user_func( $strpos_function, $normalized_query, '"', $start + 2 );
372
- }
373
- if ( false === $end ) {
374
- // Just one " in the query.
375
- $pos = $end;
376
- continue;
377
- }
378
- $phrase = call_user_func( $substr_function, $normalized_query, $start + 1, $end - $start - 1 );
379
- $phrase = trim( $phrase );
380
-
381
- // Do not count single-word phrases as phrases.
382
- if ( ! empty( $phrase ) && count( explode( ' ', $phrase ) ) > 1 ) {
383
- $phrases[] = $phrase;
384
- }
385
- $pos = $end + 1;
386
- }
387
-
388
- return $phrases;
389
- }
390
-
391
- /**
392
- * Generates the MySQL code for restricting the search to phrase hits.
393
- *
394
- * This function uses relevanssi_extract_phrases() to figure out the phrases in
395
- * the search query, then generates MySQL queries to restrict the search to the
396
- * posts containing those phrases in the title, content, taxonomy terms or meta
397
- * fields.
398
- *
399
- * @global array $relevanssi_variables The global Relevanssi variables.
400
- *
401
- * @param string $search_query The search query.
402
- * @param string $operator The search operator (AND or OR).
403
- *
404
- * @return string $queries If not phrase hits are found, an empty string;
405
- * otherwise MySQL queries to restrict the search.
406
- */
407
- function relevanssi_recognize_phrases( $search_query, $operator = 'AND' ) {
408
- global $relevanssi_variables;
409
-
410
- $phrases = relevanssi_extract_phrases( $search_query );
411
-
412
- $all_queries = array();
413
- if ( 0 === count( $phrases ) ) {
414
- return $all_queries;
415
- }
416
-
417
- $custom_fields = relevanssi_get_custom_fields();
418
- $taxonomies = get_option( 'relevanssi_index_taxonomies_list', array() );
419
- $excerpts = get_option( 'relevanssi_index_excerpt', 'off' );
420
- $index_pdf_parent = get_option( 'relevanssi_index_pdf_parent' );
421
-
422
- $phrase_queries = array();
423
- $queries = array();
424
-
425
- if (
426
- isset( $relevanssi_variables['phrase_targets'] ) &&
427
- is_array( $relevanssi_variables['phrase_targets'] )
428
- ) {
429
- $non_targeted_phrases = array();
430
- foreach ( $phrases as $phrase ) {
431
- if (
432
- isset( $relevanssi_variables['phrase_targets'][ $phrase ] ) &&
433
- function_exists( 'relevanssi_targeted_phrases' )
434
- ) {
435
- $queries = relevanssi_targeted_phrases( $phrase );
436
- } else {
437
- $non_targeted_phrases[] = $phrase;
438
- }
439
- }
440
- $phrases = $non_targeted_phrases;
441
- }
442
-
443
- $queries = array_merge(
444
- $queries,
445
- relevanssi_generate_phrase_queries(
446
- $phrases,
447
- $taxonomies,
448
- $custom_fields,
449
- $excerpts,
450
- $index_pdf_parent
451
- )
452
- );
453
-
454
- $phrase_queries = array();
455
-
456
- foreach ( $queries as $phrase => $p_queries ) {
457
- $p_queries = implode( ' OR relevanssi.doc IN ', $p_queries );
458
- $p_queries = "(relevanssi.doc IN $p_queries)";
459
- $all_queries[] = $p_queries;
460
-
461
- $phrase_queries[ $phrase ] = $p_queries;
462
- }
463
-
464
- $operator = strtoupper( $operator );
465
- if ( 'AND' !== $operator && 'OR' !== $operator ) {
466
- $operator = 'AND';
467
- }
468
-
469
- if ( ! empty( $all_queries ) ) {
470
- $all_queries = ' AND ( ' . implode( ' ' . $operator . ' ', $all_queries ) . ' ) ';
471
- }
472
-
473
- return array(
474
- 'and' => $all_queries,
475
- 'or' => $phrase_queries,
476
- );
477
- }
478
-
479
- /**
480
- * Generates the phrase queries from phrases.
481
- *
482
- * Takes in phrases and a bunch of parameters and generates the MySQL queries
483
- * that restrict the main search query to only posts that have the phrase.
484
- *
485
- * @param array $phrases A list of phrases to handle.
486
- * @param array $taxonomies An array of taxonomy names to use.
487
- * @param array $custom_fields A list of custom field names to use.
488
- * @param string $excerpts If 'on', include excerpts.
489
- * @param string $index_pdf_parent If 'on', include PDF parent.
490
- *
491
- * @global object $wpdb The WordPress database interface.
492
- *
493
- * @return array An array of queries sorted by phrase.
494
- */
495
- function relevanssi_generate_phrase_queries( $phrases, $taxonomies, $custom_fields, $excerpts, $index_pdf_parent ) {
496
- global $wpdb;
497
-
498
- $status = relevanssi_valid_status_array();
499
-
500
- // Add "inherit" to the list of allowed statuses to include attachments.
501
- if ( ! strstr( $status, 'inherit' ) ) {
502
- $status .= ",'inherit'";
503
- }
504
-
505
- $phrase_queries = array();
506
-
507
- foreach ( $phrases as $phrase ) {
508
- $queries = array();
509
- $phrase = $wpdb->esc_like( $phrase );
510
- $phrase = str_replace( array( '‘', '’', "'", '"', '”', '“', '“', '„', '´' ), '_', $phrase );
511
- $phrase = htmlspecialchars( $phrase );
512
- $phrase = esc_sql( $phrase );
513
-
514
- $excerpt = '';
515
- if ( 'on' === $excerpts ) {
516
- $excerpt = "OR post_excerpt LIKE '%$phrase%'";
517
- }
518
-
519
- $query = "(SELECT ID FROM $wpdb->posts
520
- WHERE (post_content LIKE '%$phrase%' OR post_title LIKE '%$phrase%' $excerpt)
521
- AND post_status IN ($status))";
522
-
523
- $queries[] = $query;
524
-
525
- if ( $taxonomies ) {
526
- $taxonomies_escaped = implode( "','", array_map( 'esc_sql', $taxonomies ) );
527
- $taxonomies_sql = "AND s.taxonomy IN ('$taxonomies_escaped')";
528
-
529
- $query = "(SELECT ID FROM $wpdb->posts as p, $wpdb->term_relationships as r, $wpdb->term_taxonomy as s, $wpdb->terms as t
530
- WHERE r.term_taxonomy_id = s.term_taxonomy_id AND s.term_id = t.term_id AND p.ID = r.object_id
531
- $taxonomies_sql
532
- AND t.name LIKE '%$phrase%' AND p.post_status IN ($status))";
533
-
534
- $queries[] = $query;
535
- }
536
-
537
- if ( $custom_fields ) {
538
- $keys = '';
539
-
540
- if ( is_array( $custom_fields ) ) {
541
- if ( ! in_array( '_relevanssi_pdf_content', $custom_fields, true ) ) {
542
- array_push( $custom_fields, '_relevanssi_pdf_content' );
543
- }
544
-
545
- if ( strpos( implode( ' ', $custom_fields ), '%' ) ) {
546
- // ACF repeater fields involved.
547
- $custom_fields_regexp = str_replace( '%', '.+', implode( '|', $custom_fields ) );
548
- $keys = "AND m.meta_key REGEXP ('$custom_fields_regexp')";
549
- } else {
550
- $custom_fields_escaped = implode(
551
- "','",
552
- array_map(
553
- 'esc_sql',
554
- $custom_fields
555
- )
556
- );
557
- $keys = "AND m.meta_key IN ('$custom_fields_escaped')";
558
- }
559
- }
560
-
561
- if ( 'visible' === $custom_fields ) {
562
- $keys = "AND (m.meta_key NOT LIKE '\_%' OR m.meta_key = '_relevanssi_pdf_content')";
563
- }
564
-
565
- $query = "(SELECT ID
566
- FROM $wpdb->posts AS p, $wpdb->postmeta AS m
567
- WHERE p.ID = m.post_id
568
- $keys
569
- AND m.meta_value LIKE '%$phrase%'
570
- AND p.post_status IN ($status))";
571
-
572
- $queries[] = $query;
573
- } elseif ( RELEVANSSI_PREMIUM ) {
574
- $index_post_types = get_option( 'relevanssi_index_post_types', array() );
575
- if ( in_array( 'attachment', $index_post_types, true ) ) {
576
- $query = "(SELECT ID
577
- FROM $wpdb->posts AS p, $wpdb->postmeta AS m
578
- WHERE p.ID = m.post_id
579
- AND m.meta_key = '_relevanssi_pdf_content'
580
- AND m.meta_value LIKE '%$phrase%'
581
- AND p.post_status IN ($status))";
582
-
583
- $queries[] = $query;
584
- }
585
- }
586
-
587
- if ( 'on' === $index_pdf_parent ) {
588
- $query = "(SELECT parent.ID
589
- FROM $wpdb->posts AS p, $wpdb->postmeta AS m, $wpdb->posts AS parent
590
- WHERE p.ID = m.post_id
591
- AND p.post_parent = parent.ID
592
- AND m.meta_key = '_relevanssi_pdf_content'
593
- AND m.meta_value LIKE '%$phrase%'
594
- AND p.post_status = 'inherit')";
595
-
596
- $queries[] = $query;
597
- }
598
-
599
- $phrase_queries[ $phrase ] = $queries;
600
- }
601
-
602
- return $phrase_queries;
603
- }
604
-
605
- /**
606
- * Strips invisible elements from text.
607
- *
608
- * Strips <style>, <script>, <object>, <embed>, <applet>, <noscript>, <noembed>,
609
- * <iframe>, and <del> tags and their contents from the text.
610
- *
611
- * @param string $text The source text.
612
- *
613
- * @return string The processed text.
614
- */
615
- function relevanssi_strip_invisibles( $text ) {
616
- $text = preg_replace(
617
- array(
618
- '@<style[^>]*?>.*?</style>@siu',
619
- '@<script[^>]*?.*?</script>@siu',
620
- '@<object[^>]*?.*?</object>@siu',
621
- '@<embed[^>]*?.*?</embed>@siu',
622
- '@<applet[^>]*?.*?</applet>@siu',
623
- '@<noscript[^>]*?.*?</noscript>@siu',
624
- '@<noembed[^>]*?.*?</noembed>@siu',
625
- '@<iframe[^>]*?.*?</iframe>@siu',
626
- '@<del[^>]*?.*?</del>@siu',
627
- ),
628
- ' ',
629
- $text
630
- );
631
- return $text;
632
- }
633
-
634
  /**
635
  * Returns the custom fields to index.
636
  *
@@ -659,36 +280,6 @@ function relevanssi_get_custom_fields() {
659
  return $custom_fields;
660
  }
661
 
662
- /**
663
- * Trims multibyte strings.
664
- *
665
- * Removes the 194+160 non-breakable spaces, removes null bytes and removes whitespace.
666
- *
667
- * @param string $string The source string.
668
- *
669
- * @return string Trimmed string.
670
- */
671
- function relevanssi_mb_trim( $string ) {
672
- $string = str_replace( chr( 194 ) . chr( 160 ), '', $string );
673
- $string = str_replace( "\0", '', $string );
674
- $string = preg_replace( '/(^\s+)|(\s+$)/us', '', $string );
675
- return $string;
676
- }
677
-
678
- /**
679
- * Wraps the relevanssi_mb_trim() function so that it can be used as a callback for
680
- * array_walk().
681
- *
682
- * @since 2.1.4
683
- *
684
- * @see relevanssi_mb_trim.
685
- *
686
- * @param string $string String to trim.
687
- */
688
- function relevanssi_array_walk_trim( &$string ) {
689
- $string = relevanssi_mb_trim( $string );
690
- }
691
-
692
  /**
693
  * Removes punctuation from a string.
694
  *
@@ -1154,72 +745,6 @@ function relevanssi_get_post_type( $post_id ) {
1154
  }
1155
  }
1156
 
1157
- /**
1158
- * Prints out a list of tags for post.
1159
- *
1160
- * Replacement for the_tags() that does the same, but applies Relevanssi search term
1161
- * highlighting on the results.
1162
- *
1163
- * @param string $before What is printed before the tags, default null.
1164
- * @param string $separator The separator between items, default ', '.
1165
- * @param string $after What is printed after the tags, default ''.
1166
- * @param boolean $echo If true, echo, otherwise return the result. Default true.
1167
- * @param int $post_id The post ID. Default current post ID (in the Loop).
1168
- */
1169
- function relevanssi_the_tags( $before = null, $separator = ', ', $after = '', $echo = true, $post_id = null ) {
1170
- $tag_list = get_the_tag_list( $before, $separator, $after, $post_id );
1171
- $found = preg_match_all( '~<a href=".*?" rel="tag">(.*?)</a>~', $tag_list, $matches );
1172
- if ( $found ) {
1173
- $originals = $matches[0];
1174
- $tag_names = $matches[1];
1175
- $highlighted = array();
1176
-
1177
- $count = count( $matches[0] );
1178
- for ( $i = 0; $i < $count; $i++ ) {
1179
- $highlighted_tag_name = relevanssi_highlight_terms( $tag_names[ $i ], get_search_query(), true );
1180
- $highlighted[ $i ] = str_replace( '>' . $tag_names[ $i ] . '<', '>' . $highlighted_tag_name . '<', $originals[ $i ] );
1181
- }
1182
-
1183
- $tag_list = str_replace( $originals, $highlighted, $tag_list );
1184
- }
1185
-
1186
- if ( $echo ) {
1187
- echo $tag_list; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1188
- } else {
1189
- return $tag_list;
1190
- }
1191
- }
1192
-
1193
- /**
1194
- * Gets a list of tags for post.
1195
- *
1196
- * Replacement for get_the_tags() that does the same, but applies Relevanssi search term
1197
- * highlighting on the results.
1198
- *
1199
- * @param string $before What is printed before the tags, default null.
1200
- * @param string $separator The separator between items, default ', '.
1201
- * @param string $after What is printed after the tags, default ''.
1202
- * @param int $post_id The post ID. Default current post ID (in the Loop).
1203
- */
1204
- function relevanssi_get_the_tags( $before = null, $separator = ', ', $after = '', $post_id = null ) {
1205
- return relevanssi_the_tags( $before, $separator, $after, false, $post_id );
1206
- }
1207
-
1208
- /**
1209
- * Returns the term taxonomy ID for a term based on term ID.
1210
- *
1211
- * @global object $wpdb The WordPress database interface.
1212
- *
1213
- * @param int $term_id The term ID.
1214
- * @param string $taxonomy The taxonomy.
1215
- *
1216
- * @return int Term taxonomy ID.
1217
- */
1218
- function relevanssi_get_term_tax_id( $term_id, $taxonomy ) {
1219
- global $wpdb;
1220
- return $wpdb->get_var( $wpdb->prepare( "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id = %d AND taxonomy = %s", $term_id, $taxonomy ) );
1221
- }
1222
-
1223
  /**
1224
  * Adds synonyms to a search query.
1225
  *
@@ -1234,11 +759,13 @@ function relevanssi_add_synonyms( $query ) {
1234
  return $query;
1235
  }
1236
 
1237
- $synonym_data = get_option( 'relevanssi_synonyms' );
1238
- if ( $synonym_data ) {
 
 
1239
  $synonyms = array();
1240
- $synonym_data = relevanssi_strtolower( $synonym_data );
1241
- $pairs = explode( ';', $synonym_data );
1242
 
1243
  foreach ( $pairs as $pair ) {
1244
  if ( empty( $pair ) ) {
@@ -1292,105 +819,6 @@ function relevanssi_add_synonyms( $query ) {
1292
  return $query;
1293
  }
1294
 
1295
- /**
1296
- * Returns the position of substring in the string.
1297
- *
1298
- * Uses mb_stripos() if possible, falls back to mb_strpos() and mb_strtoupper() if
1299
- * that cannot be found, and falls back to just strpos() if even that is not
1300
- * possible.
1301
- *
1302
- * @param string $haystack String where to look.
1303
- * @param string $needle The string to look for.
1304
- * @param int $offset Where to start, default 0.
1305
- *
1306
- * @return mixed False, if no result or $offset outside the length of $haystack,
1307
- * otherwise the position (which can be non-false 0!).
1308
- */
1309
- function relevanssi_stripos( $haystack, $needle, $offset = 0 ) {
1310
- if ( $offset > relevanssi_strlen( $haystack ) ) {
1311
- return false;
1312
- }
1313
-
1314
- if ( preg_match( '/[\?\*]/', $needle ) ) {
1315
- // There's a ? or an * in the string, which means it's a wildcard search
1316
- // query (a Premium feature) and requires some extra steps.
1317
-
1318
- $needle_regex = str_replace(
1319
- array( '?', '*' ),
1320
- array( '.', '.*' ),
1321
- $needle
1322
- );
1323
- $pos_found = false;
1324
- while ( ! $pos_found ) {
1325
- preg_match(
1326
- "/$needle_regex/i",
1327
- $haystack,
1328
- $matches,
1329
- PREG_OFFSET_CAPTURE,
1330
- $offset
1331
- );
1332
- /**
1333
- * This trickery is necessary, because PREG_OFFSET_CAPTURE gives
1334
- * wrong offsets for multibyte strings. The mb_strlen() gives the
1335
- * correct offset, the rest of this is because the $offset received
1336
- * as a parameter can be before the first $position, leading to an
1337
- * infinite loop.
1338
- */
1339
- $pos = isset( $matches[0][1] )
1340
- ? mb_strlen( substr( $haystack, 0, $matches[0][1] ) )
1341
- : false;
1342
- if ( $pos && $pos > $offset ) {
1343
- $pos_found = true;
1344
- } elseif ( $pos ) {
1345
- $offset++;
1346
- } else {
1347
- $pos_found = true;
1348
- }
1349
- }
1350
- } elseif ( function_exists( 'mb_stripos' ) ) {
1351
- if ( '' === $haystack ) {
1352
- $pos = false;
1353
- } else {
1354
- $pos = mb_stripos( $haystack, $needle, $offset );
1355
- }
1356
- } elseif ( function_exists( 'mb_strpos' ) && function_exists( 'mb_strtoupper' ) && function_exists( 'mb_substr' ) ) {
1357
- $pos = mb_strpos( mb_strtoupper( $haystack ), mb_strtoupper( $needle ), $offset );
1358
- } else {
1359
- $pos = strpos( strtoupper( $haystack ), strtoupper( $needle ), $offset );
1360
- }
1361
- return $pos;
1362
- }
1363
-
1364
- /**
1365
- * Closes tags in a bit of HTML code.
1366
- *
1367
- * Used to make sure no tags are left open in excerpts. This method is not foolproof,
1368
- * but it's good enough for now.
1369
- *
1370
- * @param string $html The HTML code to analyze.
1371
- *
1372
- * @return string The HTML code, with tags closed.
1373
- */
1374
- function relevanssi_close_tags( $html ) {
1375
- $result = array();
1376
- preg_match_all( '#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result );
1377
- $opened_tags = $result[1];
1378
- preg_match_all( '#</([a-z]+)>#iU', $html, $result );
1379
- $closed_tags = $result[1];
1380
- $len_opened = count( $opened_tags );
1381
- if ( count( $closed_tags ) === $len_opened ) {
1382
- return $html;
1383
- }
1384
- $opened_tags = array_reverse( $opened_tags );
1385
- for ( $i = 0; $i < $len_opened; $i++ ) {
1386
- if ( ! in_array( $opened_tags[ $i ], $closed_tags, true ) ) {
1387
- $html .= '</' . $opened_tags[ $i ] . '>';
1388
- } else {
1389
- unset( $closed_tags[ array_search( $opened_tags[ $i ], $closed_tags, true ) ] );
1390
- }
1391
- }
1392
- return $html;
1393
- }
1394
 
1395
  /**
1396
  * Prints out post title with highlighting.
@@ -1463,49 +891,6 @@ function relevanssi_async_update_doc_count() {
1463
  relevanssi_launch_ajax_action( 'relevanssi_update_counts' );
1464
  }
1465
 
1466
- /**
1467
- * Returns the length of the string.
1468
- *
1469
- * Uses mb_strlen() if available, otherwise falls back to strlen().
1470
- *
1471
- * @param string $s The string to measure.
1472
- *
1473
- * @return int The length of the string.
1474
- */
1475
- function relevanssi_strlen( $s ) {
1476
- if ( function_exists( 'mb_strlen' ) ) {
1477
- return mb_strlen( $s );
1478
- }
1479
- return strlen( $s );
1480
- }
1481
-
1482
- /**
1483
- * Prints out debugging notices.
1484
- *
1485
- * If WP_CLI is available, prints out the debug notice as a WP_CLI::log(), otherwise
1486
- * just echo.
1487
- *
1488
- * @param string $notice The notice to print out.
1489
- */
1490
- function relevanssi_debug_echo( $notice ) {
1491
- if ( defined( 'WP_CLI' ) && WP_CLI ) {
1492
- WP_CLI::log( $notice );
1493
- } else {
1494
- echo esc_html( $notice ) . "\n";
1495
- }
1496
- }
1497
-
1498
- /**
1499
- * Returns a Relevanssi_Taxonomy_Walker instance.
1500
- *
1501
- * Requires the class file and generates a new Relevanssi_Taxonomy_Walker instance.
1502
- *
1503
- * @return object A new Relevanssi_Taxonomy_Walker instance.
1504
- */
1505
- function get_relevanssi_taxonomy_walker() {
1506
- require_once 'class-relevanssi-taxonomy-walker.php';
1507
- return new Relevanssi_Taxonomy_Walker();
1508
- }
1509
 
1510
  /**
1511
  * Adjusts Relevanssi variables when switch_blog() happens.
@@ -1574,34 +959,6 @@ function relevanssi_add_highlight( $permalink, $link_post = null ) {
1574
  return $permalink;
1575
  }
1576
 
1577
- /**
1578
- * Gets the permalink to the current post within Loop.
1579
- *
1580
- * Uses get_permalink() to get the permalink, then adds the 'highlight' parameter
1581
- * if necessary using relevanssi_add_highlight().
1582
- *
1583
- * @return string The permalink.
1584
- */
1585
- function relevanssi_get_permalink() {
1586
- /**
1587
- * Filters the permalink.
1588
- *
1589
- * @param string The permalink, generated by get_permalink().
1590
- */
1591
- $permalink = apply_filters( 'relevanssi_permalink', get_permalink() );
1592
- return $permalink;
1593
- }
1594
-
1595
- /**
1596
- * Echoes out the permalink to the current post within Loop.
1597
- *
1598
- * Uses get_permalink() to get the permalink, then adds the 'highlight' parameter
1599
- * if necessary using relevanssi_add_highlight(), then echoes it out.
1600
- */
1601
- function relevanssi_the_permalink() {
1602
- echo esc_url( relevanssi_get_permalink() );
1603
- }
1604
-
1605
  /**
1606
  * Adjusts the permalink to use the Relevanssi-generated link.
1607
  *
@@ -1637,178 +994,6 @@ function relevanssi_permalink( $link, $link_post = null ) {
1637
  return $link;
1638
  }
1639
 
1640
- /**
1641
- * Generates the Did you mean suggestions.
1642
- *
1643
- * A wrapper function that prints out the Did you mean suggestions. If Premium is
1644
- * available, will use relevanssi_premium_didyoumean(), otherwise the
1645
- * relevanssi_simple_didyoumean() is used.
1646
- *
1647
- * @param string $query The query.
1648
- * @param string $pre Printed out before the suggestion.
1649
- * @param string $post Printed out after the suggestion.
1650
- * @param int $n Maximum number of search results found for the suggestions
1651
- * to show up. Default 5.
1652
- * @param boolean $echo If true, echo out. Default true.
1653
- *
1654
- * @return string The suggestion HTML element.
1655
- */
1656
- function relevanssi_didyoumean( $query, $pre, $post, $n = 5, $echo = true ) {
1657
- if ( function_exists( 'relevanssi_premium_didyoumean' ) ) {
1658
- $result = relevanssi_premium_didyoumean( $query, $pre, $post, $n );
1659
- } else {
1660
- $result = relevanssi_simple_didyoumean( $query, $pre, $post, $n );
1661
- }
1662
-
1663
- if ( $echo ) {
1664
- echo $result; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1665
- }
1666
-
1667
- return $result;
1668
- }
1669
-
1670
- /**
1671
- * Generates the Did you mean suggestions HTML code.
1672
- *
1673
- * Uses relevanssi_simple_generate_suggestion() to come up with a suggestion, then
1674
- * wraps that up with HTML code.
1675
- *
1676
- * @global object $wpdb The WordPress database interface.
1677
- * @global array $relevanssi_variables The Relevanssi global variables.
1678
- * @global object $wp_query The WP_Query object.
1679
- *
1680
- * @param string $query The query.
1681
- * @param string $pre Printed out before the suggestion.
1682
- * @param string $post Printed out after the suggestion.
1683
- * @param int $n Maximum number of search results found for the suggestions
1684
- * to show up. Default 5.
1685
- *
1686
- * @return string The suggestion HTML code, null if nothing found.
1687
- */
1688
- function relevanssi_simple_didyoumean( $query, $pre, $post, $n = 5 ) {
1689
- global $wp_query;
1690
-
1691
- $total_results = $wp_query->found_posts;
1692
-
1693
- if ( $total_results > $n ) {
1694
- return null;
1695
- }
1696
-
1697
- $suggestion = relevanssi_simple_generate_suggestion( $query );
1698
-
1699
- $result = null;
1700
- if ( $suggestion ) {
1701
- $url = get_bloginfo( 'url' );
1702
- $url = esc_attr( add_query_arg( array( 's' => rawurlencode( $suggestion ) ), $url ) );
1703
-
1704
- /**
1705
- * Filters the 'Did you mean' suggestion URL.
1706
- *
1707
- * @param string $url The URL for the suggested search query.
1708
- * @param string $query The search query.
1709
- * @param string $suggestion The suggestion.
1710
- */
1711
- $url = apply_filters( 'relevanssi_didyoumean_url', $url, $query, $suggestion );
1712
-
1713
- // Escape the suggestion to avoid XSS attacks.
1714
- $suggestion = htmlspecialchars( $suggestion );
1715
-
1716
- /**
1717
- * Filters the complete 'Did you mean' suggestion.
1718
- *
1719
- * @param string The suggestion HTML code.
1720
- */
1721
- $result = apply_filters( 'relevanssi_didyoumean_suggestion', "$pre<a href='$url'>$suggestion</a>$post" );
1722
- }
1723
-
1724
- return $result;
1725
- }
1726
-
1727
- /**
1728
- * Generates the 'Did you mean' suggestions. Can be used to correct any queries.
1729
- *
1730
- * Uses the Relevanssi search logs as source material for corrections. If there are
1731
- * no logged search queries, can't do anything.
1732
- *
1733
- * @global object $wpdb The WordPress database interface.
1734
- * @global array $relevanssi_variables The Relevanssi global variables, used for
1735
- * table names.
1736
- *
1737
- * @param string $query The query to correct.
1738
- *
1739
- * @return string Corrected query, empty if nothing found.
1740
- */
1741
- function relevanssi_simple_generate_suggestion( $query ) {
1742
- global $wpdb, $relevanssi_variables;
1743
-
1744
- /**
1745
- * The minimum limit of occurrances to include a word.
1746
- *
1747
- * To save resources, only words with more than this many occurrances are fed for
1748
- * the spelling corrector. If there are problems with the spelling corrector,
1749
- * increasing this value may fix those problems.
1750
- *
1751
- * @param int $number The number of occurrances must be more than this value,
1752
- * default 2.
1753
- */
1754
- $count = apply_filters( 'relevanssi_get_words_having', 2 );
1755
- if ( ! is_numeric( $count ) ) {
1756
- $count = 2;
1757
- }
1758
- $q = 'SELECT query, count(query) as c, AVG(hits) as a FROM ' . $relevanssi_variables['log_table'] . ' WHERE hits > ' . $count . ' GROUP BY query ORDER BY count(query) DESC';
1759
- $q = apply_filters( 'relevanssi_didyoumean_query', $q );
1760
-
1761
- $data = get_transient( 'relevanssi_didyoumean_query' );
1762
- if ( empty( $data ) ) {
1763
- $data = $wpdb->get_results( $q ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1764
- set_transient( 'relevanssi_didyoumean_query', $data, MONTH_IN_SECONDS );
1765
- }
1766
-
1767
- $query = htmlspecialchars_decode( $query, ENT_QUOTES );
1768
- $tokens = relevanssi_tokenize( $query );
1769
- $suggestions_made = false;
1770
- $suggestion = '';
1771
-
1772
- foreach ( $tokens as $token => $count ) {
1773
- $closest = '';
1774
- $distance = -1;
1775
- foreach ( $data as $row ) {
1776
- if ( $row->c < 2 ) {
1777
- break;
1778
- }
1779
-
1780
- if ( $token === $row->query ) {
1781
- $closest = '';
1782
- break;
1783
- } else {
1784
- if ( relevanssi_strlen( $token ) < 255 ) {
1785
- // The levenshtein() function has a max length of 255 characters.
1786
- $lev = levenshtein( $token, $row->query );
1787
- if ( $lev < 3 && ( $lev < $distance || $distance < 0 ) ) {
1788
- if ( $row->a > 0 ) {
1789
- $distance = $lev;
1790
- $closest = $row->query;
1791
- if ( $lev < 2 ) {
1792
- break; // get the first with distance of 1 and go.
1793
- }
1794
- }
1795
- }
1796
- }
1797
- }
1798
- }
1799
- if ( ! empty( $closest ) ) {
1800
- $query = str_ireplace( $token, $closest, $query );
1801
- $suggestions_made = true;
1802
- }
1803
- }
1804
-
1805
- if ( $suggestions_made ) {
1806
- $suggestion = $query;
1807
- }
1808
-
1809
- return $suggestion;
1810
- }
1811
-
1812
  /**
1813
  * Instructs a multisite installation to drop the tables.
1814
  *
@@ -1830,77 +1015,6 @@ function relevanssi_wpmu_drop( $tables ) {
1830
  return $tables;
1831
  }
1832
 
1833
- /**
1834
- * Replacement for get_post() that uses the Relevanssi post cache.
1835
- *
1836
- * Tries to fetch the post from the Relevanssi post cache. If that doesn't work, gets
1837
- * the post using get_post().
1838
- *
1839
- * @param int $post_id The post ID.
1840
- * @param int $blog_id The blog ID, default -1.
1841
- *
1842
- * @return object The post object.
1843
- */
1844
- function relevanssi_get_post( $post_id, $blog_id = -1 ) {
1845
- if ( function_exists( 'relevanssi_premium_get_post' ) ) {
1846
- return relevanssi_premium_get_post( $post_id, $blog_id );
1847
- }
1848
-
1849
- global $relevanssi_post_array;
1850
-
1851
- $post = null;
1852
- if ( isset( $relevanssi_post_array[ $post_id ] ) ) {
1853
- $post = $relevanssi_post_array[ $post_id ];
1854
- }
1855
- if ( ! $post ) {
1856
- $post = get_post( $post_id );
1857
-
1858
- $relevanssi_post_array[ $post_id ] = $post;
1859
- }
1860
- return $post;
1861
- }
1862
-
1863
- /**
1864
- * Recursively flattens a multidimensional array to produce a string.
1865
- *
1866
- * @param array $array The source array.
1867
- *
1868
- * @return string The array contents as a string.
1869
- */
1870
- function relevanssi_flatten_array( array $array ) {
1871
- $return_value = '';
1872
- foreach ( new RecursiveIteratorIterator( new RecursiveArrayIterator( $array ) ) as $value ) {
1873
- $return_value .= ' ' . $value;
1874
- }
1875
- return trim( $return_value );
1876
- }
1877
-
1878
- /**
1879
- * Sanitizes hex color strings.
1880
- *
1881
- * A copy of sanitize_hex_color(), because that isn't always available.
1882
- *
1883
- * @param string $color A hex color string to sanitize.
1884
- *
1885
- * @return string Sanitized hex string, or an empty string.
1886
- */
1887
- function relevanssi_sanitize_hex_color( $color ) {
1888
- if ( '' === $color ) {
1889
- return '';
1890
- }
1891
-
1892
- if ( '#' !== substr( $color, 0, 1 ) ) {
1893
- $color = '#' . $color;
1894
- }
1895
-
1896
- // 3 or 6 hex digits, or the empty string.
1897
- if ( preg_match( '|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) ) {
1898
- return $color;
1899
- }
1900
-
1901
- return '';
1902
- }
1903
-
1904
  /**
1905
  * Displays the list of most common words in the index.
1906
  *
@@ -2009,7 +1123,7 @@ function relevanssi_get_forbidden_post_types() {
2009
  'bigcommerce_task', // BigCommerce.
2010
  'slides', // Qoda slides.
2011
  'carousels', // Qoda carousels.
2012
-
2013
  );
2014
  }
2015
 
@@ -2031,17 +1145,6 @@ function relevanssi_get_forbidden_taxonomies() {
2031
  );
2032
  }
2033
 
2034
- /**
2035
- * Returns "off".
2036
- *
2037
- * Useful for returning "off" to filters easily.
2038
- *
2039
- * @return string A string with value "off".
2040
- */
2041
- function relevanssi_return_off() {
2042
- return 'off';
2043
- }
2044
-
2045
  /**
2046
  * Filters out unwanted custom fields.
2047
  *
@@ -2065,7 +1168,6 @@ function relevanssi_filter_custom_fields( $values, $field ) {
2065
  return $values;
2066
  }
2067
 
2068
-
2069
  /**
2070
  * Removes page builder short codes from content.
2071
  *
@@ -2221,46 +1323,6 @@ EOH;
2221
  return $notice;
2222
  }
2223
 
2224
- /**
2225
- * Launches an asynchronous Ajax action.
2226
- *
2227
- * Makes a wp_remote_post() call with the specific action. Handles nonce
2228
- * verification.
2229
- *
2230
- * @see wp_remove_post()
2231
- * @see wp_create_nonce()
2232
- *
2233
- * @param string $action The action to trigger (also the name of the
2234
- * nonce).
2235
- * @param array $payload_args The parameters sent to the action. Defaults to
2236
- * an empty array.
2237
- *
2238
- * @return WP_Error|array The wp_remote_post() response or WP_Error on failure.
2239
- */
2240
- function relevanssi_launch_ajax_action( $action, $payload_args = array() ) {
2241
- $cookies = array();
2242
- foreach ( $_COOKIE as $name => $value ) {
2243
- $cookies[] = "$name=" . rawurlencode(
2244
- is_array( $value ) ? wp_json_encode( $value ) : $value
2245
- );
2246
- }
2247
- $default_payload = array(
2248
- 'action' => $action,
2249
- '_nonce' => wp_create_nonce( $action ),
2250
- );
2251
- $payload = array_merge( $default_payload, $payload_args );
2252
- $args = array(
2253
- 'timeout' => 0.01,
2254
- 'blocking' => false,
2255
- 'body' => $payload,
2256
- 'headers' => array(
2257
- 'cookie' => implode( '; ', $cookies ),
2258
- ),
2259
- );
2260
- $url = admin_url( 'admin-ajax.php' );
2261
- return wp_remote_post( $url, $args );
2262
- }
2263
-
2264
  /**
2265
  * Fetches the data and generates the HTML for the "How Relevanssi sees this
2266
  * post".
@@ -2431,61 +1493,6 @@ function relevanssi_fetch_sees_data( $post_id ) {
2431
  );
2432
  }
2433
 
2434
- /**
2435
- * Removes quotes from array keys. Does not keep array values.
2436
- *
2437
- * Used to remove phrase quotes from search term array, which have the format
2438
- * of (term => hits). The number of hits is not needed, so this function
2439
- * discards it as a side effect.
2440
- *
2441
- * @param array $array An array to process.
2442
- *
2443
- * @return array The same array with quotes removed from the keys.
2444
- */
2445
- function relevanssi_remove_quotes_from_array_keys( $array ) {
2446
- $array = array_keys( $array );
2447
- array_walk(
2448
- $array,
2449
- function( &$key ) {
2450
- $key = relevanssi_remove_quotes( $key );
2451
- }
2452
- );
2453
- return array_flip( $array );
2454
- }
2455
-
2456
- /**
2457
- * Removes quotes (", ”, “) from a string.
2458
- *
2459
- * @param string $string The string to clean.
2460
- *
2461
- * @return string The cleaned string.
2462
- */
2463
- function relevanssi_remove_quotes( $string ) {
2464
- return str_replace( array( '”', '“', '"' ), '', $string );
2465
- }
2466
-
2467
- /**
2468
- * Strips tags from contents, keeping the allowed tags.
2469
- *
2470
- * The allowable tags are read from the relevanssi_excerpt_allowable_tags
2471
- * option. Spaces are added between tags before removing the tags, so that
2472
- * words don't get stuck together. The function also remove invisible content.
2473
- *
2474
- * @see relevanssi_strip_invisibles
2475
- *
2476
- * @param string $content The content.
2477
- *
2478
- * @return string The content without tags.
2479
- */
2480
- function relevanssi_strip_tags( $content ) {
2481
- $content = relevanssi_strip_invisibles( $content );
2482
- $content = preg_replace( '/(<\/[^>]+?>)(<[^>\/][^>]*?>)/', '$1 $2', $content );
2483
- return strip_tags(
2484
- $content,
2485
- get_option( 'relevanssi_excerpt_allowable_tags', '' )
2486
- );
2487
- }
2488
-
2489
  /**
2490
  * Generates a list of custom fields for a post.
2491
  *
@@ -2544,3 +1551,16 @@ function relevanssi_generate_list_of_custom_fields( $post_id, $custom_fields = n
2544
 
2545
  return $custom_fields;
2546
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  * @see https://www.relevanssi.com/
9
  */
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  /**
12
  * Adds the search result match breakdown to the post object.
13
  *
252
  wp_suspend_cache_addition( false );
253
  }
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  /**
256
  * Returns the custom fields to index.
257
  *
280
  return $custom_fields;
281
  }
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  /**
284
  * Removes punctuation from a string.
285
  *
745
  }
746
  }
747
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  /**
749
  * Adds synonyms to a search query.
750
  *
759
  return $query;
760
  }
761
 
762
+ $current_language = relevanssi_get_current_language();
763
+ $synonym_data = get_option( 'relevanssi_synonyms', array() );
764
+ $synonym_list = isset( $synonym_data[ $current_language ] ) ? $synonym_data[ $current_language ] : '';
765
+ if ( $synonym_list ) {
766
  $synonyms = array();
767
+ $synonym_list = relevanssi_strtolower( $synonym_list );
768
+ $pairs = explode( ';', $synonym_list );
769
 
770
  foreach ( $pairs as $pair ) {
771
  if ( empty( $pair ) ) {
819
  return $query;
820
  }
821
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
 
823
  /**
824
  * Prints out post title with highlighting.
891
  relevanssi_launch_ajax_action( 'relevanssi_update_counts' );
892
  }
893
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
 
895
  /**
896
  * Adjusts Relevanssi variables when switch_blog() happens.
959
  return $permalink;
960
  }
961
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962
  /**
963
  * Adjusts the permalink to use the Relevanssi-generated link.
964
  *
994
  return $link;
995
  }
996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
997
  /**
998
  * Instructs a multisite installation to drop the tables.
999
  *
1015
  return $tables;
1016
  }
1017
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1018
  /**
1019
  * Displays the list of most common words in the index.
1020
  *
1123
  'bigcommerce_task', // BigCommerce.
1124
  'slides', // Qoda slides.
1125
  'carousels', // Qoda carousels.
1126
+ 'pretty-link', // Pretty Links.
1127
  );
1128
  }
1129
 
1145
  );
1146
  }
1147
 
 
 
 
 
 
 
 
 
 
 
 
1148
  /**
1149
  * Filters out unwanted custom fields.
1150
  *
1168
  return $values;
1169
  }
1170
 
 
1171
  /**
1172
  * Removes page builder short codes from content.
1173
  *
1323
  return $notice;
1324
  }
1325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1326
  /**
1327
  * Fetches the data and generates the HTML for the "How Relevanssi sees this
1328
  * post".
1493
  );
1494
  }
1495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1496
  /**
1497
  * Generates a list of custom fields for a post.
1498
  *
1551
 
1552
  return $custom_fields;
1553
  }
1554
+
1555
+ /**
1556
+ * Updates the relevanssi_synonyms setting from a simple string to an array
1557
+ * that is required for multilingual synonyms.
1558
+ */
1559
+ function relevanssi_update_synonyms_setting() {
1560
+ $synonyms = get_option( 'relevanssi_synonyms' );
1561
+
1562
+ $current_language = relevanssi_get_current_language();
1563
+
1564
+ $array_synonyms[ $current_language ] = $synonyms;
1565
+ update_option( 'relevanssi_synonyms', $array_synonyms );
1566
+ }
lib/compatibility/oxygen.php CHANGED
@@ -12,7 +12,7 @@
12
 
13
  add_filter( 'relevanssi_custom_field_value', 'relevanssi_oxygen_compatibility', 10, 3 );
14
  add_filter( 'relevanssi_index_custom_fields', 'relevanssi_add_oxygen' );
15
-
16
  /**
17
  * Cleans up the Oxygen Builder custom field for Relevanssi consumption.
18
  *
@@ -120,3 +120,17 @@ function relevanssi_add_oxygen( $fields ) {
120
  return $fields;
121
  }
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  add_filter( 'relevanssi_custom_field_value', 'relevanssi_oxygen_compatibility', 10, 3 );
14
  add_filter( 'relevanssi_index_custom_fields', 'relevanssi_add_oxygen' );
15
+ add_filter( 'pre_option_relevanssi_index_fields', 'relevanssi_oxygen_fix_none_setting' );
16
  /**
17
  * Cleans up the Oxygen Builder custom field for Relevanssi consumption.
18
  *
120
  return $fields;
121
  }
122
 
123
+ /**
124
+ * Makes sure the Oxygen builder shortcode is included in the index, even when
125
+ * the custom field setting is set to 'none'.
126
+ *
127
+ * @param string $value The custom field indexing setting value.
128
+ *
129
+ * @return string If value is undefined, it's set to 'ct_builder_shortcodes'.
130
+ */
131
+ function relevanssi_oxygen_fix_none_setting( $value ) {
132
+ if ( ! $value ) {
133
+ $value = 'ct_builder_shortcodes';
134
+ }
135
+ return $value;
136
+ }
lib/compatibility/polylang.php CHANGED
@@ -171,3 +171,35 @@ function relevanssi_polylang_term_filter( $hits ) {
171
  }
172
  return $hits;
173
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  }
172
  return $hits;
173
  }
174
+
175
+ /**
176
+ * Returns the term_taxonomy_id matching the Polylang language based on locale.
177
+ *
178
+ * @param string $locale The locale string for the language.
179
+ *
180
+ * @return int The term_taxonomy_id for the language; 0 if nothing is found.
181
+ */
182
+ function relevanssi_get_language_term_taxonomy_id( $locale ) {
183
+ global $wpdb, $relevanssi_language_term_ids;
184
+
185
+ if ( isset( $relevanssi_language_term_ids[ $locale ] ) ) {
186
+ return $relevanssi_language_term_ids[ $locale ];
187
+ }
188
+
189
+ $languages = $wpdb->get_results(
190
+ "SELECT term_taxonomy_id, description FROM $wpdb->term_taxonomy " .
191
+ "WHERE taxonomy = 'language'"
192
+ );
193
+ $term_id = 0;
194
+ foreach ( $languages as $row ) {
195
+ $description = unserialize( $row->description ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
196
+ if ( $description['locale'] === $locale ) {
197
+ $term_id = $row->term_taxonomy_id;
198
+ break;
199
+ }
200
+ }
201
+
202
+ $relevanssi_language_term_ids[ $locale ] = $term_id;
203
+
204
+ return $term_id;
205
+ }
lib/compatibility/rankmath.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * /lib/compatibility/rankmath.php
4
+ *
5
+ * Rank Math noindex filtering function.
6
+ *
7
+ * @package Relevanssi
8
+ * @license https://wordpress.org/about/gpl/ GNU General Public License
9
+ * @see https://www.relevanssi.com/
10
+ */
11
+
12
+ add_filter( 'relevanssi_do_not_index', 'relevanssi_rankmath_noindex', 10, 2 );
13
+ add_filter( 'relevanssi_indexing_restriction', 'relevanssi_rankmath_exclude' );
14
+
15
+ /**
16
+ * Blocks indexing of posts marked "noindex" in the Rank Math settings.
17
+ *
18
+ * Attaches to the 'relevanssi_do_not_index' filter hook.
19
+ *
20
+ * @param boolean $do_not_index True, if the post shouldn't be indexed.
21
+ * @param integer $post_id The post ID number.
22
+ *
23
+ * @return string|boolean If the post shouldn't be indexed, this returns
24
+ * 'RankMath'. The value may also be a boolean.
25
+ */
26
+ function relevanssi_rankmath_noindex( $do_not_index, $post_id ) {
27
+ $noindex = get_post_meta( $post_id, 'rank_math_robots', true );
28
+ if ( in_array( 'noindex', $noindex, true ) ) {
29
+ $do_not_index = 'RankMath';
30
+ }
31
+ return $do_not_index;
32
+ }
33
+
34
+ /**
35
+ * Excludes the "noindex" posts from Relevanssi indexing.
36
+ *
37
+ * Adds a MySQL query restriction that blocks posts that have the Rank Math
38
+ * "rank_math_robots" setting set to something that includes "noindex".
39
+ *
40
+ * @param array $restriction An array with two values: 'mysql' for the MySQL
41
+ * query restriction to modify, 'reason' for the reason of restriction.
42
+ */
43
+ function relevanssi_rankmath_exclude( $restriction ) {
44
+ global $wpdb;
45
+
46
+ // Backwards compatibility code for 2.8.0, remove at some point.
47
+ if ( is_string( $restriction ) ) {
48
+ $restriction = array(
49
+ 'mysql' => $restriction,
50
+ 'reason' => '',
51
+ );
52
+ }
53
+
54
+ $restriction['mysql'] .= " AND post.ID NOT IN (SELECT post_id FROM
55
+ $wpdb->postmeta WHERE meta_key = 'rank_math_robots'
56
+ AND meta_value LIKE '%noindex%' ) ";
57
+ $restriction['reason'] .= ' Rank Math';
58
+ return $restriction;
59
+ }
lib/didyoumean.php ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * /lib/didyoumean.php
4
+ *
5
+ * @package Relevanssi
6
+ * @author Mikko Saari
7
+ * @license https://wordpress.org/about/gpl/ GNU General Public License
8
+ * @see https://www.relevanssi.com/
9
+ */
10
+
11
+ /**
12
+ * Generates the Did you mean suggestions.
13
+ *
14
+ * A wrapper function that prints out the Did you mean suggestions. If Premium
15
+ * is available, will use relevanssi_premium_didyoumean(), otherwise the
16
+ * relevanssi_simple_didyoumean() is used.
17
+ *
18
+ * @param string $query The query.
19
+ * @param string $pre Printed out before the suggestion.
20
+ * @param string $post Printed out after the suggestion.
21
+ * @param int $n Maximum number of search results found for the
22
+ * suggestions to show up. Default 5.
23
+ * @param boolean $echo If true, echo out. Default true.
24
+ *
25
+ * @return string|null The suggestion HTML element.
26
+ */
27
+ function relevanssi_didyoumean( $query, $pre, $post, $n = 5, $echo = true ) {
28
+ if ( function_exists( 'relevanssi_premium_didyoumean' ) ) {
29
+ $result = relevanssi_premium_didyoumean( $query, $pre, $post, $n );
30
+ } else {
31
+ $result = relevanssi_simple_didyoumean( $query, $pre, $post, $n );
32
+ }
33
+
34
+ if ( $echo ) {
35
+ echo $result; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
36
+ }
37
+
38
+ return $result;
39
+ }
40
+
41
+ /**
42
+ * Generates the Did you mean suggestions HTML code.
43
+ *
44
+ * Uses relevanssi_simple_generate_suggestion() to come up with a suggestion,
45
+ * then wraps that up with HTML code.
46
+ *
47
+ * @global object $wpdb The WordPress database interface.
48
+ * @global array $relevanssi_variables The Relevanssi global variables.
49
+ * @global object $wp_query The WP_Query object.
50
+ *
51
+ * @param string $query The query.
52
+ * @param string $pre Printed out before the suggestion.
53
+ * @param string $post Printed out after the suggestion.
54
+ * @param int $n Maximum number of search results found for the
55
+ * suggestions to show up. Default 5.
56
+ *
57
+ * @return string|null The suggestion HTML code, null if nothing found.
58
+ */
59
+ function relevanssi_simple_didyoumean( $query, $pre, $post, $n = 5 ) {
60
+ global $wp_query;
61
+
62
+ $total_results = $wp_query->found_posts;
63
+
64
+ if ( $total_results > $n ) {
65
+ return null;
66
+ }
67
+
68
+ $suggestion = relevanssi_simple_generate_suggestion( $query );
69
+
70
+ $result = null;
71
+ if ( $suggestion ) {
72
+ $url = get_bloginfo( 'url' );
73
+ $url = esc_attr(
74
+ add_query_arg(
75
+ array( 's' => rawurlencode( $suggestion ) ),
76
+ $url
77
+ )
78
+ );
79
+
80
+ /**
81
+ * Filters the 'Did you mean' suggestion URL.
82
+ *
83
+ * @param string $url The URL for the suggested search query.
84
+ * @param string $query The search query.
85
+ * @param string $suggestion The suggestion.
86
+ */
87
+ $url = apply_filters(
88
+ 'relevanssi_didyoumean_url',
89
+ $url,
90
+ $query,
91
+ $suggestion
92
+ );
93
+
94
+ // Escape the suggestion to avoid XSS attacks.
95
+ $suggestion = htmlspecialchars( $suggestion );
96
+
97
+ /**
98
+ * Filters the complete 'Did you mean' suggestion.
99
+ *
100
+ * @param string The suggestion HTML code.
101
+ */
102
+ $result = apply_filters(
103
+ 'relevanssi_didyoumean_suggestion',
104
+ "$pre<a href='$url'>$suggestion</a>$post"
105
+ );
106
+ }
107
+
108
+ return $result;
109
+ }
110
+
111
+ /**
112
+ * Generates the 'Did you mean' suggestions. Can be used to correct any queries.
113
+ *
114
+ * Uses the Relevanssi search logs as source material for corrections. If there
115
+ * are no logged search queries, can't do anything.
116
+ *
117
+ * @global object $wpdb The WordPress database interface.
118
+ * @global array $relevanssi_variables The Relevanssi global variables, used
119
+ * for table names.
120
+ *
121
+ * @param string $query The query to correct.
122
+ *
123
+ * @return string Corrected query, empty if nothing found.
124
+ */
125
+ function relevanssi_simple_generate_suggestion( $query ) {
126
+ global $wpdb, $relevanssi_variables;
127
+
128
+ /**
129
+ * The minimum limit of occurrances to include a word.
130
+ *
131
+ * To save resources, only words with more than this many occurrances are
132
+ * fed for the spelling corrector. If there are problems with the spelling
133
+ * corrector, increasing this value may fix those problems.
134
+ *
135
+ * @param int $number The number of occurrances must be more than this
136
+ * value, default 2.
137
+ */
138
+ $count = apply_filters( 'relevanssi_get_words_having', 2 );
139
+ if ( ! is_numeric( $count ) ) {
140
+ $count = 2;
141
+ }
142
+ $q = 'SELECT query, count(query) as c, AVG(hits) as a FROM '
143
+ . $relevanssi_variables['log_table'] . ' WHERE hits > ' . $count
144
+ . ' GROUP BY query ORDER BY count(query) DESC';
145
+ $q = apply_filters( 'relevanssi_didyoumean_query', $q );
146
+
147
+ $data = get_transient( 'relevanssi_didyoumean_query' );
148
+ if ( empty( $data ) ) {
149
+ $data = $wpdb->get_results( $q ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
150
+ set_transient( 'relevanssi_didyoumean_query', $data, MONTH_IN_SECONDS );
151
+ }
152
+
153
+ $query = htmlspecialchars_decode( $query, ENT_QUOTES );
154
+ $tokens = relevanssi_tokenize( $query );
155
+ $suggestions_made = false;
156
+ $suggestion = '';
157
+
158
+ foreach ( $tokens as $token => $count ) {
159
+ $closest = '';
160
+ $distance = -1;
161
+ foreach ( $data as $row ) {
162
+ if ( $row->c < 2 ) {
163
+ break;
164
+ }
165
+
166
+ if ( $token === $row->query ) {
167
+ $closest = '';
168
+ break;
169
+ } else {
170
+ if ( relevanssi_strlen( $token ) < 255 ) {
171
+ // The levenshtein() function has a max length of 255 characters.
172
+ $lev = levenshtein( $token, $row->query );
173
+ if ( $lev < 3 && ( $lev < $distance || $distance < 0 ) ) {
174
+ if ( $row->a > 0 ) {
175
+ $distance = $lev;
176
+ $closest = $row->query;
177
+ if ( $lev < 2 ) {
178
+ break; // get the first with distance of 1 and go.
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ if ( ! empty( $closest ) ) {
186
+ $query = str_ireplace( $token, $closest, $query );
187
+ $suggestions_made = true;
188
+ }
189
+ }
190
+
191
+ if ( $suggestions_made ) {
192
+ $suggestion = $query;
193
+ }
194
+
195
+ return $suggestion;
196
+ }
lib/excerpts-highlights.php CHANGED
@@ -8,23 +8,6 @@
8
  * @see https://www.relevanssi.com/
9
  */
10
 
11
- /**
12
- * Prints out the post excerpt.
13
- *
14
- * Prints out the post excerpt from $post->post_excerpt, unless the post is
15
- * protected. Only works in the Loop.
16
- *
17
- * @global $post The global post object.
18
- */
19
- function relevanssi_the_excerpt() {
20
- global $post;
21
- if ( ! post_password_required( $post ) ) {
22
- echo '<p>' . $post->post_excerpt . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
23
- } else {
24
- esc_html_e( 'There is no excerpt because this is a protected post.', 'relevanssi' );
25
- }
26
- }
27
-
28
  /**
29
  * Generates an excerpt for a post.
30
  *
@@ -476,14 +459,15 @@ function relevanssi_highlight_in_docs( $content ) {
476
  * you want to override the settings, 'pre_option_relevanssi_highlight' filter
477
  * hook is your friend).
478
  *
479
- * @param string $content The content to highlight.
480
- * @param string|array $query The search query (should be a string, can
481
- * sometimes be an array).
482
- * @param boolean $in_docs Are we highlighting post content? Default false.
 
483
  *
484
  * @return string The $content with highlighting.
485
  */
486
- function relevanssi_highlight_terms( $content, $query, $in_docs = false ) {
487
  $type = get_option( 'relevanssi_highlight' );
488
  if ( 'none' === $type ) {
489
  return $content;
@@ -707,7 +691,7 @@ function relevanssi_highlight_terms( $content, $query, $in_docs = false ) {
707
  }
708
 
709
  $content = relevanssi_remove_nested_highlights( $content, $start_emp_token, $end_emp_token );
710
- $content = relevanssi_fix_entities( $content, $in_docs );
711
 
712
  /**
713
  * Allows cleaning unwanted highlights.
@@ -881,25 +865,6 @@ function relevanssi_entities_inside( $content, $tag ) {
881
  return $content;
882
  }
883
 
884
- /**
885
- * Generates closing tags for an array of tags.
886
- *
887
- * @param array $tags Array of tag names.
888
- *
889
- * @return array $closing_tags Array of closing tags.
890
- */
891
- function relevanssi_generate_closing_tags( $tags ) {
892
- $closing_tags = array();
893
- foreach ( $tags as $tag ) {
894
- $a = str_replace( '<', '</', $tag );
895
- $b = str_replace( '>', '/>', $tag );
896
-
897
- $closing_tags[] = $a;
898
- $closing_tags[] = $b;
899
- }
900
- return $closing_tags;
901
- }
902
-
903
  /**
904
  * Removes nested highlights from a string.
905
  *
@@ -1248,8 +1213,8 @@ function relevanssi_add_accent_variations( $word ) {
1248
  array(
1249
  'from' => array( 'a', 'c', 'e', 'i', 'o', 'u', 'n' ),
1250
  'to' => array( '(?:a|á|à|â)', '(?:c|ç)', '(?:e|é|è|ê|ë)', '(?:i|í|ì|î|ï)', '(?:o|ó|ò|ô|õ)', '(?:u|ú|ù|ü|û)', '(?:n|ñ)' ),
1251
- 'from_re' => array( "/(s)('|’)?$/", "/[^\(\|]('|’)/" ),
1252
- 'to_re' => array( "(('|’)?\\1|\\1('|’)?)", "?('|’)?" ),
1253
  )
1254
  );
1255
 
8
  * @see https://www.relevanssi.com/
9
  */
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  /**
12
  * Generates an excerpt for a post.
13
  *
459
  * you want to override the settings, 'pre_option_relevanssi_highlight' filter
460
  * hook is your friend).
461
  *
462
+ * @param string $content The content to highlight.
463
+ * @param string|array $query The search query (should be a string,
464
+ * can also be an array of string).
465
+ * @param boolean $convert_entities Are we highlighting post content?
466
+ * Default false.
467
  *
468
  * @return string The $content with highlighting.
469
  */
470
+ function relevanssi_highlight_terms( $content, $query, $convert_entities = false ) {
471
  $type = get_option( 'relevanssi_highlight' );
472
  if ( 'none' === $type ) {
473
  return $content;
691
  }
692
 
693
  $content = relevanssi_remove_nested_highlights( $content, $start_emp_token, $end_emp_token );
694
+ $content = relevanssi_fix_entities( $content, $convert_entities );
695
 
696
  /**
697
  * Allows cleaning unwanted highlights.
865
  return $content;
866
  }
867
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  /**
869
  * Removes nested highlights from a string.
870
  *
1213
  array(
1214
  'from' => array( 'a', 'c', 'e', 'i', 'o', 'u', 'n' ),
1215
  'to' => array( '(?:a|á|à|â)', '(?:c|ç)', '(?:e|é|è|ê|ë)', '(?:i|í|ì|î|ï)', '(?:o|ó|ò|ô|õ)', '(?:u|ú|ù|ü|û)', '(?:n|ñ)' ),
1216
+ 'from_re' => array( "/(s)('|’)?$/", "/[^\(\|:]('|’)/" ),
1217
+ 'to_re' => array( "(?:(?:'|’)?\\1|\\1(?:'|’)?)", "?('|’)?" ),
1218
  )
1219
  );
1220
 
lib/indexing.php CHANGED
@@ -356,9 +356,6 @@ function relevanssi_build_index( $extend_offset = false, $verbose = null, $post_
356
  // @codeCoverageIgnoreEnd
357
  }
358
 
359
- // To prevent empty indices.
360
- $wpdb->query( "ANALYZE TABLE $relevanssi_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
361
-
362
  $complete = false;
363
  $size = $indexing_query_args['size'];
364
 
@@ -366,6 +363,9 @@ function relevanssi_build_index( $extend_offset = false, $verbose = null, $post_
366
  $complete = true;
367
  update_option( 'relevanssi_indexed', 'done', false );
368
 
 
 
 
369
  // Update the document count variable.
370
  relevanssi_async_update_doc_count();
371
  }
356
  // @codeCoverageIgnoreEnd
357
  }
358
 
 
 
 
359
  $complete = false;
360
  $size = $indexing_query_args['size'];
361
 
363
  $complete = true;
364
  update_option( 'relevanssi_indexed', 'done', false );
365
 
366
+ // To prevent empty indices.
367
+ $wpdb->query( "ANALYZE TABLE $relevanssi_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
368
+
369
  // Update the document count variable.
370
  relevanssi_async_update_doc_count();
371
  }
lib/init.php CHANGED
@@ -35,7 +35,7 @@ add_action( 'deleted_comment', 'relevanssi_index_comment' );
35
 
36
  // Attachment indexing.
37
  add_action( 'delete_attachment', 'relevanssi_remove_doc' );
38
- add_action( 'add_attachment', 'relevanssi_publish', 12 );
39
  add_action( 'edit_attachment', 'relevanssi_insert_edit' );
40
 
41
  // When a post status changes, check child posts that inherit their status from parent.
@@ -186,6 +186,10 @@ function relevanssi_init() {
186
  require_once 'compatibility/seoframework.php';
187
  }
188
 
 
 
 
 
189
  if ( function_exists( 'members_content_permissions_enabled' ) ) {
190
  require_once 'compatibility/members.php';
191
  }
@@ -235,6 +239,20 @@ function relevanssi_init() {
235
  if ( defined( 'CT_VERSION' ) ) {
236
  require_once 'compatibility/oxygen.php';
237
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  }
239
 
240
  /**
@@ -391,7 +409,7 @@ function relevanssi_create_database_tables( $relevanssi_db_version ) {
391
  $relevanssi_term_reverse_idx_exists = false;
392
  $docs_exists = false;
393
  $typeitem_exists = false;
394
- $doctermitem_exists = false;
395
  foreach ( $indices as $index ) {
396
  if ( 'terms' === $index->Key_name ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName
397
  $terms_exists = true;
@@ -420,11 +438,6 @@ function relevanssi_create_database_tables( $relevanssi_db_version ) {
420
  $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
421
  }
422
 
423
- if ( ! $docs_exists ) {
424
- $sql = "CREATE INDEX docs ON $relevanssi_table (doc)";
425
- $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
426
- }
427
-
428
  if ( ! $typeitem_exists ) {
429
  $sql = "CREATE INDEX typeitem ON $relevanssi_table (type(190), item)";
430
  $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
@@ -435,6 +448,11 @@ function relevanssi_create_database_tables( $relevanssi_db_version ) {
435
  $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
436
  }
437
 
 
 
 
 
 
438
  $sql = 'CREATE TABLE ' . $relevanssi_stopword_table . " (stopword varchar(50) $charset_collate_bin_column NOT NULL,
439
  PRIMARY KEY stopword (stopword)) $charset_collate;";
440
 
@@ -477,7 +495,8 @@ function relevanssi_create_database_tables( $relevanssi_db_version ) {
477
  update_option( 'relevanssi_db_version', $relevanssi_db_version );
478
  }
479
 
480
- if ( empty( get_option( 'relevanssi_stopwords', '' ) ) ) {
 
481
  relevanssi_populate_stopwords();
482
  }
483
  }
35
 
36
  // Attachment indexing.
37
  add_action( 'delete_attachment', 'relevanssi_remove_doc' );
38
+ add_action( 'add_attachment', 'relevanssi_insert_edit', 12 );
39
  add_action( 'edit_attachment', 'relevanssi_insert_edit' );
40
 
41
  // When a post status changes, check child posts that inherit their status from parent.
186
  require_once 'compatibility/seoframework.php';
187
  }
188
 
189
+ if ( class_exists( 'RankMath', false ) ) {
190
+ require_once 'compatibility/rankmath.php';
191
+ }
192
+
193
  if ( function_exists( 'members_content_permissions_enabled' ) ) {
194
  require_once 'compatibility/members.php';
195
  }
239
  if ( defined( 'CT_VERSION' ) ) {
240
  require_once 'compatibility/oxygen.php';
241
  }
242
+
243
+ if ( ! is_array( get_option( 'relevanssi_stopwords' ) ) ) {
244
+ // Version 2.12 / 4.10 changes stopwords option from a string to an
245
+ // array to support multilingual stopwords. This function converts old
246
+ // style to new style. Remove eventually.
247
+ relevanssi_update_stopwords_setting();
248
+ }
249
+
250
+ if ( ! is_array( get_option( 'relevanssi_synonyms' ) ) ) {
251
+ // Version 2.12 / 4.10 changes synonyms option from a string to an
252
+ // array to support multilingual synonyms. This function converts old
253
+ // style to new style. Remove eventually.
254
+ relevanssi_update_synonyms_setting();
255
+ }
256
  }
257
 
258
  /**
409
  $relevanssi_term_reverse_idx_exists = false;
410
  $docs_exists = false;
411
  $typeitem_exists = false;
412
+ $doctermitem_exists = false;
413
  foreach ( $indices as $index ) {
414
  if ( 'terms' === $index->Key_name ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName
415
  $terms_exists = true;
438
  $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
439
  }
440
 
 
 
 
 
 
441
  if ( ! $typeitem_exists ) {
442
  $sql = "CREATE INDEX typeitem ON $relevanssi_table (type(190), item)";
443
  $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
448
  $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
449
  }
450
 
451
+ if ( $docs_exists ) { // This index was removed in 4.9.2 / 2.11.2.
452
+ $sql = "DROP INDEX docs ON $relevanssi_table (doc)";
453
+ $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery
454
+ }
455
+
456
  $sql = 'CREATE TABLE ' . $relevanssi_stopword_table . " (stopword varchar(50) $charset_collate_bin_column NOT NULL,
457
  PRIMARY KEY stopword (stopword)) $charset_collate;";
458
 
495
  update_option( 'relevanssi_db_version', $relevanssi_db_version );
496
  }
497
 
498
+ $stopwords = relevanssi_fetch_stopwords();
499
+ if ( empty( $stopwords ) ) {
500
  relevanssi_populate_stopwords();
501
  }
502
  }
lib/install.php CHANGED
@@ -121,8 +121,8 @@ function _relevanssi_install() {
121
  add_option( 'relevanssi_respect_exclude', 'on' );
122
  add_option( 'relevanssi_show_matches', '' );
123
  add_option( 'relevanssi_show_matches_text', '(Search hits: %body% in body, %title% in title, %categories% in categories, %tags% in tags, %taxonomies% in other taxonomies, %comments% in comments. Score: %score%)' );
124
- add_option( 'relevanssi_stopwords', '' );
125
- add_option( 'relevanssi_synonyms', '' );
126
  add_option( 'relevanssi_throttle', 'on' );
127
  add_option( 'relevanssi_throttle_limit', '500' );
128
  add_option( 'relevanssi_title_boost', $relevanssi_variables['title_boost_default'] );
121
  add_option( 'relevanssi_respect_exclude', 'on' );
122
  add_option( 'relevanssi_show_matches', '' );
123
  add_option( 'relevanssi_show_matches_text', '(Search hits: %body% in body, %title% in title, %categories% in categories, %tags% in tags, %taxonomies% in other taxonomies, %comments% in comments. Score: %score%)' );
124
+ add_option( 'relevanssi_stopwords', array() );
125
+ add_option( 'relevanssi_synonyms', array() );
126
  add_option( 'relevanssi_throttle', 'on' );
127
  add_option( 'relevanssi_throttle_limit', '500' );
128
  add_option( 'relevanssi_title_boost', $relevanssi_variables['title_boost_default'] );
lib/interface.php CHANGED
@@ -99,272 +99,6 @@ function relevanssi_options() {
99
  echo "<div style='clear:both'></div></div>";
100
  }
101
 
102
- /**
103
- * Updates Relevanssi options.
104
- *
105
- * Checks the option values and updates the options. It's safe to use $_REQUEST here,
106
- * check_admin_referer() is done immediately before this function is called.
107
- */
108
- function update_relevanssi_options() {
109
- // phpcs:disable WordPress.Security.NonceVerification
110
- if ( 'indexing' === $_REQUEST['tab'] ) {
111
- relevanssi_turn_off_options(
112
- $_REQUEST,
113
- array(
114
- 'relevanssi_expand_shortcodes',
115
- 'relevanssi_index_author',
116
- 'relevanssi_index_excerpt',
117
- 'relevanssi_index_image_files',
118
- )
119
- );
120
- relevanssi_update_intval( $_REQUEST, 'relevanssi_min_word_length', true, 3 );
121
- }
122
-
123
- if ( 'searching' === $_REQUEST['tab'] ) {
124
- relevanssi_turn_off_options(
125
- $_REQUEST,
126
- array(
127
- 'relevanssi_admin_search',
128
- 'relevanssi_disable_or_fallback',
129
- 'relevanssi_exact_match_bonus',
130
- 'relevanssi_polylang_all_languages',
131
- 'relevanssi_respect_exclude',
132
- 'relevanssi_throttle',
133
- 'relevanssi_wpml_only_current',
134
- )
135
- );
136
- relevanssi_update_floatval( $_REQUEST, 'relevanssi_content_boost', true, 1 );
137
- relevanssi_update_floatval( $_REQUEST, 'relevanssi_title_boost', true, 1 );
138
- relevanssi_update_floatval( $_REQUEST, 'relevanssi_comment_boost', true, 1 );
139
- }
140
-
141
- if ( 'logging' === $_REQUEST['tab'] ) {
142
- relevanssi_turn_off_options(
143
- $_REQUEST,
144
- array(
145
- 'relevanssi_log_queries',
146
- 'relevanssi_log_queries_with_ip',
147
- )
148
- );
149
- }
150
-
151
- if ( 'excerpts' === $_REQUEST['tab'] ) {
152
- relevanssi_turn_off_options(
153
- $_REQUEST,
154
- array(
155
- 'relevanssi_excerpt_custom_fields',
156
- 'relevanssi_excerpts',
157
- 'relevanssi_expand_highlights',
158
- 'relevanssi_highlight_comments',
159
- 'relevanssi_highlight_docs',
160
- 'relevanssi_hilite_title',
161
- 'relevanssi_show_matches',
162
- )
163
- );
164
- if ( isset( $_REQUEST['relevanssi_show_matches_text'] ) ) {
165
- $value = $_REQUEST['relevanssi_show_matches_text'];
166
- $value = str_replace( '"', "'", $value );
167
- update_option( 'relevanssi_show_matches_text', $value );
168
- }
169
- }
170
-
171
- if ( isset( $_REQUEST['relevanssi_synonyms'] ) ) {
172
- $linefeeds = array( "\r\n", "\n", "\r" );
173
- $value = str_replace( $linefeeds, ';', $_REQUEST['relevanssi_synonyms'] );
174
- $value = stripslashes( $value );
175
- update_option( 'relevanssi_synonyms', $value );
176
- }
177
-
178
- $relevanssi_punct = array();
179
- if ( isset( $_REQUEST['relevanssi_punct_quotes'] ) ) {
180
- $relevanssi_punct['quotes'] = $_REQUEST['relevanssi_punct_quotes'];
181
- }
182
- if ( isset( $_REQUEST['relevanssi_punct_hyphens'] ) ) {
183
- $relevanssi_punct['hyphens'] = $_REQUEST['relevanssi_punct_hyphens'];
184
- }
185
- if ( isset( $_REQUEST['relevanssi_punct_ampersands'] ) ) {
186
- $relevanssi_punct['ampersands'] = $_REQUEST['relevanssi_punct_ampersands'];
187
- }
188
- if ( isset( $_REQUEST['relevanssi_punct_decimals'] ) ) {
189
- $relevanssi_punct['decimals'] = $_REQUEST['relevanssi_punct_decimals'];
190
- }
191
- if ( ! empty( $relevanssi_punct ) ) {
192
- update_option( 'relevanssi_punctuation', $relevanssi_punct );
193
- }
194
-
195
- $post_type_weights = array();
196
- $index_post_types = array();
197
- $index_taxonomies_list = array();
198
- $index_terms_list = array();
199
- foreach ( $_REQUEST as $key => $value ) {
200
- if ( empty( $value ) ) {
201
- $value = 0;
202
- }
203
-
204
- if ( 'relevanssi_weight_' === substr( $key, 0, strlen( 'relevanssi_weight_' ) ) ) {
205
- $type = substr( $key, strlen( 'relevanssi_weight_' ) );
206
- $post_type_weights[ $type ] = $value;
207
- }
208
- if ( 'relevanssi_taxonomy_weight_' === substr( $key, 0, strlen( 'relevanssi_taxonomy_weight_' ) ) ) {
209
- $type = 'post_tagged_with_' . substr( $key, strlen( 'relevanssi_taxonomy_weight_' ) );
210
- $post_type_weights[ $type ] = $value;
211
- }
212
- if ( 'relevanssi_term_weight_' === substr( $key, 0, strlen( 'relevanssi_term_weight_' ) ) ) {
213
- $type = 'taxonomy_term_' . substr( $key, strlen( 'relevanssi_term_weight_' ) );
214
- $post_type_weights[ $type ] = $value;
215
- }
216
- if ( 'relevanssi_index_type_' === substr( $key, 0, strlen( 'relevanssi_index_type_' ) ) ) {
217
- $type = substr( $key, strlen( 'relevanssi_index_type_' ) );
218
- if ( 'on' === $value ) {
219
- $index_post_types[ $type ] = true;
220
- }
221
- }
222
- if ( 'relevanssi_index_taxonomy_' === substr( $key, 0, strlen( 'relevanssi_index_taxonomy_' ) ) ) {
223
- $type = substr( $key, strlen( 'relevanssi_index_taxonomy_' ) );
224
- if ( 'on' === $value ) {
225
- $index_taxonomies_list[ $type ] = true;
226
- }
227
- }
228
- if ( 'relevanssi_index_terms_' === substr( $key, 0, strlen( 'relevanssi_index_terms_' ) ) ) {
229
- $type = substr( $key, strlen( 'relevanssi_index_terms_' ) );
230
- if ( 'on' === $value ) {
231
- $index_terms_list[ $type ] = true;
232
- }
233
- }
234
- }
235
-
236
- if ( 'indexing' === $_REQUEST['tab'] ) {
237
- update_option( 'relevanssi_index_taxonomies_list', array_keys( $index_taxonomies_list ), false );
238
- if ( RELEVANSSI_PREMIUM ) {
239
- update_option( 'relevanssi_index_terms', array_keys( $index_terms_list ), false );
240
- }
241
- }
242
-
243
- if ( count( $post_type_weights ) > 0 ) {
244
- update_option( 'relevanssi_post_type_weights', $post_type_weights );
245
- }
246
-
247
- if ( count( $index_post_types ) > 0 ) {
248
- update_option( 'relevanssi_index_post_types', array_keys( $index_post_types ), false );
249
- }
250
-
251
- if ( isset( $_REQUEST['relevanssi_index_fields_select'] ) ) {
252
- $fields_option = '';
253
- if ( 'all' === $_REQUEST['relevanssi_index_fields_select'] ) {
254
- $fields_option = 'all';
255
- }
256
- if ( 'visible' === $_REQUEST['relevanssi_index_fields_select'] ) {
257
- $fields_option = 'visible';
258
- }
259
- if ( 'some' === $_REQUEST['relevanssi_index_fields_select'] ) {
260
- if ( isset( $_REQUEST['relevanssi_index_fields'] ) ) {
261
- $fields_option = rtrim( $_REQUEST['relevanssi_index_fields'], " \t\n\r\0\x0B," );
262
- }
263
- }
264
- update_option( 'relevanssi_index_fields', $fields_option, false );
265
- }
266
-
267
- if ( isset( $_REQUEST['relevanssi_trim_logs'] ) ) {
268
- $trim_logs = $_REQUEST['relevanssi_trim_logs'];
269
- if ( ! is_numeric( $trim_logs ) || $trim_logs < 0 ) {
270
- $trim_logs = 0;
271
- }
272
- update_option( 'relevanssi_trim_logs', $trim_logs );
273
- }
274
-
275
- if ( isset( $_REQUEST['relevanssi_cat'] ) ) {
276
- if ( is_array( $_REQUEST['relevanssi_cat'] ) ) {
277
- $csv_cats = implode( ',', $_REQUEST['relevanssi_cat'] );
278
- update_option( 'relevanssi_cat', $csv_cats );
279
- }
280
- } else {
281
- if ( isset( $_REQUEST['relevanssi_cat_active'] ) ) {
282
- update_option( 'relevanssi_cat', '' );
283
- }
284
- }
285
-
286
- if ( isset( $_REQUEST['relevanssi_excat'] ) ) {
287
- if ( is_array( $_REQUEST['relevanssi_excat'] ) ) {
288
- $array_excats = $_REQUEST['relevanssi_excat'];
289
- $cat = get_option( 'relevanssi_cat' );
290
- if ( $cat ) {
291
- $array_cats = explode( ',', $cat );
292
- $valid_excats = array();
293
- foreach ( $array_excats as $excat ) {
294
- if ( ! in_array( $excat, $array_cats, true ) ) {
295
- $valid_excats[] = $excat;
296
- }
297
- }
298
- } else {
299
- // No category restrictions, so everything's good.
300
- $valid_excats = $array_excats;
301
- }
302
- $csv_excats = implode( ',', $valid_excats );
303
- update_option( 'relevanssi_excat', $csv_excats );
304
- }
305
- } else {
306
- if ( isset( $_REQUEST['relevanssi_excat_active'] ) ) {
307
- update_option( 'relevanssi_excat', '' );
308
- }
309
- }
310
-
311
- $options = array(
312
- 'relevanssi_admin_search' => false,
313
- 'relevanssi_bg_col' => true,
314
- 'relevanssi_class' => true,
315
- 'relevanssi_css' => true,
316
- 'relevanssi_default_orderby' => true,
317
- 'relevanssi_disable_or_fallback' => true,
318
- 'relevanssi_exact_match_bonus' => true,
319
- 'relevanssi_excerpt_allowable_tags' => true,
320
- 'relevanssi_excerpt_custom_fields' => true,
321
- 'relevanssi_excerpt_type' => true,
322
- 'relevanssi_excerpts' => true,
323
- 'relevanssi_expand_shortcodes' => false,
324
- 'relevanssi_expand_highlights' => true,
325
- 'relevanssi_expst' => true,
326
- 'relevanssi_fuzzy' => true,
327
- 'relevanssi_highlight_comments' => true,
328
- 'relevanssi_highlight_docs' => true,
329
- 'relevanssi_highlight' => true,
330
- 'relevanssi_hilite_title' => true,
331
- 'relevanssi_implicit_operator' => true,
332
- 'relevanssi_index_author' => false,
333
- 'relevanssi_index_comments' => false,
334
- 'relevanssi_index_excerpt' => false,
335
- 'relevanssi_index_image_files' => false,
336
- 'relevanssi_log_queries_with_ip' => true,
337
- 'relevanssi_log_queries' => true,
338
- 'relevanssi_omit_from_logs' => true,
339
- 'relevanssi_polylang_all_languages' => true,
340
- 'relevanssi_respect_exclude' => true,
341
- 'relevanssi_show_matches' => true,
342
- 'relevanssi_throttle' => true,
343
- 'relevanssi_txt_col' => true,
344
- 'relevanssi_wpml_only_current' => true,
345
- );
346
-
347
- if ( isset( $_REQUEST['relevanssi_expst'] ) ) {
348
- $_REQUEST['relevanssi_expst'] = trim( $_REQUEST['relevanssi_expst'], ' ,' );
349
- }
350
-
351
- array_walk(
352
- $options,
353
- function( $autoload, $option ) {
354
- if ( isset( $_REQUEST[ $option ] ) ) {
355
- update_option( $option, $_REQUEST[ $option ], $autoload );
356
- }
357
- }
358
- );
359
-
360
- relevanssi_update_intval( $_REQUEST, 'relevanssi_excerpt_length', true, 10 );
361
-
362
- if ( function_exists( 'relevanssi_update_premium_options' ) ) {
363
- relevanssi_update_premium_options();
364
- }
365
- // phpcs:enable
366
- }
367
-
368
  /**
369
  * Prints out the 'User searches' page.
370
  */
@@ -666,39 +400,6 @@ function relevanssi_date_queries( $days, $title, $version = 'good' ) {
666
  }
667
  }
668
 
669
- /**
670
- * Returns 'checked' if the option is enabled.
671
- *
672
- * @param string $option Value to check.
673
- *
674
- * @return string If the option is 'on', returns 'checked', otherwise returns an
675
- * empty string.
676
- */
677
- function relevanssi_check( $option ) {
678
- $checked = '';
679
- if ( 'on' === $option ) {
680
- $checked = 'checked';
681
- }
682
- return $checked;
683
- }
684
-
685
- /**
686
- * Returns 'selected' if the option matches a value.
687
- *
688
- * @param string $option Value to check.
689
- * @param string $value The 'selected' value.
690
- *
691
- * @return string If the option matches the value, returns 'selected', otherwise
692
- * returns an empty string.
693
- */
694
- function relevanssi_select( $option, $value ) {
695
- $selected = '';
696
- if ( $option === $value ) {
697
- $selected = 'selected';
698
- }
699
- return $selected;
700
- }
701
-
702
  /**
703
  * Prints out the Relevanssi options form.
704
  *
@@ -1005,178 +706,3 @@ function relevanssi_form_tag_weight() {
1005
  </tr>
1006
  <?php
1007
  }
1008
-
1009
- /**
1010
- * Turns off options, ie. sets them to "off".
1011
- *
1012
- * If the specified options don't exist in the request array, they are set to
1013
- * "off".
1014
- *
1015
- * @param array $request The _REQUEST array, passed as reference.
1016
- * @param array $options An array of option names.
1017
- */
1018
- function relevanssi_turn_off_options( &$request, $options ) {
1019
- array_walk(
1020
- $options,
1021
- function( $option ) use ( &$request ) {
1022
- if ( ! isset( $request[ $option ] ) ) {
1023
- $request[ $option ] = 'off';
1024
- }
1025
- }
1026
- );
1027
- }
1028
-
1029
- /**
1030
- * Returns 'on' if option exists and value is not 'off', otherwise 'off'.
1031
- *
1032
- * @param array $request An array of option values.
1033
- * @param string $option The key to check.
1034
- *
1035
- * @return string 'on' or 'off'.
1036
- */
1037
- function relevanssi_off_or_on( $request, $option ) {
1038
- if ( isset( $request[ $option ] ) && 'off' !== $request[ $option ] ) {
1039
- return 'on';
1040
- }
1041
- return 'off';
1042
- }
1043
-
1044
- /**
1045
- * Returns an imploded string if the option exists and is an array, an empty
1046
- * string otherwise.
1047
- *
1048
- * @param array $request An array of option values.
1049
- * @param string $option The key to check.
1050
- * @param string $glue The glue string for implode(), default ','.
1051
- *
1052
- * @return string Imploded string or an empty string.
1053
- */
1054
- function relevanssi_implode( $request, $option, $glue = ',' ) {
1055
- if ( isset( $request[ $option ] ) && is_array( $request[ $option ] ) ) {
1056
- return implode( $glue, $request[ $option ] );
1057
- }
1058
- return '';
1059
- }
1060
-
1061
- /**
1062
- * Returns the intval of the option if it exists, null otherwise.
1063
- *
1064
- * @param array $request An array of option values.
1065
- * @param string $option The key to check.
1066
- *
1067
- * @return int|null Integer value of the option, or null.
1068
- */
1069
- function relevanssi_intval( $request, $option ) {
1070
- if ( isset( $request[ $option ] ) ) {
1071
- return intval( $request[ $option ] );
1072
- }
1073
- return null;
1074
- }
1075
-
1076
- /**
1077
- * Returns a legal value.
1078
- *
1079
- * @param array $request An array of option values.
1080
- * @param string $option The key to check.
1081
- * @param array $values The legal values.
1082
- * @param string $default The default value.
1083
- *
1084
- * @return string|null A legal value or the default value, null if the option
1085
- * isn't set.
1086
- */
1087
- function relevanssi_legal_value( $request, $option, $values, $default ) {
1088
- $value = null;
1089
- if ( isset( $request[ $option ] ) ) {
1090
- $value = $default;
1091
- if ( in_array( $request[ $option ], $values, true ) ) {
1092
- $value = $request[ $option ];
1093
- }
1094
- }
1095
- return $value;
1096
- }
1097
-
1098
- /**
1099
- * Sets an on/off option according to the request value.
1100
- *
1101
- * @param array $request An array of option values.
1102
- * @param string $option The key to check.
1103
- * @param boolean $autoload Should the option autoload, default true.
1104
- */
1105
- function relevanssi_update_off_or_on( $request, $option, $autoload = true ) {
1106
- relevanssi_update_legal_value(
1107
- $request,
1108
- $option,
1109
- array( 'off', 'on' ),
1110
- 'off',
1111
- $autoload
1112
- );
1113
- }
1114
-
1115
- /**
1116
- * Sets an option after sanitizing and unslashing the value.
1117
- *
1118
- * @param array $request An array of option values.
1119
- * @param string $option The key to check.
1120
- * @param boolean $autoload Should the option autoload, default true.
1121
- */
1122
- function relevanssi_update_sanitized( $request, $option, $autoload = true ) {
1123
- if ( isset( $request[ $option ] ) ) {
1124
- $value = sanitize_text_field( wp_unslash( $request[ $option ] ) );
1125
- update_option( $option, $value, $autoload );
1126
- }
1127
- }
1128
-
1129
- /**
1130
- * Sets an option after doing intval.
1131
- *
1132
- * @param array $request An array of option values.
1133
- * @param string $option The key to check.
1134
- * @param boolean $autoload Should the option autoload, default true.
1135
- * @param int $default The default value if intval() fails, default 0.
1136
- */
1137
- function relevanssi_update_intval( $request, $option, $autoload = true, $default = 0 ) {
1138
- if ( isset( $request[ $option ] ) ) {
1139
- $value = intval( $request[ $option ] );
1140
- if ( ! $value ) {
1141
- $value = $default;
1142
- }
1143
- update_option( $option, $value, $autoload );
1144
- }
1145
- }
1146
-
1147
- /**
1148
- * Sets an option after doing floatval.
1149
- *
1150
- * @param array $request An array of option values.
1151
- * @param string $option The key to check.
1152
- * @param boolean $autoload Should the option autoload, default true.
1153
- * @param int $default The default value if floatval() fails, default 0.
1154
- */
1155
- function relevanssi_update_floatval( $request, $option, $autoload = true, $default = 0 ) {
1156
- if ( isset( $request[ $option ] ) ) {
1157
- $value = floatval( $request[ $option ] );
1158
- if ( ! $value ) {
1159
- $value = $default;
1160
- }
1161
- update_option( $option, $value, $autoload );
1162
- }
1163
- }
1164
-
1165
- /**
1166
- * Sets an option with one of the listed legal values.
1167
- *
1168
- * @param array $request An array of option values.
1169
- * @param string $option The key to check.
1170
- * @param array $values The legal values.
1171
- * @param string $default The default value.
1172
- * @param boolean $autoload Should the option autoload, default true.
1173
- */
1174
- function relevanssi_update_legal_value( $request, $option, $values, $default, $autoload = true ) {
1175
- if ( isset( $request[ $option ] ) ) {
1176
- $value = $default;
1177
- if ( in_array( $request[ $option ], $values, true ) ) {
1178
- $value = $request[ $option ];
1179
- }
1180
- update_option( $option, $value, $autoload );
1181
- }
1182
- }
99
  echo "<div style='clear:both'></div></div>";
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  /**
103
  * Prints out the 'User searches' page.
104
  */
400
  }
401
  }
402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  /**
404
  * Prints out the Relevanssi options form.
405
  *
706
  </tr>
707
  <?php
708
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/options.php ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * /lib/options.php
4
+ *
5
+ * @package Relevanssi
6
+ * @author Mikko Saari
7
+ * @license https://wordpress.org/about/gpl/ GNU General Public License
8
+ * @see https://www.relevanssi.com/
9
+ */
10
+
11
+ /**
12
+ * Updates Relevanssi options.
13
+ *
14
+ * Checks the option values and updates the options. It's safe to use $_REQUEST
15
+ * here, check_admin_referer() is done immediately before this function is
16
+ * called.
17
+ */
18
+ function update_relevanssi_options() {
19
+ // phpcs:disable WordPress.Security.NonceVerification
20
+ $data = relevanssi_process_weights_and_indexing( $_REQUEST );
21
+ $post_type_weights = $data['post_type_weights'];
22
+ $index_post_types = $data['index_post_types'];
23
+ $index_taxonomies_list = $data['index_taxonomies_list'];
24
+ $index_terms_list = $data['index_terms_list'];
25
+
26
+ if ( 'indexing' === $_REQUEST['tab'] ) {
27
+ relevanssi_turn_off_options(
28
+ $_REQUEST,
29
+ array(
30
+ 'relevanssi_expand_shortcodes',
31
+ 'relevanssi_index_author',
32
+ 'relevanssi_index_excerpt',
33
+ 'relevanssi_index_image_files',
34
+ )
35
+ );
36
+ relevanssi_update_intval( $_REQUEST, 'relevanssi_min_word_length', true, 3 );
37
+ update_option( 'relevanssi_index_taxonomies_list', array_keys( $index_taxonomies_list ), false );
38
+ if ( RELEVANSSI_PREMIUM ) {
39
+ update_option( 'relevanssi_index_terms', array_keys( $index_terms_list ), false );
40
+ }
41
+ }
42
+
43
+ if ( 'searching' === $_REQUEST['tab'] ) {
44
+ relevanssi_turn_off_options(
45
+ $_REQUEST,
46
+ array(
47
+ 'relevanssi_admin_search',
48
+ 'relevanssi_disable_or_fallback',
49
+ 'relevanssi_exact_match_bonus',
50
+ 'relevanssi_polylang_all_languages',
51
+ 'relevanssi_respect_exclude',
52
+ 'relevanssi_throttle',
53
+ 'relevanssi_wpml_only_current',
54
+ )
55
+ );
56
+ relevanssi_update_floatval( $_REQUEST, 'relevanssi_content_boost', true, 1, true );
57
+ relevanssi_update_floatval( $_REQUEST, 'relevanssi_title_boost', true, 1, true );
58
+ relevanssi_update_floatval( $_REQUEST, 'relevanssi_comment_boost', true, 1, true );
59
+ }
60
+
61
+ if ( 'logging' === $_REQUEST['tab'] ) {
62
+ relevanssi_turn_off_options(
63
+ $_REQUEST,
64
+ array(
65
+ 'relevanssi_log_queries',
66
+ 'relevanssi_log_queries_with_ip',
67
+ )
68
+ );
69
+ }
70
+
71
+ if ( 'excerpts' === $_REQUEST['tab'] ) {
72
+ relevanssi_turn_off_options(
73
+ $_REQUEST,
74
+ array(
75
+ 'relevanssi_excerpt_custom_fields',
76
+ 'relevanssi_excerpts',
77
+ 'relevanssi_expand_highlights',
78
+ 'relevanssi_highlight_comments',
79
+ 'relevanssi_highlight_docs',
80
+ 'relevanssi_hilite_title',
81
+ 'relevanssi_show_matches',
82
+ )
83
+ );
84
+ if ( isset( $_REQUEST['relevanssi_show_matches_text'] ) ) {
85
+ $value = $_REQUEST['relevanssi_show_matches_text'];
86
+ $value = str_replace( '"', "'", $value );
87
+ update_option( 'relevanssi_show_matches_text', $value );
88
+ }
89
+ }
90
+
91
+ if ( isset( $_REQUEST['relevanssi_synonyms'] ) ) {
92
+ $linefeeds = array( "\r\n", "\n", "\r" );
93
+ $value = str_replace( $linefeeds, ';', $_REQUEST['relevanssi_synonyms'] );
94
+ $value = stripslashes( $value );
95
+
96
+ $synonym_option = get_option( 'relevanssi_synonyms', array() );
97
+ $current_language = relevanssi_get_current_language();
98
+
99
+ $synonym_option[ $current_language ] = $value;
100
+
101
+ update_option( 'relevanssi_synonyms', $synonym_option );
102
+ }
103
+
104
+ $relevanssi_punct = array();
105
+ if ( isset( $_REQUEST['relevanssi_punct_quotes'] ) ) {
106
+ $relevanssi_punct['quotes'] = $_REQUEST['relevanssi_punct_quotes'];
107
+ }
108
+ if ( isset( $_REQUEST['relevanssi_punct_hyphens'] ) ) {
109
+ $relevanssi_punct['hyphens'] = $_REQUEST['relevanssi_punct_hyphens'];
110
+ }
111
+ if ( isset( $_REQUEST['relevanssi_punct_ampersands'] ) ) {
112
+ $relevanssi_punct['ampersands'] = $_REQUEST['relevanssi_punct_ampersands'];
113
+ }
114
+ if ( isset( $_REQUEST['relevanssi_punct_decimals'] ) ) {
115
+ $relevanssi_punct['decimals'] = $_REQUEST['relevanssi_punct_decimals'];
116
+ }
117
+ if ( ! empty( $relevanssi_punct ) ) {
118
+ update_option( 'relevanssi_punctuation', $relevanssi_punct );
119
+ }
120
+
121
+ if ( count( $post_type_weights ) > 0 ) {
122
+ update_option( 'relevanssi_post_type_weights', $post_type_weights );
123
+ }
124
+
125
+ if ( count( $index_post_types ) > 0 ) {
126
+ update_option( 'relevanssi_index_post_types', array_keys( $index_post_types ), false );
127
+ }
128
+
129
+ if ( isset( $_REQUEST['relevanssi_index_fields_select'] ) ) {
130
+ $fields_option = '';
131
+ if ( 'all' === $_REQUEST['relevanssi_index_fields_select'] ) {
132
+ $fields_option = 'all';
133
+ }
134
+ if ( 'visible' === $_REQUEST['relevanssi_index_fields_select'] ) {
135
+ $fields_option = 'visible';
136
+ }
137
+ if ( 'some' === $_REQUEST['relevanssi_index_fields_select'] ) {
138
+ if ( isset( $_REQUEST['relevanssi_index_fields'] ) ) {
139
+ $fields_option = rtrim( $_REQUEST['relevanssi_index_fields'], " \t\n\r\0\x0B," );
140
+ }
141
+ }
142
+ update_option( 'relevanssi_index_fields', $fields_option, false );
143
+ }
144
+
145
+ if ( isset( $_REQUEST['relevanssi_trim_logs'] ) ) {
146
+ $trim_logs = $_REQUEST['relevanssi_trim_logs'];
147
+ if ( ! is_numeric( $trim_logs ) || $trim_logs < 0 ) {
148
+ $trim_logs = 0;
149
+ }
150
+ update_option( 'relevanssi_trim_logs', $trim_logs );
151
+ }
152
+
153
+ if ( isset( $_REQUEST['relevanssi_cat'] ) ) {
154
+ if ( is_array( $_REQUEST['relevanssi_cat'] ) ) {
155
+ $csv_cats = implode( ',', $_REQUEST['relevanssi_cat'] );
156
+ update_option( 'relevanssi_cat', $csv_cats );
157
+ }
158
+ } else {
159
+ if ( isset( $_REQUEST['relevanssi_cat_active'] ) ) {
160
+ update_option( 'relevanssi_cat', '' );
161
+ }
162
+ }
163
+
164
+ if ( isset( $_REQUEST['relevanssi_excat'] ) ) {
165
+ if ( is_array( $_REQUEST['relevanssi_excat'] ) ) {
166
+ $array_excats = $_REQUEST['relevanssi_excat'];
167
+ $cat = get_option( 'relevanssi_cat' );
168
+ if ( $cat ) {
169
+ $array_cats = explode( ',', $cat );
170
+ $valid_excats = array();
171
+ foreach ( $array_excats as $excat ) {
172
+ if ( ! in_array( $excat, $array_cats, true ) ) {
173
+ $valid_excats[] = $excat;
174
+ }
175
+ }
176
+ } else {
177
+ // No category restrictions, so everything's good.
178
+ $valid_excats = $array_excats;
179
+ }
180
+ $csv_excats = implode( ',', $valid_excats );
181
+ update_option( 'relevanssi_excat', $csv_excats );
182
+ }
183
+ } else {
184
+ if ( isset( $_REQUEST['relevanssi_excat_active'] ) ) {
185
+ update_option( 'relevanssi_excat', '' );
186
+ }
187
+ }
188
+
189
+ $options = array(
190
+ 'relevanssi_admin_search' => false,
191
+ 'relevanssi_bg_col' => true,
192
+ 'relevanssi_class' => true,
193
+ 'relevanssi_css' => true,
194
+ 'relevanssi_default_orderby' => true,
195
+ 'relevanssi_disable_or_fallback' => true,
196
+ 'relevanssi_exact_match_bonus' => true,
197
+ 'relevanssi_excerpt_allowable_tags' => true,
198
+ 'relevanssi_excerpt_custom_fields' => true,
199
+ 'relevanssi_excerpt_type' => true,
200
+ 'relevanssi_excerpts' => true,
201
+ 'relevanssi_exclude_posts' => true,
202
+ 'relevanssi_expand_shortcodes' => false,
203
+ 'relevanssi_expand_highlights' => true,
204
+ 'relevanssi_fuzzy' => true,
205
+ 'relevanssi_highlight_comments' => true,
206
+ 'relevanssi_highlight_docs' => true,
207
+ 'relevanssi_highlight' => true,
208
+ 'relevanssi_hilite_title' => true,
209
+ 'relevanssi_implicit_operator' => true,
210
+ 'relevanssi_index_author' => false,
211
+ 'relevanssi_index_comments' => false,
212
+ 'relevanssi_index_excerpt' => false,
213
+ 'relevanssi_index_image_files' => false,
214
+ 'relevanssi_log_queries_with_ip' => true,
215
+ 'relevanssi_log_queries' => true,
216
+ 'relevanssi_omit_from_logs' => true,
217
+ 'relevanssi_polylang_all_languages' => true,
218
+ 'relevanssi_respect_exclude' => true,
219
+ 'relevanssi_show_matches' => true,
220
+ 'relevanssi_throttle' => true,
221
+ 'relevanssi_txt_col' => true,
222
+ 'relevanssi_wpml_only_current' => true,
223
+ );
224
+
225
+ if ( isset( $_REQUEST['relevanssi_exclude_posts'] ) ) {
226
+ $_REQUEST['relevanssi_exclude_posts'] = trim( $_REQUEST['relevanssi_exclude_posts'], ' ,' );
227
+ }
228
+
229
+ array_walk(
230
+ $options,
231
+ function( $autoload, $option ) {
232
+ if ( isset( $_REQUEST[ $option ] ) ) {
233
+ update_option( $option, $_REQUEST[ $option ], $autoload );
234
+ }
235
+ }
236
+ );
237
+
238
+ relevanssi_update_intval( $_REQUEST, 'relevanssi_excerpt_length', true, 10 );
239
+
240
+ if ( function_exists( 'relevanssi_update_premium_options' ) ) {
241
+ relevanssi_update_premium_options();
242
+ }
243
+ // phpcs:enable
244
+ }
245
+
246
+ /**
247
+ * Fetches option values for variable name options.
248
+ *
249
+ * Goes through all options and picks up all options that have names that
250
+ * contain post types, taxonomies and so on.
251
+ *
252
+ * @param array $request The $_REQUEST array.
253
+ *
254
+ * @return array Four arrays containing the required data.
255
+ */
256
+ function relevanssi_process_weights_and_indexing( $request ) {
257
+ $post_type_weights = array();
258
+ $index_post_types = array();
259
+ $index_taxonomies_list = array();
260
+ $index_terms_list = array();
261
+ foreach ( $request as $key => $value ) {
262
+ if ( empty( $value ) ) {
263
+ $value = 0;
264
+ }
265
+
266
+ if ( 'relevanssi_weight_' === substr( $key, 0, strlen( 'relevanssi_weight_' ) ) ) {
267
+ $type = substr( $key, strlen( 'relevanssi_weight_' ) );
268
+ $post_type_weights[ $type ] = $value;
269
+ }
270
+ if ( 'relevanssi_taxonomy_weight_' === substr( $key, 0, strlen( 'relevanssi_taxonomy_weight_' ) ) ) {
271
+ $type = 'post_tagged_with_' . substr( $key, strlen( 'relevanssi_taxonomy_weight_' ) );
272
+ $post_type_weights[ $type ] = $value;
273
+ }
274
+ if ( 'relevanssi_term_weight_' === substr( $key, 0, strlen( 'relevanssi_term_weight_' ) ) ) {
275
+ $type = 'taxonomy_term_' . substr( $key, strlen( 'relevanssi_term_weight_' ) );
276
+ $post_type_weights[ $type ] = $value;
277
+ }
278
+ if ( 'relevanssi_index_type_' === substr( $key, 0, strlen( 'relevanssi_index_type_' ) ) ) {
279
+ $type = substr( $key, strlen( 'relevanssi_index_type_' ) );
280
+ if ( 'on' === $value ) {
281
+ $index_post_types[ $type ] = true;
282
+ }
283
+ }
284
+ if ( 'relevanssi_index_taxonomy_' === substr( $key, 0, strlen( 'relevanssi_index_taxonomy_' ) ) ) {
285
+ $type = substr( $key, strlen( 'relevanssi_index_taxonomy_' ) );
286
+ if ( 'on' === $value ) {
287
+ $index_taxonomies_list[ $type ] = true;
288
+ }
289
+ }
290
+ if ( 'relevanssi_index_terms_' === substr( $key, 0, strlen( 'relevanssi_index_terms_' ) ) ) {
291
+ $type = substr( $key, strlen( 'relevanssi_index_terms_' ) );
292
+ if ( 'on' === $value ) {
293
+ $index_terms_list[ $type ] = true;
294
+ }
295
+ }
296
+ }
297
+
298
+ $post_type_weights = array_map( 'relevanssi_sanitize_weights', $post_type_weights );
299
+
300
+ return array(
301
+ 'post_type_weights' => $post_type_weights,
302
+ 'index_post_types' => $index_post_types,
303
+ 'index_taxonomies_list' => $index_taxonomies_list,
304
+ 'index_terms_list' => $index_terms_list,
305
+ );
306
+ }
307
+
308
+ /**
309
+ * Takes a value, converts it to float and if it's negative or zero, sets it
310
+ * to 1.
311
+ *
312
+ * @param mixed $weight The weight value, which can be anything user enters.
313
+ *
314
+ * @return float The float value of the weight.
315
+ */
316
+ function relevanssi_sanitize_weights( $weight ) {
317
+ $weight = floatval( $weight );
318
+ if ( $weight <= 0 ) {
319
+ $weight = 1;
320
+ }
321
+ return $weight;
322
+ }
lib/phrases.php ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * /lib/phrases.php
4
+ *
5
+ * @package Relevanssi
6
+ * @author Mikko Saari
7
+ * @license https://wordpress.org/about/gpl/ GNU General Public License
8
+ * @see https://www.relevanssi.com/
9
+ */
10
+
11
+ /**
12
+ * Extracts phrases from the search query.
13
+ *
14
+ * Finds all phrases wrapped in quotes (curly or straight) from the search
15
+ * query.
16
+ *
17
+ * @param string $query The search query.
18
+ *
19
+ * @return array An array of phrases (strings).
20
+ */
21
+ function relevanssi_extract_phrases( $query ) {
22
+ // iOS uses “” as the default quotes, so Relevanssi needs to understand
23
+ // that as well.
24
+ $normalized_query = str_replace( array( '”', '“' ), '"', $query );
25
+ $pos = relevanssi_stripos( $normalized_query, '"' );
26
+
27
+ $phrases = array();
28
+ while ( false !== $pos ) {
29
+ if ( $pos + 2 > relevanssi_strlen( $normalized_query ) ) {
30
+ $pos = false;
31
+ continue;
32
+ }
33
+ $start = relevanssi_stripos( $normalized_query, '"', $pos );
34
+ $end = false;
35
+ if ( false !== $start ) {
36
+ $end = relevanssi_stripos( $normalized_query, '"', $start + 2 );
37
+ }
38
+ if ( false === $end ) {
39
+ // Just one " in the query.
40
+ $pos = $end;
41
+ continue;
42
+ }
43
+ $phrase = relevanssi_substr(
44
+ $normalized_query,
45
+ $start + 1,
46
+ $end - $start - 1
47
+ );
48
+ $phrase = trim( $phrase );
49
+
50
+ // Do not count single-word phrases as phrases.
51
+ if ( ! empty( $phrase ) && count( explode( ' ', $phrase ) ) > 1 ) {
52
+ $phrases[] = $phrase;
53
+ }
54
+ $pos = $end + 1;
55
+ }
56
+
57
+ return $phrases;
58
+ }
59
+
60
+ /**
61
+ * Generates the MySQL code for restricting the search to phrase hits.
62
+ *
63
+ * This function uses relevanssi_extract_phrases() to figure out the phrases in
64
+ * the search query, then generates MySQL queries to restrict the search to the
65
+ * posts containing those phrases in the title, content, taxonomy terms or meta
66
+ * fields.
67
+ *
68
+ * @global array $relevanssi_variables The global Relevanssi variables.
69
+ *
70
+ * @param string $search_query The search query.
71
+ * @param string $operator The search operator (AND or OR).
72
+ *
73
+ * @return string $queries If not phrase hits are found, an empty string;
74
+ * otherwise MySQL queries to restrict the search.
75
+ */
76
+ function relevanssi_recognize_phrases( $search_query, $operator = 'AND' ) {
77
+ global $relevanssi_variables;
78
+
79
+ $phrases = relevanssi_extract_phrases( $search_query );
80
+
81
+ $all_queries = array();
82
+ if ( 0 === count( $phrases ) ) {
83
+ return $all_queries;
84
+ }
85
+
86
+ $custom_fields = relevanssi_get_custom_fields();
87
+ $taxonomies = get_option( 'relevanssi_index_taxonomies_list', array() );
88
+ $excerpts = get_option( 'relevanssi_index_excerpt', 'off' );
89
+ $index_pdf_parent = get_option( 'relevanssi_index_pdf_parent' );
90
+
91
+ $phrase_queries = array();
92
+ $queries = array();
93
+
94
+ if (
95
+ isset( $relevanssi_variables['phrase_targets'] ) &&
96
+ is_array( $relevanssi_variables['phrase_targets'] )
97
+ ) {
98
+ $non_targeted_phrases = array();
99
+ foreach ( $phrases as $phrase ) {
100
+ if (
101
+ isset( $relevanssi_variables['phrase_targets'][ $phrase ] ) &&
102
+ function_exists( 'relevanssi_targeted_phrases' )
103
+ ) {
104
+ $queries = relevanssi_targeted_phrases( $phrase );
105
+ } else {
106
+ $non_targeted_phrases[] = $phrase;
107
+ }
108
+ }
109
+ $phrases = $non_targeted_phrases;
110
+ }
111
+
112
+ $queries = array_merge(
113
+ $queries,
114
+ relevanssi_generate_phrase_queries(
115
+ $phrases,
116
+ $taxonomies,
117
+ $custom_fields,
118
+ $excerpts,
119
+ $index_pdf_parent
120
+ )
121
+ );
122
+
123
+ $phrase_queries = array();
124
+
125
+ foreach ( $queries as $phrase => $p_queries ) {
126
+ $p_queries = implode( ' OR relevanssi.doc IN ', $p_queries );
127
+ $p_queries = "(relevanssi.doc IN $p_queries)";
128
+ $all_queries[] = $p_queries;
129
+
130
+ $phrase_queries[ $phrase ] = $p_queries;
131
+ }
132
+
133
+ $operator = strtoupper( $operator );
134
+ if ( 'AND' !== $operator && 'OR' !== $operator ) {
135
+ $operator = 'AND';
136
+ }
137
+
138
+ if ( ! empty( $all_queries ) ) {
139
+ $all_queries = ' AND ( ' . implode( ' ' . $operator . ' ', $all_queries ) . ' ) ';
140
+ }
141
+
142
+ return array(
143
+ 'and' => $all_queries,
144
+ 'or' => $phrase_queries,
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Generates the phrase queries from phrases.
150
+ *
151
+ * Takes in phrases and a bunch of parameters and generates the MySQL queries
152
+ * that restrict the main search query to only posts that have the phrase.
153
+ *
154
+ * @param array $phrases A list of phrases to handle.
155
+ * @param array $taxonomies An array of taxonomy names to use.
156
+ * @param array $custom_fields A list of custom field names to use.
157
+ * @param string $excerpts If 'on', include excerpts.
158
+ * @param string $index_pdf_parent If 'on', include PDF parent.
159
+ *
160
+ * @global object $wpdb The WordPress database interface.
161
+ *
162
+ * @return array An array of queries sorted by phrase.
163
+ */
164
+ function relevanssi_generate_phrase_queries( $phrases, $taxonomies, $custom_fields, $excerpts, $index_pdf_parent ) {
165
+ global $wpdb;
166
+
167
+ $status = relevanssi_valid_status_array();
168
+
169
+ // Add "inherit" to the list of allowed statuses to include attachments.
170
+ if ( ! strstr( $status, 'inherit' ) ) {
171
+ $status .= ",'inherit'";
172
+ }
173
+
174
+ $phrase_queries = array();
175
+
176
+ foreach ( $phrases as $phrase ) {
177
+ $queries = array();
178
+ $phrase = $wpdb->esc_like( $phrase );
179
+ $phrase = str_replace( array( '‘', '’', "'", '"', '”', '“', '“', '„', '´' ), '_', $phrase );
180
+ $phrase = htmlspecialchars( $phrase );
181
+ $phrase = esc_sql( $phrase );
182
+
183
+ $excerpt = '';
184
+ if ( 'on' === $excerpts ) {
185
+ $excerpt = "OR post_excerpt LIKE '%$phrase%'";
186
+ }
187
+
188
+ $query = "(SELECT ID FROM $wpdb->posts
189
+ WHERE (post_content LIKE '%$phrase%'
190
+ OR post_title LIKE '%$phrase%' $excerpt)
191
+ AND post_status IN ($status))";
192
+
193
+ $queries[] = $query;
194
+
195
+ if ( $taxonomies ) {
196
+ $taxonomies_escaped = implode( "','", array_map( 'esc_sql', $taxonomies ) );
197
+ $taxonomies_sql = "AND s.taxonomy IN ('$taxonomies_escaped')";
198
+
199
+ $query = "(SELECT ID FROM
200
+ $wpdb->posts as p,
201
+ $wpdb->term_relationships as r,
202
+ $wpdb->term_taxonomy as s, $wpdb->terms as t
203
+ WHERE r.term_taxonomy_id = s.term_taxonomy_id
204
+ AND s.term_id = t.term_id AND p.ID = r.object_id
205
+ $taxonomies_sql
206
+ AND t.name LIKE '%$phrase%' AND p.post_status IN ($status))";
207
+
208
+ $queries[] = $query;
209
+ }
210
+
211
+ if ( $custom_fields ) {
212
+ $keys = '';
213
+
214
+ if ( is_array( $custom_fields ) ) {
215
+ if ( ! in_array( '_relevanssi_pdf_content', $custom_fields, true ) ) {
216
+ array_push( $custom_fields, '_relevanssi_pdf_content' );
217
+ }
218
+
219
+ if ( strpos( implode( ' ', $custom_fields ), '%' ) ) {
220
+ // ACF repeater fields involved.
221
+ $custom_fields_regexp = str_replace( '%', '.+', implode( '|', $custom_fields ) );
222
+ $keys = "AND m.meta_key REGEXP ('$custom_fields_regexp')";
223
+ } else {
224
+ $custom_fields_escaped = implode(
225
+ "','",
226
+ array_map(
227
+ 'esc_sql',
228
+ $custom_fields
229
+ )
230
+ );
231
+ $keys = "AND m.meta_key IN ('$custom_fields_escaped')";
232
+ }
233
+ }
234
+
235
+ if ( 'visible' === $custom_fields ) {
236
+ $keys = "AND (m.meta_key NOT LIKE '\_%' OR m.meta_key = '_relevanssi_pdf_content')";
237
+ }
238
+
239
+ $query = "(SELECT ID
240
+ FROM $wpdb->posts AS p, $wpdb->postmeta AS m
241
+ WHERE p.ID = m.post_id
242
+ $keys
243
+ AND m.meta_value LIKE '%$phrase%'
244
+ AND p.post_status IN ($status))";
245
+
246
+ $queries[] = $query;
247
+ } elseif ( RELEVANSSI_PREMIUM ) {
248
+ $index_post_types = get_option( 'relevanssi_index_post_types', array() );
249
+ if ( in_array( 'attachment', $index_post_types, true ) ) {
250
+ $query = "(SELECT ID
251
+ FROM $wpdb->posts AS p, $wpdb->postmeta AS m
252
+ WHERE p.ID = m.post_id
253
+ AND m.meta_key = '_relevanssi_pdf_content'
254
+ AND m.meta_value LIKE '%$phrase%'
255
+ AND p.post_status IN ($status))";
256
+
257
+ $queries[] = $query;
258
+ }
259
+ }
260
+
261
+ if ( 'on' === $index_pdf_parent ) {
262
+ $query = "(SELECT parent.ID
263
+ FROM $wpdb->posts AS p, $wpdb->postmeta AS m, $wpdb->posts AS parent
264
+ WHERE p.ID = m.post_id
265
+ AND p.post_parent = parent.ID
266
+ AND m.meta_key = '_relevanssi_pdf_content'
267
+ AND m.meta_value LIKE '%$phrase%'
268
+ AND p.post_status = 'inherit')";
269
+
270
+ $queries[] = $query;
271
+ }
272
+
273
+ $phrase_queries[ $phrase ] = $queries;
274
+ }
275
+
276
+ return $phrase_queries;
277
+ }
lib/search.php CHANGED
@@ -885,11 +885,12 @@ function relevanssi_do_query( &$query ) {
885
  * One of the key filters for Relevanssi. If you want to modify the results
886
  * Relevanssi finds, use this filter.
887
  *
888
- * @param array $filter_data The index 0 has an array of post objects found in
889
- * the search, index 1 has the search query string.
 
890
  *
891
- * @return array The return array composition is the same as the parameter array,
892
- * but Relevanssi only uses the index 0.
893
  */
894
  $hits_filters_applied = apply_filters( 'relevanssi_hits_filter', $filter_data );
895
  // array_values() to make sure the $hits array is indexed in numerical order
@@ -1003,6 +1004,8 @@ function relevanssi_do_query( &$query ) {
1003
  $query->posts = $posts;
1004
  $query->post_count = count( $posts );
1005
 
 
 
1006
  return $posts;
1007
  }
1008
 
885
  * One of the key filters for Relevanssi. If you want to modify the results
886
  * Relevanssi finds, use this filter.
887
  *
888
+ * @param array $filter_data The index 0 has an array of post objects (or
889
+ * post IDs, or parent=>ID pairs, depending on the `fields` parameter) found
890
+ * in the search, index 1 has the search query string.
891
  *
892
+ * @return array The return array composition is the same as the parameter
893
+ * array, but Relevanssi only uses the index 0.
894
  */
895
  $hits_filters_applied = apply_filters( 'relevanssi_hits_filter', $filter_data );
896
  // array_values() to make sure the $hits array is indexed in numerical order
1004
  $query->posts = $posts;
1005
  $query->post_count = count( $posts );
1006
 
1007
+ $relevanssi_active = false;
1008
+
1009
  return $posts;
1010
  }
1011
 
lib/stopwords.php CHANGED
@@ -40,9 +40,9 @@ function relevanssi_populate_stopwords( $verbose = false ) {
40
  return 'database';
41
  }
42
 
43
- $lang = get_locale();
44
  $stopword_file = $relevanssi_variables['plugin_dir']
45
- . 'stopwords/stopwords.' . $lang;
46
 
47
  if ( ! file_exists( $stopword_file ) ) {
48
  $verbose && printf(
@@ -53,7 +53,7 @@ function relevanssi_populate_stopwords( $verbose = false ) {
53
  "The stopword file for the language '%s' doesn't exist.",
54
  'relevanssi'
55
  ),
56
- esc_html( $lang )
57
  )
58
  );
59
  return 'no_file';
@@ -83,15 +83,19 @@ function relevanssi_populate_stopwords( $verbose = false ) {
83
  }
84
 
85
  /**
86
- * Fetches the list of stopwords.
87
  *
88
- * Gets the list of stopwords from the relevanssi_stopwords option.
 
89
  *
90
- * @return array An array of stopwords.
 
91
  */
92
  function relevanssi_fetch_stopwords() {
93
- $stopwords = get_option( 'relevanssi_stopwords', '' );
94
- $stopword_list = $stopwords ? explode( ',', $stopwords ) : array();
 
 
95
 
96
  return $stopword_list;
97
  }
@@ -182,27 +186,102 @@ function relevanssi_add_single_stopword( $term ) {
182
  return false;
183
  }
184
 
 
185
  $term = stripslashes( relevanssi_strtolower( $term ) );
186
- $stopwords = get_option( 'relevanssi_stopwords', '' );
187
 
188
- $stopwords_array = explode( ',', $stopwords );
189
- if ( in_array( $term, $stopwords_array, true ) ) {
190
  return false;
191
  }
192
 
193
- $stopwords_array[] = $term;
194
- $success = update_option(
195
- 'relevanssi_stopwords',
196
- implode( ',', array_filter( $stopwords_array ) )
197
- );
198
 
199
  if ( ! $success ) {
200
  return false;
201
  }
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  global $wpdb, $relevanssi_variables;
204
 
205
- // Remove from index.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  $wpdb->query(
207
  $wpdb->prepare(
208
  'DELETE FROM ' . $relevanssi_variables['relevanssi_table'] . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -210,21 +289,27 @@ function relevanssi_add_single_stopword( $term ) {
210
  $term
211
  )
212
  );
213
-
214
- return true;
215
  }
216
 
217
  /**
218
- * Removes all stopwords.
219
  *
220
- * Empties the relevanssi_stopwords option.
221
  *
222
- * @param boolean $verbose If true, print out notice. Default true.
 
 
223
  *
224
  * @return boolean True, if able to remove the options.
225
  */
226
- function relevanssi_remove_all_stopwords( $verbose = true ) {
227
- $success = update_option( 'relevanssi_stopwords', '' );
 
 
 
 
 
 
228
 
229
  $verbose && $success && printf(
230
  "<div id='message' class='updated fade'><p>%s</p></div>",
@@ -257,23 +342,16 @@ function relevanssi_remove_all_stopwords( $verbose = true ) {
257
  * @return boolean True if success, false if not.
258
  */
259
  function relevanssi_remove_stopword( $term, $verbose = true ) {
260
- $stopwords = get_option( 'relevanssi_stopwords', '' );
261
- $success = false;
262
-
263
- $term = stripslashes( $term );
264
-
265
- $stopwords_array = explode( ',', $stopwords );
266
- if ( is_array( $stopwords_array ) ) {
267
- $stopwords_array = array_filter(
268
- $stopwords_array,
269
- function( $stopword ) use ( $term ) {
270
- return $stopword !== $term;
271
- }
272
- );
273
 
274
- $stopwords = implode( ',', $stopwords_array );
275
- $success = update_option( 'relevanssi_stopwords', $stopwords );
276
- }
277
 
278
  $verbose && $success &&
279
  printf(
@@ -322,3 +400,16 @@ function relevanssi_remove_stopwords_from_array( $terms ) {
322
  $terms_without_stops = array_diff( $terms, $stopword_list );
323
  return $terms_without_stops;
324
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  return 'database';
41
  }
42
 
43
+ $language = relevanssi_get_current_language();
44
  $stopword_file = $relevanssi_variables['plugin_dir']
45
+ . 'stopwords/stopwords.' . $language;
46
 
47
  if ( ! file_exists( $stopword_file ) ) {
48
  $verbose && printf(
53
  "The stopword file for the language '%s' doesn't exist.",
54
  'relevanssi'
55
  ),
56
+ esc_html( $language )
57
  )
58
  );
59
  return 'no_file';
83
  }
84
 
85
  /**
86
+ * Fetches the list of stopwords in the current language.
87
  *
88
+ * Gets the list of stopwords from the relevanssi_stopwords option using the
89
+ * current language.
90
  *
91
+ * @return array An array of stopwords; if nothing is found, returns an empty
92
+ * array.
93
  */
94
  function relevanssi_fetch_stopwords() {
95
+ $current_language = relevanssi_get_current_language();
96
+ $stopwords_array = get_option( 'relevanssi_stopwords', array() );
97
+ $stopwords = isset( $stopwords_array[ $current_language ] ) ? $stopwords_array[ $current_language ] : '';
98
+ $stopword_list = $stopwords ? explode( ',', $stopwords ) : array();
99
 
100
  return $stopword_list;
101
  }
186
  return false;
187
  }
188
 
189
+ $stopwords = relevanssi_fetch_stopwords();
190
  $term = stripslashes( relevanssi_strtolower( $term ) );
 
191
 
192
+ if ( in_array( $term, $stopwords, true ) ) {
 
193
  return false;
194
  }
195
 
196
+ $stopwords[] = $term;
197
+
198
+ $success = relevanssi_update_stopwords( $stopwords );
 
 
199
 
200
  if ( ! $success ) {
201
  return false;
202
  }
203
 
204
+ relevanssi_delete_term_from_all_posts( $term );
205
+
206
+ return true;
207
+ }
208
+
209
+ /**
210
+ * Updates the current language stopwords in the stopwords option.
211
+ *
212
+ * Fetches the stopwords option, replaces the current language stopwords with
213
+ * the parameter array and updates the option.
214
+ *
215
+ * @param array $stopwords An array of stopwords.
216
+ *
217
+ * @return boolean The return value from update_option().
218
+ */
219
+ function relevanssi_update_stopwords( $stopwords ) {
220
+ $current_language = relevanssi_get_current_language();
221
+ $stopwords_option = get_option( 'relevanssi_stopwords', array() );
222
+
223
+ $stopwords_option[ $current_language ] = implode( ',', array_filter( $stopwords ) );
224
+ return update_option(
225
+ 'relevanssi_stopwords',
226
+ $stopwords_option
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Deletes a term from all posts in the database, language considered.
232
+ *
233
+ * If Polylang or WPML are used, deletes the term only from the posts matching
234
+ * the current language.
235
+ *
236
+ * @param string $term The term to delete.
237
+ */
238
+ function relevanssi_delete_term_from_all_posts( $term ) {
239
  global $wpdb, $relevanssi_variables;
240
 
241
+ if ( function_exists( 'pll_languages_list' ) ) {
242
+ $term_id = relevanssi_get_language_term_taxonomy_id(
243
+ relevanssi_get_current_language()
244
+ );
245
+
246
+ $wpdb->query(
247
+ $wpdb->prepare(
248
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
249
+ "DELETE FROM {$relevanssi_variables['relevanssi_table']}
250
+ WHERE term=%s
251
+ AND doc IN (
252
+ SELECT object_id
253
+ FROM $wpdb->term_relationships
254
+ WHERE term_taxonomy_id = %d
255
+ )",
256
+ $term,
257
+ $term_id
258
+ )
259
+ );
260
+
261
+ return;
262
+ }
263
+
264
+ if ( function_exists( 'icl_object_id' ) && ! function_exists( 'pll_is_translated_post_type' ) ) {
265
+ $language = relevanssi_get_current_language( false );
266
+ $wpdb->query(
267
+ $wpdb->prepare(
268
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
269
+ "DELETE FROM {$relevanssi_variables['relevanssi_table']}
270
+ WHERE term=%s
271
+ AND doc IN (
272
+ SELECT DISTINCT(element_id)
273
+ FROM {$wpdb->prefix}icl_translations
274
+ WHERE language_code = %s
275
+ )",
276
+ $term,
277
+ $language
278
+ )
279
+ );
280
+
281
+ return;
282
+ }
283
+
284
+ // No language defined, just remove from the index.
285
  $wpdb->query(
286
  $wpdb->prepare(
287
  'DELETE FROM ' . $relevanssi_variables['relevanssi_table'] . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
289
  $term
290
  )
291
  );
 
 
292
  }
293
 
294
  /**
295
+ * Removes all stopwords in specific language.
296
  *
297
+ * Empties the relevanssi_stopwords option for particular language.
298
  *
299
+ * @param boolean $verbose If true, print out notice. Default true.
300
+ * @param string $language The language code of stopwords. If empty, removes
301
+ * the stopwords for the current language.
302
  *
303
  * @return boolean True, if able to remove the options.
304
  */
305
+ function relevanssi_remove_all_stopwords( $verbose = true, $language = false ) {
306
+ if ( ! $language ) {
307
+ $language = relevanssi_get_current_language();
308
+ }
309
+
310
+ $stopwords = get_option( 'relevanssi_stopwords', array() );
311
+ unset( $stopwords[ $language ] );
312
+ $success = update_option( 'relevanssi_stopwords', $stopwords );
313
 
314
  $verbose && $success && printf(
315
  "<div id='message' class='updated fade'><p>%s</p></div>",
342
  * @return boolean True if success, false if not.
343
  */
344
  function relevanssi_remove_stopword( $term, $verbose = true ) {
345
+ $stopwords = relevanssi_fetch_stopwords();
346
+ $term = stripslashes( $term );
347
+ $stopwords = array_filter(
348
+ $stopwords,
349
+ function( $stopword ) use ( $term ) {
350
+ return $stopword !== $term;
351
+ }
352
+ );
 
 
 
 
 
353
 
354
+ $success = relevanssi_update_stopwords( $stopwords );
 
 
355
 
356
  $verbose && $success &&
357
  printf(
400
  $terms_without_stops = array_diff( $terms, $stopword_list );
401
  return $terms_without_stops;
402
  }
403
+
404
+ /**
405
+ * Updates the relevanssi_stopwords setting from a simple string to an array
406
+ * that is required for multilingual stopwords.
407
+ */
408
+ function relevanssi_update_stopwords_setting() {
409
+ $stopwords = get_option( 'relevanssi_stopwords' );
410
+
411
+ $current_language = relevanssi_get_current_language();
412
+
413
+ $array_stopwords[ $current_language ] = $stopwords;
414
+ update_option( 'relevanssi_stopwords', $array_stopwords );
415
+ }
lib/tabs/searching-tab.php CHANGED
@@ -393,10 +393,10 @@ function relevanssi_searching_tab() {
393
  </tr>
394
  <tr>
395
  <th scope="row">
396
- <label for='relevanssi_expst'><?php esc_html_e( 'Post exclusion', 'relevanssi' ); ?>
397
  </th>
398
  <td>
399
- <input type='text' name='relevanssi_expst' id='relevanssi_expst' size='60' value='<?php echo esc_attr( $exclude_posts ); ?>' />
400
  <p class="description"><?php esc_html_e( "Enter a comma-separated list of post or page ID's to exclude those pages from the search results.", 'relevanssi' ); ?></p>
401
  <?php if ( RELEVANSSI_PREMIUM ) { ?>
402
  <p class="description"><?php esc_html_e( "With Relevanssi Premium, it's better to use the check box on post edit pages. That will remove the posts completely from the index, and will work with multisite searches unlike this setting.", 'relevanssi' ); ?></p>
393
  </tr>
394
  <tr>
395
  <th scope="row">
396
+ <label for='relevanssi_exclude_posts'><?php esc_html_e( 'Post exclusion', 'relevanssi' ); ?>
397
  </th>
398
  <td>
399
+ <input type='text' name='relevanssi_exclude_posts' id='relevanssi_exclude_posts' size='60' value='<?php echo esc_attr( $exclude_posts ); ?>' />
400
  <p class="description"><?php esc_html_e( "Enter a comma-separated list of post or page ID's to exclude those pages from the search results.", 'relevanssi' ); ?></p>
401
  <?php if ( RELEVANSSI_PREMIUM ) { ?>
402
  <p class="description"><?php esc_html_e( "With Relevanssi Premium, it's better to use the check box on post edit pages. That will remove the posts completely from the index, and will work with multisite searches unlike this setting.", 'relevanssi' ); ?></p>
lib/tabs/stopwords-tab.php CHANGED
@@ -28,15 +28,21 @@ function relevanssi_stopwords_tab() {
28
  if ( function_exists( 'relevanssi_show_body_stopwords' ) ) {
29
  relevanssi_show_body_stopwords();
30
  } else {
31
- printf( '<p>%s</p>', esc_html__( 'Content stopwords are a premium feature where you can set stopwords that only apply to the post content. Those stopwords will still be indexed if they appear in post titles, tags, categories, custom fields or other parts of the post. To use content stopwords, you need Relevanssi Premium.', 'relevanssi' ) );
 
 
 
 
 
 
32
  }
33
 
34
  /**
35
  * Filters whether the common words list is displayed or not.
36
  *
37
- * The list of 25 most common words is displayed by default, but if the index is
38
- * big, displaying the list can take a long time. This filter can be used to
39
- * turn the list off.
40
  *
41
  * @param boolean If true, show the list; if false, don't show it.
42
  */
@@ -48,10 +54,17 @@ function relevanssi_stopwords_tab() {
48
  /**
49
  * Displays a list of stopwords.
50
  *
51
- * Displays the list of stopwords and gives the controls for adding new stopwords.
 
52
  */
53
  function relevanssi_show_stopwords() {
54
- printf( '<p>%s</p>', esc_html__( 'Enter a word here to add it to the list of stopwords. The word will automatically be removed from the index, so re-indexing is not necessary. You can enter many words at the same time, separate words with commas.', 'relevanssi' ) );
 
 
 
 
 
 
55
  ?>
56
  <table class="form-table" role="presentation">
57
  <tr>
@@ -74,17 +87,16 @@ function relevanssi_show_stopwords() {
74
  <td>
75
  <ul>
76
  <?php
77
- $stopword_list = get_option( 'relevanssi_stopwords', '' );
78
- $stopword_array = array_map( 'stripslashes', explode( ',', $stopword_list ) );
79
- sort( $stopword_array );
80
  array_walk(
81
- $stopword_array,
82
  function ( $term ) {
83
  printf( '<li style="display: inline;"><input type="submit" name="removestopword" value="%s"/></li>', esc_attr( $term ) );
84
  }
85
  );
86
 
87
- $exportlist = htmlspecialchars( str_replace( ',', ', ', $stopword_list ) );
88
  ?>
89
  </ul>
90
  <p>
28
  if ( function_exists( 'relevanssi_show_body_stopwords' ) ) {
29
  relevanssi_show_body_stopwords();
30
  } else {
31
+ printf(
32
+ '<p>%s</p>',
33
+ esc_html__(
34
+ 'Content stopwords are a premium feature where you can set stopwords that only apply to the post content. Those stopwords will still be indexed if they appear in post titles, tags, categories, custom fields or other parts of the post. To use content stopwords, you need Relevanssi Premium.',
35
+ 'relevanssi'
36
+ )
37
+ );
38
  }
39
 
40
  /**
41
  * Filters whether the common words list is displayed or not.
42
  *
43
+ * The list of 25 most common words is displayed by default, but if the
44
+ * index is big, displaying the list can take a long time. This filter can
45
+ * be used to turn the list off.
46
  *
47
  * @param boolean If true, show the list; if false, don't show it.
48
  */
54
  /**
55
  * Displays a list of stopwords.
56
  *
57
+ * Displays the list of stopwords and gives the controls for adding new
58
+ * stopwords.
59
  */
60
  function relevanssi_show_stopwords() {
61
+ printf(
62
+ '<p>%s</p>',
63
+ esc_html__(
64
+ 'Enter a word here to add it to the list of stopwords. The word will automatically be removed from the index, so re-indexing is not necessary. You can enter many words at the same time, separate words with commas.',
65
+ 'relevanssi'
66
+ )
67
+ );
68
  ?>
69
  <table class="form-table" role="presentation">
70
  <tr>
87
  <td>
88
  <ul>
89
  <?php
90
+ $stopwords = array_map( 'stripslashes', relevanssi_fetch_stopwords() );
91
+ sort( $stopwords );
92
+ $exportlist = htmlspecialchars( implode( ', ', $stopwords ) );
93
  array_walk(
94
+ $stopwords,
95
  function ( $term ) {
96
  printf( '<li style="display: inline;"><input type="submit" name="removestopword" value="%s"/></li>', esc_attr( $term ) );
97
  }
98
  );
99
 
 
100
  ?>
101
  </ul>
102
  <p>
lib/tabs/synonyms-tab.php CHANGED
@@ -14,7 +14,9 @@
14
  * Prints out the synonyms tab in Relevanssi settings.
15
  */
16
  function relevanssi_synonyms_tab() {
17
- $synonyms = get_option( 'relevanssi_synonyms' );
 
 
18
 
19
  if ( isset( $synonyms ) ) {
20
  $synonyms = str_replace( ';', "\n", $synonyms );
14
  * Prints out the synonyms tab in Relevanssi settings.
15
  */
16
  function relevanssi_synonyms_tab() {
17
+ $current_language = relevanssi_get_current_language();
18
+ $synonyms_array = get_option( 'relevanssi_synonyms', array() );
19
+ $synonyms = isset( $synonyms_array[ $current_language ] ) ? $synonyms_array[ $current_language ] : '';
20
 
21
  if ( isset( $synonyms ) ) {
22
  $synonyms = str_replace( ';', "\n", $synonyms );
lib/uninstall.php CHANGED
@@ -131,6 +131,7 @@ function relevanssi_uninstall_free() {
131
  delete_option( 'relevanssi_taxonomies_to_index' );
132
  delete_option( 'relevanssi_highlight_docs_external' );
133
  delete_option( 'relevanssi_word_boundaries' );
 
134
 
135
  global $wpdb;
136
  $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key = '_relevanssi_noindex_reason'" );
131
  delete_option( 'relevanssi_taxonomies_to_index' );
132
  delete_option( 'relevanssi_highlight_docs_external' );
133
  delete_option( 'relevanssi_word_boundaries' );
134
+ delete_option( 'relevanssi_expst' );
135
 
136
  global $wpdb;
137
  $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key = '_relevanssi_noindex_reason'" );
lib/utils.php ADDED
@@ -0,0 +1,867 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * /lib/utils.php
4
+ *
5
+ * @package Relevanssi
6
+ * @author Mikko Saari
7
+ * @license https://wordpress.org/about/gpl/ GNU General Public License
8
+ * @see https://www.relevanssi.com/
9
+ */
10
+
11
+ /**
12
+ * Returns a Relevanssi_Taxonomy_Walker instance.
13
+ *
14
+ * Requires the class file and generates a new Relevanssi_Taxonomy_Walker instance.
15
+ *
16
+ * @return object A new Relevanssi_Taxonomy_Walker instance.
17
+ */
18
+ function get_relevanssi_taxonomy_walker() {
19
+ require_once 'class-relevanssi-taxonomy-walker.php';
20
+ return new Relevanssi_Taxonomy_Walker();
21
+ }
22
+
23
+ /**
24
+ * Wraps the relevanssi_mb_trim() function so that it can be used as a callback
25
+ * for array_walk().
26
+ *
27
+ * @since 2.1.4
28
+ *
29
+ * @see relevanssi_mb_trim.
30
+ *
31
+ * @param string $string String to trim.
32
+ */
33
+ function relevanssi_array_walk_trim( &$string ) {
34
+ $string = relevanssi_mb_trim( $string );
35
+ }
36
+
37
+ /**
38
+ * Returns 'checked' if the option is enabled.
39
+ *
40
+ * @param string $option Value to check.
41
+ *
42
+ * @return string If the option is 'on', returns 'checked', otherwise returns an
43
+ * empty string.
44
+ */
45
+ function relevanssi_check( $option ) {
46
+ $checked = '';
47
+ if ( 'on' === $option ) {
48
+ $checked = 'checked';
49
+ }
50
+ return $checked;
51
+ }
52
+
53
+ /**
54
+ * Closes tags in a bit of HTML code.
55
+ *
56
+ * Used to make sure no tags are left open in excerpts. This method is not
57
+ * foolproof, but it's good enough for now.
58
+ *
59
+ * @param string $html The HTML code to analyze.
60
+ *
61
+ * @return string The HTML code, with tags closed.
62
+ */
63
+ function relevanssi_close_tags( $html ) {
64
+ $result = array();
65
+ preg_match_all(
66
+ '#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU',
67
+ $html,
68
+ $result
69
+ );
70
+ $opened_tags = $result[1];
71
+ preg_match_all( '#</([a-z]+)>#iU', $html, $result );
72
+ $closed_tags = $result[1];
73
+ $len_opened = count( $opened_tags );
74
+ if ( count( $closed_tags ) === $len_opened ) {
75
+ return $html;
76
+ }
77
+ $opened_tags = array_reverse( $opened_tags );
78
+ for ( $i = 0; $i < $len_opened; $i++ ) {
79
+ if ( ! in_array( $opened_tags[ $i ], $closed_tags, true ) ) {
80
+ $html .= '</' . $opened_tags[ $i ] . '>';
81
+ } else {
82
+ unset(
83
+ $closed_tags[ array_search( $opened_tags[ $i ], $closed_tags, true ) ]
84
+ );
85
+ }
86
+ }
87
+ return $html;
88
+ }
89
+
90
+ /**
91
+ * Prints out debugging notices.
92
+ *
93
+ * If WP_CLI is available, prints out the debug notice as a WP_CLI::log(),
94
+ * otherwise just echo.
95
+ *
96
+ * @param string $notice The notice to print out.
97
+ */
98
+ function relevanssi_debug_echo( $notice ) {
99
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
100
+ WP_CLI::log( $notice );
101
+ } else {
102
+ echo esc_html( $notice ) . "\n";
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Recursively flattens a multidimensional array to produce a string.
108
+ *
109
+ * @param array $array The source array.
110
+ *
111
+ * @return string The array contents as a string.
112
+ */
113
+ function relevanssi_flatten_array( array $array ) {
114
+ $return_value = '';
115
+ foreach ( new RecursiveIteratorIterator( new RecursiveArrayIterator( $array ) ) as $value ) {
116
+ $return_value .= ' ' . $value;
117
+ }
118
+ return trim( $return_value );
119
+ }
120
+
121
+ /**
122
+ * Generates closing tags for an array of tags.
123
+ *
124
+ * @param array $tags Array of tag names.
125
+ *
126
+ * @return array $closing_tags Array of closing tags.
127
+ */
128
+ function relevanssi_generate_closing_tags( $tags ) {
129
+ $closing_tags = array();
130
+ foreach ( $tags as $tag ) {
131
+ $a = str_replace( '<', '</', $tag );
132
+ $b = str_replace( '>', '/>', $tag );
133
+
134
+ $closing_tags[] = $a;
135
+ $closing_tags[] = $b;
136
+ }
137
+ return $closing_tags;
138
+ }
139
+
140
+ /**
141
+ * Returns the locale or language code.
142
+ *
143
+ * First checks `pll_current_language()`, then `wpml_current_language`, then
144
+ * falls back to `get_locale()`.
145
+ *
146
+ * @param boolean $locale If true, return locale; if false, return language
147
+ * code.
148
+ *
149
+ * @return string The locale for the current site language.
150
+ */
151
+ function relevanssi_get_current_language( $locale = true ) {
152
+ $current_language = get_locale();
153
+ if ( ! $locale ) {
154
+ $current_language = substr( $locale, 0, 2 );
155
+ }
156
+ if ( function_exists( 'pll_current_language' ) ) {
157
+ $current_language = pll_current_language( $locale ? 'locale' : 'slug' );
158
+ }
159
+ if ( function_exists( 'icl_object_id' ) && ! function_exists( 'pll_is_translated_post_type' ) ) {
160
+ if ( $locale ) {
161
+ $languages = apply_filters( 'wpml_active_languages', null );
162
+ foreach ( $languages as $l ) {
163
+ if ( $l['active'] ) {
164
+ $current_language = $l['default_locale'];
165
+ break;
166
+ }
167
+ }
168
+ } else {
169
+ $current_language = apply_filters( 'wpml_current_language', null );
170
+ }
171
+ }
172
+
173
+ return $current_language;
174
+ }
175
+
176
+ /**
177
+ * Gets the permalink to the current post within Loop.
178
+ *
179
+ * Uses get_permalink() to get the permalink, then adds the 'highlight'
180
+ * parameter if necessary using relevanssi_add_highlight().
181
+ *
182
+ * @return string The permalink.
183
+ */
184
+ function relevanssi_get_permalink() {
185
+ /**
186
+ * Filters the permalink.
187
+ *
188
+ * @param string The permalink, generated by get_permalink().
189
+ */
190
+ $permalink = apply_filters( 'relevanssi_permalink', get_permalink() );
191
+ return $permalink;
192
+ }
193
+
194
+ /**
195
+ * Replacement for get_post() that uses the Relevanssi post cache.
196
+ *
197
+ * Tries to fetch the post from the Relevanssi post cache. If that doesn't work,
198
+ * gets the post using get_post().
199
+ *
200
+ * @param int $post_id The post ID.
201
+ * @param int $blog_id The blog ID, default -1.
202
+ *
203
+ * @return object The post object.
204
+ */
205
+ function relevanssi_get_post( $post_id, $blog_id = -1 ) {
206
+ if ( function_exists( 'relevanssi_premium_get_post' ) ) {
207
+ return relevanssi_premium_get_post( $post_id, $blog_id );
208
+ }
209
+
210
+ global $relevanssi_post_array;
211
+
212
+ $post = null;
213
+ if ( isset( $relevanssi_post_array[ $post_id ] ) ) {
214
+ $post = $relevanssi_post_array[ $post_id ];
215
+ }
216
+ if ( ! $post ) {
217
+ $post = get_post( $post_id );
218
+
219
+ $relevanssi_post_array[ $post_id ] = $post;
220
+ }
221
+ return $post;
222
+ }
223
+
224
+ /**
225
+ * Returns the term taxonomy ID for a term based on term ID.
226
+ *
227
+ * @global object $wpdb The WordPress database interface.
228
+ *
229
+ * @param int $term_id The term ID.
230
+ * @param string $taxonomy The taxonomy.
231
+ *
232
+ * @return int Term taxonomy ID.
233
+ */
234
+ function relevanssi_get_term_tax_id( $term_id, $taxonomy ) {
235
+ global $wpdb;
236
+ return $wpdb->get_var(
237
+ $wpdb->prepare(
238
+ "SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id = %d AND taxonomy = %s",
239
+ $term_id,
240
+ $taxonomy
241
+ )
242
+ );
243
+ }
244
+
245
+ /**
246
+ * Fetches the taxonomy based on term ID.
247
+ *
248
+ * Fetches the taxonomy from wp_term_taxonomy based on term_id.
249
+ *
250
+ * @global object $wpdb The WordPress database interface.
251
+ * @param int $term_id The term ID.
252
+ * @deprecated Will be removed in future versions.
253
+ * @return string $taxonomy The term taxonomy.
254
+ */
255
+ function relevanssi_get_term_taxonomy( $term_id ) {
256
+ global $wpdb;
257
+
258
+ $taxonomy = $wpdb->get_var( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
259
+ return $taxonomy;
260
+ }
261
+
262
+ /**
263
+ * Gets a list of tags for post.
264
+ *
265
+ * Replacement for get_the_tags() that does the same, but applies Relevanssi
266
+ * search term highlighting on the results.
267
+ *
268
+ * @param string $before What is printed before the tags, default null.
269
+ * @param string $separator The separator between items, default ', '.
270
+ * @param string $after What is printed after the tags, default ''.
271
+ * @param int $post_id The post ID. Default current post ID (in the Loop).
272
+ */
273
+ function relevanssi_get_the_tags( $before = null, $separator = ', ', $after = '', $post_id = null ) {
274
+ return relevanssi_the_tags( $before, $separator, $after, false, $post_id );
275
+ }
276
+
277
+ /**
278
+ * Returns an imploded string if the option exists and is an array, an empty
279
+ * string otherwise.
280
+ *
281
+ * @param array $request An array of option values.
282
+ * @param string $option The key to check.
283
+ * @param string $glue The glue string for implode(), default ','.
284
+ *
285
+ * @return string Imploded string or an empty string.
286
+ */
287
+ function relevanssi_implode( $request, $option, $glue = ',' ) {
288
+ if ( isset( $request[ $option ] ) && is_array( $request[ $option ] ) ) {
289
+ return implode( $glue, $request[ $option ] );
290
+ }
291
+ return '';
292
+ }
293
+
294
+ /**
295
+ * Returns the intval of the option if it exists, null otherwise.
296
+ *
297
+ * @param array $request An array of option values.
298
+ * @param string $option The key to check.
299
+ *
300
+ * @return int|null Integer value of the option, or null.
301
+ */
302
+ function relevanssi_intval( $request, $option ) {
303
+ if ( isset( $request[ $option ] ) ) {
304
+ return intval( $request[ $option ] );
305
+ }
306
+ return null;
307
+ }
308
+
309
+ /**
310
+ * Launches an asynchronous Ajax action.
311
+ *
312
+ * Makes a wp_remote_post() call with the specific action. Handles nonce
313
+ * verification.
314
+ *
315
+ * @see wp_remove_post()
316
+ * @see wp_create_nonce()
317
+ *
318
+ * @param string $action The action to trigger (also the name of the
319
+ * nonce).
320
+ * @param array $payload_args The parameters sent to the action. Defaults to
321
+ * an empty array.
322
+ *
323
+ * @return WP_Error|array The wp_remote_post() response or WP_Error on failure.
324
+ */
325
+ function relevanssi_launch_ajax_action( $action, $payload_args = array() ) {
326
+ $cookies = array();
327
+ foreach ( $_COOKIE as $name => $value ) {
328
+ $cookies[] = "$name=" . rawurlencode(
329
+ is_array( $value ) ? wp_json_encode( $value ) : $value
330
+ );
331
+ }
332
+ $default_payload = array(
333
+ 'action' => $action,
334
+ '_nonce' => wp_create_nonce( $action ),
335
+ );
336
+ $payload = array_merge( $default_payload, $payload_args );
337
+ $args = array(
338
+ 'timeout' => 0.01,
339
+ 'blocking' => false,
340
+ 'body' => $payload,
341
+ 'headers' => array(
342
+ 'cookie' => implode( '; ', $cookies ),
343
+ ),
344
+ );
345
+ $url = admin_url( 'admin-ajax.php' );
346
+ return wp_remote_post( $url, $args );
347
+ }
348
+
349
+ /**
350
+ * Returns a legal value.
351
+ *
352
+ * @param array $request An array of option values.
353
+ * @param string $option The key to check.
354
+ * @param array $values The legal values.
355
+ * @param string $default The default value.
356
+ *
357
+ * @return string|null A legal value or the default value, null if the option
358
+ * isn't set.
359
+ */
360
+ function relevanssi_legal_value( $request, $option, $values, $default ) {
361
+ $value = null;
362
+ if ( isset( $request[ $option ] ) ) {
363
+ $value = $default;
364
+ if ( in_array( $request[ $option ], $values, true ) ) {
365
+ $value = $request[ $option ];
366
+ }
367
+ }
368
+ return $value;
369
+ }
370
+
371
+ /**
372
+ * Multibyte friendly case-insensitive string comparison.
373
+ *
374
+ * If multibyte string functions are available, do strcmp() after using
375
+ * mb_strtoupper() to both strings. Otherwise use strcasecmp().
376
+ *
377
+ * @param string $str1 First string to compare.
378
+ * @param string $str2 Second string to compare.
379
+ * @param string $encoding The encoding to use, default mb_internal_encoding().
380
+ *
381
+ * @return int $val Returns < 0 if str1 is less than str2; > 0 if str1 is
382
+ * greater than str2, and 0 if they are equal.
383
+ */
384
+ function relevanssi_mb_strcasecmp( $str1, $str2, $encoding = null ) {
385
+ if ( ! function_exists( 'mb_internal_encoding' ) ) {
386
+ return strnatcasecmp( $str1, $str2 );
387
+ } else {
388
+ if ( null === $encoding ) {
389
+ $encoding = mb_internal_encoding();
390
+ }
391
+ return strnatcmp( mb_strtoupper( $str1, $encoding ), mb_strtoupper( $str2, $encoding ) );
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Trims multibyte strings.
397
+ *
398
+ * Removes the 194+160 non-breakable spaces, removes null bytes and removes
399
+ * whitespace.
400
+ *
401
+ * @param string $string The source string.
402
+ *
403
+ * @return string Trimmed string.
404
+ */
405
+ function relevanssi_mb_trim( $string ) {
406
+ $string = str_replace( chr( 194 ) . chr( 160 ), '', $string );
407
+ $string = str_replace( "\0", '', $string );
408
+ $string = preg_replace( '/(^\s+)|(\s+$)/us', '', $string );
409
+ return $string;
410
+ }
411
+
412
+ /**
413
+ * Returns 'on' if option exists and value is not 'off', otherwise 'off'.
414
+ *
415
+ * @param array $request An array of option values.
416
+ * @param string $option The key to check.
417
+ *
418
+ * @return string 'on' or 'off'.
419
+ */
420
+ function relevanssi_off_or_on( $request, $option ) {
421
+ if ( isset( $request[ $option ] ) && 'off' !== $request[ $option ] ) {
422
+ return 'on';
423
+ }
424
+ return 'off';
425
+ }
426
+
427
+ /**
428
+ * Removes quotes (", ”, “) from a string.
429
+ *
430
+ * @param string $string The string to clean.
431
+ *
432
+ * @return string The cleaned string.
433
+ */
434
+ function relevanssi_remove_quotes( $string ) {
435
+ return str_replace( array( '”', '“', '"' ), '', $string );
436
+ }
437
+
438
+ /**
439
+ * Removes quotes from array keys. Does not keep array values.
440
+ *
441
+ * Used to remove phrase quotes from search term array, which have the format
442
+ * of (term => hits). The number of hits is not needed, so this function
443
+ * discards it as a side effect.
444
+ *
445
+ * @param array $array An array to process.
446
+ *
447
+ * @return array The same array with quotes removed from the keys.
448
+ */
449
+ function relevanssi_remove_quotes_from_array_keys( $array ) {
450
+ $array = array_keys( $array );
451
+ array_walk(
452
+ $array,
453
+ function( &$key ) {
454
+ $key = relevanssi_remove_quotes( $key );
455
+ }
456
+ );
457
+ return array_flip( $array );
458
+ }
459
+
460
+ /**
461
+ * Returns "off".
462
+ *
463
+ * Useful for returning "off" to filters easily.
464
+ *
465
+ * @return string A string with value "off".
466
+ */
467
+ function relevanssi_return_off() {
468
+ return 'off';
469
+ }
470
+
471
+ /**
472
+ * Sanitizes hex color strings.
473
+ *
474
+ * A copy of sanitize_hex_color(), because that isn't always available.
475
+ *
476
+ * @param string $color A hex color string to sanitize.
477
+ *
478
+ * @return string Sanitized hex string, or an empty string.
479
+ */
480
+ function relevanssi_sanitize_hex_color( $color ) {
481
+ if ( '' === $color ) {
482
+ return '';
483
+ }
484
+
485
+ if ( '#' !== substr( $color, 0, 1 ) ) {
486
+ $color = '#' . $color;
487
+ }
488
+
489
+ // 3 or 6 hex digits, or the empty string.
490
+ if ( preg_match( '|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) ) {
491
+ return $color;
492
+ }
493
+
494
+ return '';
495
+ }
496
+
497
+ /**
498
+ * Returns 'selected' if the option matches a value.
499
+ *
500
+ * @param string $option Value to check.
501
+ * @param string $value The 'selected' value.
502
+ *
503
+ * @return string If the option matches the value, returns 'selected', otherwise
504
+ * returns an empty string.
505
+ */
506
+ function relevanssi_select( $option, $value ) {
507
+ $selected = '';
508
+ if ( $option === $value ) {
509
+ $selected = 'selected';
510
+ }
511
+ return $selected;
512
+ }
513
+
514
+ /**
515
+ * Strips invisible elements from text.
516
+ *
517
+ * Strips <style>, <script>, <object>, <embed>, <applet>, <noscript>, <noembed>,
518
+ * <iframe>, and <del> tags and their contents from the text.
519
+ *
520
+ * @param string $text The source text.
521
+ *
522
+ * @return string The processed text.
523
+ */
524
+ function relevanssi_strip_invisibles( $text ) {
525
+ $text = preg_replace(
526
+ array(
527
+ '@<style[^>]*?>.*?</style>@siu',
528
+ '@<script[^>]*?.*?</script>@siu',
529
+ '@<object[^>]*?.*?</object>@siu',
530
+ '@<embed[^>]*?.*?</embed>@siu',
531
+ '@<applet[^>]*?.*?</applet>@siu',
532
+ '@<noscript[^>]*?.*?</noscript>@siu',
533
+ '@<noembed[^>]*?.*?</noembed>@siu',
534
+ '@<iframe[^>]*?.*?</iframe>@siu',
535
+ '@<del[^>]*?.*?</del>@siu',
536
+ ),
537
+ ' ',
538
+ $text
539
+ );
540
+ return $text;
541
+ }
542
+
543
+ /**
544
+ * Strips tags from contents, keeping the allowed tags.
545
+ *
546
+ * The allowable tags are read from the relevanssi_excerpt_allowable_tags
547
+ * option. Spaces are added between tags before removing the tags, so that
548
+ * words don't get stuck together. The function also remove invisible content.
549
+ *
550
+ * @see relevanssi_strip_invisibles
551
+ *
552
+ * @param string $content The content.
553
+ *
554
+ * @return string The content without tags.
555
+ */
556
+ function relevanssi_strip_tags( $content ) {
557
+ $content = relevanssi_strip_invisibles( $content );
558
+ $content = preg_replace( '/(<\/[^>]+?>)(<[^>\/][^>]*?>)/', '$1 $2', $content );
559
+ return strip_tags(
560
+ $content,
561
+ get_option( 'relevanssi_excerpt_allowable_tags', '' )
562
+ );
563
+ }
564
+
565
+ /**
566
+ * Returns the position of substring in the string.
567
+ *
568
+ * Uses mb_stripos() if possible, falls back to mb_strpos() and mb_strtoupper()
569
+ * if that cannot be found, and falls back to just strpos() if even that is not
570
+ * possible.
571
+ *
572
+ * @param string $haystack String where to look.
573
+ * @param string $needle The string to look for.
574
+ * @param int $offset Where to start, default 0.
575
+ *
576
+ * @return mixed False, if no result or $offset outside the length of $haystack,
577
+ * otherwise the position (which can be non-false 0!).
578
+ */
579
+ function relevanssi_stripos( $haystack, $needle, $offset = 0 ) {
580
+ if ( $offset > relevanssi_strlen( $haystack ) ) {
581
+ return false;
582
+ }
583
+
584
+ if ( preg_match( '/[\?\*]/', $needle ) ) {
585
+ // There's a ? or an * in the string, which means it's a wildcard search
586
+ // query (a Premium feature) and requires some extra steps.
587
+ $needle_regex = str_replace(
588
+ array( '?', '*' ),
589
+ array( '.', '.*' ),
590
+ $needle
591
+ );
592
+ $pos_found = false;
593
+ while ( ! $pos_found ) {
594
+ preg_match(
595
+ "/$needle_regex/i",
596
+ $haystack,
597
+ $matches,
598
+ PREG_OFFSET_CAPTURE,
599
+ $offset
600
+ );
601
+ /**
602
+ * This trickery is necessary, because PREG_OFFSET_CAPTURE gives
603
+ * wrong offsets for multibyte strings. The mb_strlen() gives the
604
+ * correct offset, the rest of this is because the $offset received
605
+ * as a parameter can be before the first $position, leading to an
606
+ * infinite loop.
607
+ */
608
+ $pos = isset( $matches[0][1] )
609
+ ? mb_strlen( substr( $haystack, 0, $matches[0][1] ) )
610
+ : false;
611
+ if ( $pos && $pos > $offset ) {
612
+ $pos_found = true;
613
+ } elseif ( $pos ) {
614
+ $offset++;
615
+ } else {
616
+ $pos_found = true;
617
+ }
618
+ }
619
+ } elseif ( function_exists( 'mb_stripos' ) ) {
620
+ if ( '' === $haystack ) {
621
+ $pos = false;
622
+ } else {
623
+ $pos = mb_stripos( $haystack, $needle, $offset );
624
+ }
625
+ } elseif ( function_exists( 'mb_strpos' ) && function_exists( 'mb_strtoupper' ) && function_exists( 'mb_substr' ) ) {
626
+ $pos = mb_strpos(
627
+ mb_strtoupper( $haystack ),
628
+ mb_strtoupper( $needle ),
629
+ $offset
630
+ );
631
+ } else {
632
+ $pos = strpos( strtoupper( $haystack ), strtoupper( $needle ), $offset );
633
+ }
634
+ return $pos;
635
+ }
636
+
637
+ /**
638
+ * Returns the length of the string.
639
+ *
640
+ * Uses mb_strlen() if available, otherwise falls back to strlen().
641
+ *
642
+ * @param string $s The string to measure.
643
+ *
644
+ * @return int The length of the string.
645
+ */
646
+ function relevanssi_strlen( $s ) {
647
+ if ( function_exists( 'mb_strlen' ) ) {
648
+ return mb_strlen( $s );
649
+ }
650
+ return strlen( $s );
651
+ }
652
+
653
+ /**
654
+ * Multibyte friendly strtolower.
655
+ *
656
+ * If multibyte string functions are available, returns mb_strtolower() and
657
+ * falls back to strtolower() if multibyte functions are not available.
658
+ *
659
+ * @param string $string The string to lowercase.
660
+ *
661
+ * @return string $string The string in lowercase.
662
+ */
663
+ function relevanssi_strtolower( $string ) {
664
+ if ( ! function_exists( 'mb_strtolower' ) ) {
665
+ return strtolower( $string );
666
+ } else {
667
+ return mb_strtolower( $string );
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Multibyte friendly substr.
673
+ *
674
+ * If multibyte string functions are available, returns mb_substr() and falls
675
+ * back to substr() if multibyte functions are not available.
676
+ *
677
+ * @param string $string The source string.
678
+ * @param int $start If start is non-negative, the returned string will
679
+ * start at the start'th position in str, counting from zero. If start is
680
+ * negative, the returned string will start at the start'th character from the
681
+ * end of string.
682
+ * @param int $length Maximum number of characters to use from string. If
683
+ * omitted or null is passed, extract all characters to the end of the string.
684
+ *
685
+ * @return string $string The string in lowercase.
686
+ */
687
+ function relevanssi_substr( $string, $start, $length = null ) {
688
+ if ( ! function_exists( 'mb_substr' ) ) {
689
+ return substr( $string, $start, $length );
690
+ } else {
691
+ return mb_substr( $string, $start, $length );
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Prints out the post excerpt.
697
+ *
698
+ * Prints out the post excerpt from $post->post_excerpt, unless the post is
699
+ * protected. Only works in the Loop.
700
+ *
701
+ * @global $post The global post object.
702
+ */
703
+ function relevanssi_the_excerpt() {
704
+ global $post;
705
+ if ( ! post_password_required( $post ) ) {
706
+ echo '<p>' . $post->post_excerpt . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
707
+ } else {
708
+ esc_html_e( 'There is no excerpt because this is a protected post.', 'relevanssi' );
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Echoes out the permalink to the current post within Loop.
714
+ *
715
+ * Uses get_permalink() to get the permalink, then adds the 'highlight'
716
+ * parameter if necessary using relevanssi_add_highlight(), then echoes it out.
717
+ */
718
+ function relevanssi_the_permalink() {
719
+ echo esc_url( relevanssi_get_permalink() );
720
+ }
721
+
722
+ /**
723
+ * Prints out a list of tags for post.
724
+ *
725
+ * Replacement for the_tags() that does the same, but applies Relevanssi search term
726
+ * highlighting on the results.
727
+ *
728
+ * @param string $before What is printed before the tags, default null.
729
+ * @param string $separator The separator between items, default ', '.
730
+ * @param string $after What is printed after the tags, default ''.
731
+ * @param boolean $echo If true, echo, otherwise return the result. Default true.
732
+ * @param int $post_id The post ID. Default current post ID (in the Loop).
733
+ */
734
+ function relevanssi_the_tags( $before = null, $separator = ', ', $after = '', $echo = true, $post_id = null ) {
735
+ $tag_list = get_the_tag_list( $before, $separator, $after, $post_id );
736
+ $found = preg_match_all( '~<a href=".*?" rel="tag">(.*?)</a>~', $tag_list, $matches );
737
+ if ( $found ) {
738
+ $originals = $matches[0];
739
+ $tag_names = $matches[1];
740
+ $highlighted = array();
741
+
742
+ $count = count( $matches[0] );
743
+ for ( $i = 0; $i < $count; $i++ ) {
744
+ $highlighted_tag_name = relevanssi_highlight_terms( $tag_names[ $i ], get_search_query(), true );
745
+ $highlighted[ $i ] = str_replace( '>' . $tag_names[ $i ] . '<', '>' . $highlighted_tag_name . '<', $originals[ $i ] );
746
+ }
747
+
748
+ $tag_list = str_replace( $originals, $highlighted, $tag_list );
749
+ }
750
+
751
+ if ( $echo ) {
752
+ echo $tag_list; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
753
+ } else {
754
+ return $tag_list;
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Turns off options, ie. sets them to "off".
760
+ *
761
+ * If the specified options don't exist in the request array, they are set to
762
+ * "off".
763
+ *
764
+ * @param array $request The _REQUEST array, passed as reference.
765
+ * @param array $options An array of option names.
766
+ */
767
+ function relevanssi_turn_off_options( &$request, $options ) {
768
+ array_walk(
769
+ $options,
770
+ function( $option ) use ( &$request ) {
771
+ if ( ! isset( $request[ $option ] ) ) {
772
+ $request[ $option ] = 'off';
773
+ }
774
+ }
775
+ );
776
+ }
777
+
778
+ /**
779
+ * Sets an option after doing floatval.
780
+ *
781
+ * @param array $request An array of option values.
782
+ * @param string $option The key to check.
783
+ * @param boolean $autoload Should the option autoload, default true.
784
+ * @param int $default The default value if floatval() fails, default 0.
785
+ * @param boolean $positive If true, replace negative values and zeroes with
786
+ * $default.
787
+ */
788
+ function relevanssi_update_floatval( $request, $option, $autoload = true, $default = 0, $positive = false ) {
789
+ if ( isset( $request[ $option ] ) ) {
790
+ $value = floatval( $request[ $option ] );
791
+ if ( ! $value ) {
792
+ $value = $default;
793
+ }
794
+ if ( $positive && $value <= 0 ) {
795
+ $value = $default;
796
+ }
797
+ update_option( $option, $value, $autoload );
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Sets an option after doing intval.
803
+ *
804
+ * @param array $request An array of option values.
805
+ * @param string $option The key to check.
806
+ * @param boolean $autoload Should the option autoload, default true.
807
+ * @param int $default The default value if intval() fails, default 0.
808
+ */
809
+ function relevanssi_update_intval( $request, $option, $autoload = true, $default = 0 ) {
810
+ if ( isset( $request[ $option ] ) ) {
811
+ $value = intval( $request[ $option ] );
812
+ if ( ! $value ) {
813
+ $value = $default;
814
+ }
815
+ update_option( $option, $value, $autoload );
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Sets an option with one of the listed legal values.
821
+ *
822
+ * @param array $request An array of option values.
823
+ * @param string $option The key to check.
824
+ * @param array $values The legal values.
825
+ * @param string $default The default value.
826
+ * @param boolean $autoload Should the option autoload, default true.
827
+ */
828
+ function relevanssi_update_legal_value( $request, $option, $values, $default, $autoload = true ) {
829
+ if ( isset( $request[ $option ] ) ) {
830
+ $value = $default;
831
+ if ( in_array( $request[ $option ], $values, true ) ) {
832
+ $value = $request[ $option ];
833
+ }
834
+ update_option( $option, $value, $autoload );
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Sets an on/off option according to the request value.
840
+ *
841
+ * @param array $request An array of option values.
842
+ * @param string $option The key to check.
843
+ * @param boolean $autoload Should the option autoload, default true.
844
+ */
845
+ function relevanssi_update_off_or_on( $request, $option, $autoload = true ) {
846
+ relevanssi_update_legal_value(
847
+ $request,
848
+ $option,
849
+ array( 'off', 'on' ),
850
+ 'off',
851
+ $autoload
852
+ );
853
+ }
854
+
855
+ /**
856
+ * Sets an option after sanitizing and unslashing the value.
857
+ *
858
+ * @param array $request An array of option values.
859
+ * @param string $option The key to check.
860
+ * @param boolean $autoload Should the option autoload, default true.
861
+ */
862
+ function relevanssi_update_sanitized( $request, $option, $autoload = true ) {
863
+ if ( isset( $request[ $option ] ) ) {
864
+ $value = sanitize_text_field( wp_unslash( $request[ $option ] ) );
865
+ update_option( $option, $value, $autoload );
866
+ }
867
+ }
readme.txt CHANGED
@@ -3,9 +3,9 @@ Contributors: msaari
3
  Donate link: https://www.relevanssi.com/buy-premium/
4
  Tags: search, relevance, better search, product search, woocommerce search
5
  Requires at least: 4.9
6
- Tested up to: 5.5.3
7
  Requires PHP: 7.0
8
- Stable tag: 4.9.1
9
  License: GPLv2 or later
10
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
11
 
@@ -133,6 +133,15 @@ Each document database is full of useless words. All the little words that appea
133
  * John Calahan for extensive 4.0 beta testing.
134
 
135
  == Changelog ==
 
 
 
 
 
 
 
 
 
136
  = 4.9.1 =
137
  * Changed behaviour: The `relevanssi_excerpt_part` filter hook now gets the post ID as a second parameter. The documentation for the filter has been fixed to match actual use: this filter is applied to the excerpt part after the highlighting and the ellipsis have been added.
138
  * Changed behaviour: The `relevanssi_index_custom_fields` filter hook is no longer used when determining which custom fields are used for phrase searching. If you have a use case where this change matters, please contact us.
@@ -189,27 +198,10 @@ Each document database is full of useless words. All the little words that appea
189
  * Minor fix: The doc count update, which is a heavy task, is now moved to an asynchronous action to avoid slowing down the site for users.
190
  * Minor fix: Relevanssi only updates doc count on `relevanssi_insert_edit()` when the post is indexed.
191
 
192
- = 4.7.2 =
193
- * Minor fix: Media Library searches failed if Relevanssi was enabled in the WP admin, but the `attachment` post type wasn't indexed. Relevanssi will no longer block the default Media Library search in these cases.
194
- * Minor fix: Adds more backwards compatibility for the `relevanssi_indexing_restriction` change, there's now an alert on indexing tab if there's a problem.
195
-
196
- = 4.7.1 =
197
- * New feature: New filter hook `relevanssi_post_content_after_shortcodes` filters the post content after shortcodes have been processed but before the HTML tags are stripped.
198
- * Minor fix: Adds more backwards compatibility for the `relevanssi_indexing_restriction` change.
199
-
200
- = 4.7.0 =
201
- * New feature: New filter hook `relevanssi_admin_search_blocked_post_types` makes it easy to block Relevanssi from searching a specific post type in the admin dashboard. There's built-in support for Reusable Content Blocks `rc_blocks` post type, for example.
202
- * New feature: The reason why a post is not indexed is now stored in the `_relevanssi_noindex_reason` custom field.
203
- * Changed behaviour: The `relevanssi_indexing_restriction` filter hook has a changed format. Instead of a string value, the filter now expects an array with the MySQL query in the index 'mysql' and a reason in string format in 'reason'. There's some temporary backwards compatibility for this.
204
- * Changed behaviour: Relevanssi now applies minimum word length when tokenizing search query terms.
205
- * Changed behaviour: Content stopwords are removed from the search queries when doing excerpts and highlights. When Relevanssi uses the untokenized search terms for excerpt-building, stopwords are removed from those words. This should lead to better excerpts.
206
- * Minor fix: Improves handling of emoji in indexing. If the database supports emoji, they are allowed, otherwise they are encoded.
207
-
208
- = 4.6.0 =
209
- * Changed behaviour: Phrases in OR search are now less restrictive. A search for 'foo "bar baz"' used to only return posts with the "bar baz" phrase, but now also posts with just the word 'foo' will be returned.
210
- * Minor fix: User Access Manager showed drafts in search results for all users. This is now fixed.
211
-
212
  == Upgrade notice ==
 
 
 
213
  = 4.9.1 =
214
  * Bug fixing, better Oxygen Builder compatibility.
215
 
@@ -226,16 +218,4 @@ Each document database is full of useless words. All the little words that appea
226
  * WooCommerce 4.4 compatibility, other minor fixes.
227
 
228
  = 4.8.0 =
229
- * Fixes a major bug in comment indexing, if you include comments in the index rebuild the index after updating.
230
-
231
- = 4.7.2 =
232
- * Improved backwards compatibility for the `relevanssi_indexing_restriction` filter hook change, better Media Library support.
233
-
234
- = 4.7.1 =
235
- * Improved backwards compatibility for the `relevanssi_indexing_restriction` filter hook change.
236
-
237
- = 4.7.0 =
238
- * The `relevanssi_indexing_restriction` filter hook has been changed, stopwords are handled in a different way in excerpts.
239
-
240
- = 4.6.0 =
241
- * Changes how phrases work in OR search and fixes a User Access Manager issue.
3
  Donate link: https://www.relevanssi.com/buy-premium/
4
  Tags: search, relevance, better search, product search, woocommerce search
5
  Requires at least: 4.9
6
+ Tested up to: 5.6.1
7
  Requires PHP: 7.0
8
+ Stable tag: 4.10.0
9
  License: GPLv2 or later
10
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
11
 
133
  * John Calahan for extensive 4.0 beta testing.
134
 
135
  == Changelog ==
136
+ = 4.10.0 =
137
+ * New feature: Relevanssi now supports multilingual synonyms and stopwords. Relevanssi now has a different set of synonyms and stopwords for each language. This feature is compatible with WPML and Polylang.
138
+ * New feature: SEO by Rank Math compatibility is added: posts marked as 'noindex' with Rank Math are not indexed by Relevanssi.
139
+ * Minor fix: With keyword matching set to 'whole words' and the 'expand highlights' disabled, words that ended with an 's' weren't highlighted correctly.
140
+ * Minor fix: The 'Post exclusion' setting didn't work correctly. It has been fixed.
141
+ * Minor fix: It's now impossible to set negative weights in searching settings. They did not work as expected anyway.
142
+ * Minor fix: Relevanssi had an unnecessary index on the `doc` column in the `wp_relevanssi` database table. It is now removed to save space. Thanks to Matthew Wang.
143
+ * Minor fix: Improved Oxygen Builder support makes sure `ct_builder_shortcodes` custom field is always indexed.
144
+
145
  = 4.9.1 =
146
  * Changed behaviour: The `relevanssi_excerpt_part` filter hook now gets the post ID as a second parameter. The documentation for the filter has been fixed to match actual use: this filter is applied to the excerpt part after the highlighting and the ellipsis have been added.
147
  * Changed behaviour: The `relevanssi_index_custom_fields` filter hook is no longer used when determining which custom fields are used for phrase searching. If you have a use case where this change matters, please contact us.
198
  * Minor fix: The doc count update, which is a heavy task, is now moved to an asynchronous action to avoid slowing down the site for users.
199
  * Minor fix: Relevanssi only updates doc count on `relevanssi_insert_edit()` when the post is indexed.
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  == Upgrade notice ==
202
+ = 4.10.0 =
203
+ * Adds support for multilingual stopwords and synonyms.
204
+
205
  = 4.9.1 =
206
  * Bug fixing, better Oxygen Builder compatibility.
207
 
218
  * WooCommerce 4.4 compatibility, other minor fixes.
219
 
220
  = 4.8.0 =
221
+ * Fixes a major bug in comment indexing, if you include comments in the index rebuild the index after updating.
 
 
 
 
 
 
 
 
 
 
 
 
relevanssi.php CHANGED
@@ -13,7 +13,7 @@
13
  * Plugin Name: Relevanssi
14
  * Plugin URI: https://www.relevanssi.com/
15
  * Description: This plugin replaces WordPress search with a relevance-sorting search.
16
- * Version: 4.9.1
17
  * Author: Mikko Saari
18
  * Author URI: http://www.mikkosaari.fi/
19
  * Text Domain: relevanssi
@@ -63,23 +63,28 @@ $relevanssi_variables['comment_boost_default'] = 0.75;
63
  $relevanssi_variables['post_type_weight_defaults']['post_tag'] = 0.75;
64
  $relevanssi_variables['post_type_weight_defaults']['category'] = 0.75;
65
  $relevanssi_variables['post_type_index_defaults'] = array( 'post', 'page' );
66
- $relevanssi_variables['database_version'] = 5;
67
  $relevanssi_variables['file'] = __FILE__;
68
  $relevanssi_variables['plugin_dir'] = plugin_dir_path( __FILE__ );
69
  $relevanssi_variables['plugin_basename'] = plugin_basename( __FILE__ );
70
- $relevanssi_variables['plugin_version'] = '4.9.1';
71
 
72
  require_once 'lib/admin-ajax.php';
73
  require_once 'lib/common.php';
 
74
  require_once 'lib/excerpts-highlights.php';
75
  require_once 'lib/indexing.php';
76
  require_once 'lib/init.php';
77
  require_once 'lib/install.php';
78
  require_once 'lib/interface.php';
79
  require_once 'lib/log.php';
 
 
 
80
  require_once 'lib/search.php';
81
  require_once 'lib/search-tax-query.php';
82
  require_once 'lib/search-query-restrictions.php';
83
  require_once 'lib/shortcodes.php';
84
  require_once 'lib/sorting.php';
85
  require_once 'lib/stopwords.php';
 
13
  * Plugin Name: Relevanssi
14
  * Plugin URI: https://www.relevanssi.com/
15
  * Description: This plugin replaces WordPress search with a relevance-sorting search.
16
+ * Version: 4.10.0
17
  * Author: Mikko Saari
18
  * Author URI: http://www.mikkosaari.fi/
19
  * Text Domain: relevanssi
63
  $relevanssi_variables['post_type_weight_defaults']['post_tag'] = 0.75;
64
  $relevanssi_variables['post_type_weight_defaults']['category'] = 0.75;
65
  $relevanssi_variables['post_type_index_defaults'] = array( 'post', 'page' );
66
+ $relevanssi_variables['database_version'] = 6;
67
  $relevanssi_variables['file'] = __FILE__;
68
  $relevanssi_variables['plugin_dir'] = plugin_dir_path( __FILE__ );
69
  $relevanssi_variables['plugin_basename'] = plugin_basename( __FILE__ );
70
+ $relevanssi_variables['plugin_version'] = '4.10.0';
71
 
72
  require_once 'lib/admin-ajax.php';
73
  require_once 'lib/common.php';
74
+ require_once 'lib/didyoumean.php';
75
  require_once 'lib/excerpts-highlights.php';
76
  require_once 'lib/indexing.php';
77
  require_once 'lib/init.php';
78
  require_once 'lib/install.php';
79
  require_once 'lib/interface.php';
80
  require_once 'lib/log.php';
81
+ require_once 'lib/options.php';
82
+ require_once 'lib/phrases.php';
83
+ require_once 'lib/privacy.php';
84
  require_once 'lib/search.php';
85
  require_once 'lib/search-tax-query.php';
86
  require_once 'lib/search-query-restrictions.php';
87
  require_once 'lib/shortcodes.php';
88
  require_once 'lib/sorting.php';
89
  require_once 'lib/stopwords.php';
90
+ require_once 'lib/utils.php';
stopwords/stopword.zh_TW ADDED
@@ -0,0 +1,769 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ $stopwords = array(
3
+ "$",
4
+ "0",
5
+ "1",
6
+ "2",
7
+ "3",
8
+ "4",
9
+ "5",
10
+ "6",
11
+ "7",
12
+ "8",
13
+ "9",
14
+ "?",
15
+ "_",
16
+ "“",
17
+ "”",
18
+ "、",
19
+ "。",
20
+ "《",
21
+ "》",
22
+ "一",
23
+ "一些",
24
+ "一何",
25
+ "一切",
26
+ "一則",
27
+ "一方面",
28
+ "一旦",
29
+ "一來",
30
+ "一樣",
31
+ "一般",
32
+ "一轉眼",
33
+ "萬一",
34
+ "上",
35
+ "上下",
36
+ "下",
37
+ "不",
38
+ "不僅",
39
+ "不但",
40
+ "不光",
41
+ "不單",
42
+ "不只",
43
+ "不外乎",
44
+ "不如",
45
+ "不妨",
46
+ "不盡",
47
+ "不盡然",
48
+ "不得",
49
+ "不怕",
50
+ "不惟",
51
+ "不成",
52
+ "不拘",
53
+ "不料",
54
+ "不是",
55
+ "不比",
56
+ "不然",
57
+ "不特",
58
+ "不獨",
59
+ "不管",
60
+ "不至於",
61
+ "不若",
62
+ "不論",
63
+ "不過",
64
+ "不問",
65
+ "與",
66
+ "與其",
67
+ "與其說",
68
+ "與否",
69
+ "與此同時",
70
+ "且",
71
+ "且不說",
72
+ "且說",
73
+ "兩者",
74
+ "個",
75
+ "個別",
76
+ "臨",
77
+ "為",
78
+ "為了",
79
+ "為什麼",
80
+ "為何",
81
+ "為止",
82
+ "為此",
83
+ "為著",
84
+ "乃",
85
+ "乃至",
86
+ "乃至於",
87
+ "麼",
88
+ "之",
89
+ "之一",
90
+ "之所以",
91
+ "之類",
92
+ "烏乎",
93
+ "乎",
94
+ "乘",
95
+ "也",
96
+ "也好",
97
+ "也罷",
98
+ "了",
99
+ "二來",
100
+ "於",
101
+ "於是",
102
+ "於是乎",
103
+ "云云",
104
+ "云爾",
105
+ "些",
106
+ "亦",
107
+ "人",
108
+ "人們",
109
+ "人家",
110
+ "什麼",
111
+ "什麼樣",
112
+ "今",
113
+ "介於",
114
+ "仍",
115
+ "仍舊",
116
+ "從",
117
+ "從此",
118
+ "從而",
119
+ "他",
120
+ "他人",
121
+ "他們",
122
+ "以",
123
+ "以上",
124
+ "以為",
125
+ "以便",
126
+ "以免",
127
+ "以及",
128
+ "以故",
129
+ "以期",
130
+ "以來",
131
+ "以至",
132
+ "以至於",
133
+ "以致",
134
+ "們",
135
+ "任",
136
+ "任何",
137
+ "任憑",
138
+ "似的",
139
+ "但",
140
+ "但凡",
141
+ "但是",
142
+ "何",
143
+ "何以",
144
+ "何況",
145
+ "何處",
146
+ "何時",
147
+ "余外",
148
+ "作為",
149
+ "你",
150
+ "你們",
151
+ "使",
152
+ "使得",
153
+ "例如",
154
+ "依",
155
+ "依據",
156
+ "依照",
157
+ "便於",
158
+ "俺",
159
+ "俺們",
160
+ "倘",
161
+ "倘使",
162
+ "倘或",
163
+ "倘然",
164
+ "倘若",
165
+ "借",
166
+ "假使",
167
+ "假如",
168
+ "假若",
169
+ "儻然",
170
+ "像",
171
+ "兒",
172
+ "先不先",
173
+ "光是",
174
+ "全體",
175
+ "全部",
176
+ "兮",
177
+ "關於",
178
+ "其",
179
+ "其一",
180
+ "其中",
181
+ "其二",
182
+ "其他",
183
+ "其餘",
184
+ "其它",
185
+ "其次",
186
+ "具體地說",
187
+ "具體說來",
188
+ "兼之",
189
+ "內",
190
+ "再",
191
+ "再其次",
192
+ "再則",
193
+ "再有",
194
+ "再者",
195
+ "再者說",
196
+ "再說",
197
+ "冒",
198
+ "沖",
199
+ "況且",
200
+ "幾",
201
+ "幾時",
202
+ "凡",
203
+ "凡是",
204
+ "憑",
205
+ "憑藉",
206
+ "出於",
207
+ "出來",
208
+ "分別",
209
+ "則",
210
+ "則甚",
211
+ "別",
212
+ "別人",
213
+ "別處",
214
+ "別是",
215
+ "別的",
216
+ "別管",
217
+ "別說",
218
+ "到",
219
+ "前後",
220
+ "前此",
221
+ "前者",
222
+ "加之",
223
+ "加以",
224
+ "即",
225
+ "即令",
226
+ "即使",
227
+ "即便",
228
+ "即如",
229
+ "即或",
230
+ "即若",
231
+ "卻",
232
+ "去",
233
+ "又",
234
+ "又及",
235
+ "及",
236
+ "及其",
237
+ "及至",
238
+ "反之",
239
+ "反而",
240
+ "反過來",
241
+ "反過來說",
242
+ "受到",
243
+ "另",
244
+ "另一方面",
245
+ "另外",
246
+ "另悉",
247
+ "只",
248
+ "只當",
249
+ "只怕",
250
+ "只是",
251
+ "只有",
252
+ "只消",
253
+ "只要",
254
+ "只限",
255
+ "叫",
256
+ "叮咚",
257
+ "可",
258
+ "可以",
259
+ "可是",
260
+ "可見",
261
+ "各",
262
+ "各個",
263
+ "各位",
264
+ "各種",
265
+ "各自",
266
+ "同",
267
+ "同時",
268
+ "後",
269
+ "後者",
270
+ "向",
271
+ "向使",
272
+ "向著",
273
+ "嚇",
274
+ "嗎",
275
+ "否則",
276
+ "吧",
277
+ "吧噠",
278
+ "吱",
279
+ "呀",
280
+ "呃",
281
+ "嘔",
282
+ "唄",
283
+ "嗚",
284
+ "嗚呼",
285
+ "呢",
286
+ "呵",
287
+ "呵呵",
288
+ "呸",
289
+ "呼哧",
290
+ "咋",
291
+ "和",
292
+ "咚",
293
+ "咦",
294
+ "咧",
295
+ "咱",
296
+ "咱們",
297
+ "咳",
298
+ "哇",
299
+ "哈",
300
+ "哈哈",
301
+ "哉",
302
+ "哎",
303
+ "哎呀",
304
+ "哎喲",
305
+ "嘩",
306
+ "喲",
307
+ "哦",
308
+ "哩",
309
+ "哪",
310
+ "哪個",
311
+ "哪些",
312
+ "哪兒",
313
+ "哪天",
314
+ "哪年",
315
+ "哪怕",
316
+ "哪樣",
317
+ "哪邊",
318
+ "哪裡",
319
+ "哼",
320
+ "哼唷",
321
+ "唉",
322
+ "唯有",
323
+ "啊",
324
+ "啐",
325
+ "啥",
326
+ "啦",
327
+ "啪達",
328
+ "啷噹",
329
+ "喂",
330
+ "喏",
331
+ "喔唷",
332
+ "嘍",
333
+ "嗡",
334
+ "嗡嗡",
335
+ "嗬",
336
+ "嗯",
337
+ "噯",
338
+ "嘎",
339
+ "嘎登",
340
+ "噓",
341
+ "嘛",
342
+ "嘻",
343
+ "嘿",
344
+ "嘿嘿",
345
+ "因",
346
+ "因為",
347
+ "因了",
348
+ "因此",
349
+ "因著",
350
+ "因而",
351
+ "固然",
352
+ "在",
353
+ "在下",
354
+ "在於",
355
+ "地",
356
+ "基於",
357
+ "處在",
358
+ "多",
359
+ "多麼",
360
+ "多少",
361
+ "大",
362
+ "大家",
363
+ "她",
364
+ "她們",
365
+ "好",
366
+ "如",
367
+ "如上",
368
+ "如上所述",
369
+ "如下",
370
+ "如何",
371
+ "如其",
372
+ "如同",
373
+ "如是",
374
+ "如果",
375
+ "如此",
376
+ "如若",
377
+ "始而",
378
+ "孰料",
379
+ "孰知",
380
+ "寧",
381
+ "寧可",
382
+ "寧願",
383
+ "寧肯",
384
+ "它",
385
+ "它們",
386
+ "對",
387
+ "對於",
388
+ "對待",
389
+ "對方",
390
+ "對比",
391
+ "將",
392
+ "小",
393
+ "爾",
394
+ "爾後",
395
+ "爾爾",
396
+ "尚且",
397
+ "就",
398
+ "就是",
399
+ "就是了",
400
+ "就是說",
401
+ "就算",
402
+ "就要",
403
+ "盡",
404
+ "儘管",
405
+ "儘管如此",
406
+ "豈但",
407
+ "己",
408
+ "已",
409
+ "已矣",
410
+ "巴",
411
+ "巴巴",
412
+ "並",
413
+ "並且",
414
+ "並非",
415
+ "庶乎",
416
+ "庶幾",
417
+ "開外",
418
+ "開始",
419
+ "歸",
420
+ "歸齊",
421
+ "當",
422
+ "當地",
423
+ "當然",
424
+ "當著",
425
+ "彼",
426
+ "彼時",
427
+ "彼此",
428
+ "往",
429
+ "待",
430
+ "很",
431
+ "得",
432
+ "得了",
433
+ "怎",
434
+ "怎麼",
435
+ "怎麼辦",
436
+ "怎麼樣",
437
+ "怎奈",
438
+ "怎樣",
439
+ "總之",
440
+ "總的來看",
441
+ "總的來說",
442
+ "總的說來",
443
+ "總而言之",
444
+ "恰恰相反",
445
+ "您",
446
+ "惟其",
447
+ "慢說",
448
+ "我",
449
+ "我們",
450
+ "或",
451
+ "或則",
452
+ "或是",
453
+ "或曰",
454
+ "或者",
455
+ "截至",
456
+ "所",
457
+ "所以",
458
+ "所在",
459
+ "所幸",
460
+ "所有",
461
+ "才",
462
+ "才能",
463
+ "打",
464
+ "打從",
465
+ "把",
466
+ "抑或",
467
+ "拿",
468
+ "按",
469
+ "按照",
470
+ "換句話說",
471
+ "換言之",
472
+ "據",
473
+ "據此",
474
+ "接著",
475
+ "故",
476
+ "故此",
477
+ "故而",
478
+ "旁人",
479
+ "無",
480
+ "無寧",
481
+ "無論",
482
+ "既",
483
+ "既往",
484
+ "既是",
485
+ "既然",
486
+ "時候",
487
+ "是",
488
+ "是以",
489
+ "是的",
490
+ "曾",
491
+ "替",
492
+ "替代",
493
+ "最",
494
+ "有",
495
+ "有些",
496
+ "有關",
497
+ "有及",
498
+ "有時",
499
+ "有的",
500
+ "望",
501
+ "朝",
502
+ "朝著",
503
+ "本",
504
+ "本人",
505
+ "本地",
506
+ "本著",
507
+ "本身",
508
+ "來",
509
+ "來著",
510
+ "來自",
511
+ "來說",
512
+ "極了",
513
+ "果然",
514
+ "果真",
515
+ "某",
516
+ "某個",
517
+ "某些",
518
+ "某某",
519
+ "根據",
520
+ "歟",
521
+ "正值",
522
+ "正如",
523
+ "正巧",
524
+ "正是",
525
+ "此",
526
+ "此地",
527
+ "此處",
528
+ "此外",
529
+ "此時",
530
+ "此次",
531
+ "此間",
532
+ "毋寧",
533
+ "每",
534
+ "每當",
535
+ "比",
536
+ "比及",
537
+ "比如",
538
+ "比方",
539
+ "沒奈何",
540
+ "沿",
541
+ "沿著",
542
+ "漫說",
543
+ "焉",
544
+ "然則",
545
+ "然後",
546
+ "然而",
547
+ "照",
548
+ "照著",
549
+ "猶且",
550
+ "猶自",
551
+ "甚且",
552
+ "甚麼",
553
+ "甚或",
554
+ "甚而",
555
+ "甚至",
556
+ "甚至於",
557
+ "用",
558
+ "用來",
559
+ "由",
560
+ "由於",
561
+ "由是",
562
+ "由此",
563
+ "由此可見",
564
+ "的",
565
+ "的確",
566
+ "的話",
567
+ "直到",
568
+ "相對而言",
569
+ "省得",
570
+ "看",
571
+ "眨眼",
572
+ "著",
573
+ "著呢",
574
+ "矣",
575
+ "矣乎",
576
+ "矣哉",
577
+ "離",
578
+ "竟而",
579
+ "第",
580
+ "等",
581
+ "等到",
582
+ "等等",
583
+ "簡言之",
584
+ "管",
585
+ "類如",
586
+ "緊接著",
587
+ "縱",
588
+ "縱令",
589
+ "縱使",
590
+ "縱然",
591
+ "經",
592
+ "經過",
593
+ "結果",
594
+ "給",
595
+ "繼之",
596
+ "繼後",
597
+ "繼而",
598
+ "綜上所述",
599
+ "罷了",
600
+ "者",
601
+ "而",
602
+ "而且",
603
+ "而況",
604
+ "而後",
605
+ "而外",
606
+ "而已",
607
+ "而是",
608
+ "而言",
609
+ "能",
610
+ "能否",
611
+ "騰",
612
+ "自",
613
+ "自個兒",
614
+ "自從",
615
+ "自各兒",
616
+ "自後",
617
+ "自家",
618
+ "自己",
619
+ "自打",
620
+ "自身",
621
+ "至",
622
+ "至於",
623
+ "至今",
624
+ "至若",
625
+ "致",
626
+ "般的",
627
+ "若",
628
+ "若夫",
629
+ "若是",
630
+ "若果",
631
+ "若非",
632
+ "莫不然",
633
+ "莫如",
634
+ "莫若",
635
+ "雖",
636
+ "雖則",
637
+ "雖然",
638
+ "雖說",
639
+ "被",
640
+ "要",
641
+ "要不",
642
+ "要不是",
643
+ "要不然",
644
+ "要麼",
645
+ "要是",
646
+ "譬喻",
647
+ "譬如",
648
+ "讓",
649
+ "許多",
650
+ "論",
651
+ "設使",
652
+ "設或",
653
+ "設若",
654
+ "誠如",
655
+ "誠然",
656
+ "該",
657
+ "說來",
658
+ "諸",
659
+ "諸位",
660
+ "諸如",
661
+ "誰",
662
+ "誰人",
663
+ "誰料",
664
+ "誰知",
665
+ "賊死",
666
+ "賴以",
667
+ "趕",
668
+ "起",
669
+ "起見",
670
+ "趁",
671
+ "趁著",
672
+ "越是",
673
+ "距",
674
+ "跟",
675
+ "較",
676
+ "較之",
677
+ "邊",
678
+ "過",
679
+ "還",
680
+ "還是",
681
+ "還有",
682
+ "還要",
683
+ "這",
684
+ "這一來",
685
+ "這個",
686
+ "這麼",
687
+ "這麼些",
688
+ "這麼樣",
689
+ "這麼點兒",
690
+ "這些",
691
+ "這會兒",
692
+ "這兒",
693
+ "這就是說",
694
+ "這時",
695
+ "這樣",
696
+ "這次",
697
+ "這般",
698
+ "這邊",
699
+ "這裡",
700
+ "進而",
701
+ "連",
702
+ "連同",
703
+ "逐步",
704
+ "通過",
705
+ "遵循",
706
+ "遵照",
707
+ "那",
708
+ "那個",
709
+ "那麼",
710
+ "那麼些",
711
+ "那麼樣",
712
+ "那些",
713
+ "那會兒",
714
+ "那兒",
715
+ "那時",
716
+ "那樣",
717
+ "那般",
718
+ "那邊",
719
+ "那裡",
720
+ "都",
721
+ "鄙人",
722
+ "鑒於",
723
+ "針對",
724
+ "阿",
725
+ "除",
726
+ "除了",
727
+ "除外",
728
+ "除開",
729
+ "除此之外",
730
+ "除非",
731
+ "隨",
732
+ "隨後",
733
+ "隨時",
734
+ "隨著",
735
+ "難道說",
736
+ "非但",
737
+ "非徒",
738
+ "非特",
739
+ "非獨",
740
+ "靠",
741
+ "順",
742
+ "順著",
743
+ "首先",
744
+ "!",
745
+ ",",
746
+ ":",
747
+ ";",
748
+ "?",
749
+ "「",
750
+ "」",
751
+ "(",
752
+ ")",
753
+ "可能",
754
+ "閱讀",
755
+ "延伸",
756
+ "表示",
757
+ "已經",
758
+ "沒有",
759
+ "其實",
760
+ "容易",
761
+ "時間",
762
+ "需要",
763
+ "芊淩",
764
+ "很多",
765
+ "不會",
766
+ "像是",
767
+ "近幾年",
768
+ "注意");
769
+ ?>