Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
HighlightingTrait
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
5 / 5
11
100.00% covered (success)
100.00%
1 / 1
 escapeHighlightedText
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 findRedirectTitle
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 containsMatches
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stripHighlighting
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findSectionTitle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTitleHelper
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3namespace CirrusSearch\Search\Fetch;
4
5use CirrusSearch\LogChannel;
6use CirrusSearch\Search\TitleHelper;
7use CirrusSearch\Searcher;
8use MediaWiki\Logger\LoggerFactory;
9use MediaWiki\Title\Title;
10
11trait HighlightingTrait {
12    /**
13     * Escape highlighted text coming back from Elasticsearch.
14     *
15     * @param string $snippet highlighted snippet returned from elasticsearch
16     * @return string $snippet with html escaped _except_ highlighting pre and post tags
17     */
18    protected function escapeHighlightedText( $snippet ) {
19        /**
20         * \p{M} matches any combining Unicode character
21         * \P{M} matches any non-combining Unicode character
22         *
23         * For HIGHLIGHT_PRE_MARKER, move the marker earlier if it occurs before a
24         * combining character, and there is a non-combining character (and zero
25         * or more combining characters) directly before it.
26         *
27         * For HIGHLIGHT_POST_MARKER, move the marker later if it occurs before
28         * one or more combining characters.
29         */
30        $snippet = preg_replace( '/(\P{M}\p{M}*)(' . Searcher::HIGHLIGHT_PRE_MARKER .
31                                 ')(\p{M}+)/u', '$2$1$3', $snippet );
32        $snippet = preg_replace( '/(' . Searcher::HIGHLIGHT_POST_MARKER . ')(\p{M}+)/u',
33            '$2$1', $snippet );
34        return strtr( htmlspecialchars( $snippet ), [
35            Searcher::HIGHLIGHT_PRE_MARKER => Searcher::HIGHLIGHT_PRE,
36            Searcher::HIGHLIGHT_POST_MARKER => Searcher::HIGHLIGHT_POST
37        ] );
38    }
39
40    /**
41     * Build the redirect title from the highlighted redirect snippet.
42     *
43     * @param \Elastica\Result $result
44     * @param string $snippet Highlighted redirect snippet
45     * @return Title|null object representing the redirect
46     */
47    protected function findRedirectTitle( \Elastica\Result $result, $snippet ) {
48        $title = $this->stripHighlighting( $snippet );
49        // Grab the redirect that matches the highlighted title with the lowest namespace.
50        $redirects = $result->redirect;
51        // That is pretty arbitrary but it prioritizes 0 over others.
52        $best = null;
53        if ( $redirects !== null ) {
54            foreach ( $redirects as $redirect ) {
55                if ( $redirect[ 'title' ] === $title && ( $best === null || $best[ 'namespace' ] > $redirect['namespace'] ) ) {
56                    $best = $redirect;
57                }
58            }
59        }
60        if ( $best === null ) {
61            LoggerFactory::getInstance( LogChannel::DEFAULT )->warning(
62                "Search backend highlighted a redirect ({title}) but didn't return it.",
63                [ 'title' => $title ]
64            );
65            return null;
66        }
67        return $this->getTitleHelper()->makeRedirectTitle( $result, $best['title'], $best['namespace'] );
68    }
69
70    /**
71     * Checks if a snippet contains matches by looking for HIGHLIGHT_PRE.
72     *
73     * @param string $snippet highlighted snippet returned from elasticsearch
74     * @return bool true if $snippet contains matches, false otherwise
75     */
76    protected function containsMatches( $snippet ) {
77        return strpos( $snippet, Searcher::HIGHLIGHT_PRE_MARKER ) !== false;
78    }
79
80    /**
81     * @param string $highlighted
82     * @return string
83     */
84    protected function stripHighlighting( $highlighted ) {
85        $markers = [ Searcher::HIGHLIGHT_PRE_MARKER, Searcher::HIGHLIGHT_POST_MARKER ];
86        return str_replace( $markers, '', $highlighted );
87    }
88
89    /**
90     * @param string $highlighted
91     * @param Title $title
92     * @return Title
93     */
94    protected function findSectionTitle( $highlighted, Title $title ) {
95        return $title->createFragmentTarget( $this->getTitleHelper()->sanitizeSectionFragment(
96            $this->stripHighlighting( $highlighted )
97        ) );
98    }
99
100    /**
101     * @return TitleHelper
102     */
103    abstract protected function getTitleHelper(): TitleHelper;
104}