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