Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntityResult
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 14
870
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
56
 getHighlightOrField
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getSourceField
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 processHighlighting
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getLabelData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescriptionData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLabelHighlightedData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescriptionHighlightedData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTextSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtraDisplay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatementCount
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSitelinkCount
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getExtensionData
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2namespace Wikibase\Search\Elastic;
3
4use CirrusSearch\Search\Result;
5use CirrusSearch\Searcher;
6use HtmlArmor;
7use Wikibase\Lib\TermLanguageFallbackChain;
8use Wikibase\Repo\Search\ExtendedResult;
9
10/**
11 * Single result for entity search.
12 */
13class EntityResult extends Result implements ExtendedResult {
14    /**
15     * Key which holds wikibase data for result extra data.
16     */
17    private const WIKIBASE_EXTRA_DATA = 'wikibase';
18
19    /**
20     * Label data with highlighting.
21     * ['language' => LANG, 'value' => TEXT]
22     * @var string[]
23     */
24    private $labelHighlightedData;
25    /**
26     * Raw label data from source.
27     * ['language' => LANG, 'value' => TEXT]
28     * @var string[]
29     */
30    private $labelData;
31    /**
32     * Description data with highlighting.
33     * ['language' => LANG, 'value' => TEXT]
34     * @var string[]
35     */
36    private $descriptionData;
37    /**
38     * Description data from source.
39     * ['language' => LANG, 'value' => TEXT]
40     * @var string[]
41     */
42    private $descriptionHighlightedData;
43
44    /**
45     * Did we capture actual match in display of
46     * label or description?
47     * @var bool
48     */
49    private $haveMatch;
50    /**
51     * Extra display field for match.
52     * ['language' => LANG, 'value' => TEXT]
53     * @var array
54     */
55    private $extraDisplay;
56    /**
57     * Display language
58     * @var string
59     */
60    private $displayLanguage;
61    /**
62     * Original source data
63     * @var array
64     */
65    private $sourceData;
66
67    /**
68     * @param string $displayLanguage
69     * @param TermLanguageFallbackChain $displayFallbackChain
70     * @param \Elastica\Result $result
71     */
72    public function __construct( $displayLanguage, TermLanguageFallbackChain $displayFallbackChain,
73                                    $result ) {
74        // Let Cirrus\Result class handle the boring stuff
75        parent::__construct( null, $result );
76        // FIXME: null is not nice, but Result doesn't really need it...
77        // Think how to fix this.
78        $this->displayLanguage = $displayLanguage;
79
80        $this->sourceData = $result->getSource();
81        $highlightData = $result->getHighlights();
82
83        // If our highlight hit is on alias, we have to put real label into the field,
84        // and alias in extra
85        $isAlias = false;
86        if ( !empty( $highlightData["labels.{$displayLanguage}.plain"] ) ) {
87            $hlLabel = $highlightData["labels.{$displayLanguage}.plain"][0];
88            if ( preg_match( ElasticTermResult::HIGHLIGHT_PATTERN, $hlLabel, $match ) ) {
89                $isAlias = ( $hlLabel[0] !== '0' );
90            }
91        }
92
93        $this->labelData = $this->getSourceField(
94            'labels', $displayLanguage, $displayFallbackChain, $this->sourceData );
95        if ( $isAlias ) {
96            // We have matched alias for highlighting, so we will show regular label and
97            // alias as extra data
98            $this->labelHighlightedData = $this->labelData;
99            $this->extraDisplay = $this->getHighlightOrField( 'labels', $displayLanguage,
100                $displayFallbackChain, $highlightData, $this->sourceData, true );
101            // Since isAlias can be true only if labels is in highlight data, the above will
102            // also set $this->haveMatch
103        } else {
104            $this->labelHighlightedData = $this->getHighlightOrField( 'labels', $displayLanguage,
105                $displayFallbackChain, $highlightData, $this->sourceData, true );
106        }
107
108        $this->descriptionHighlightedData = $this->getHighlightOrField(
109            'descriptions', $displayLanguage, $displayFallbackChain,
110            $highlightData, $this->sourceData );
111        $this->descriptionData = $this->getSourceField(
112            'descriptions', $displayLanguage, $displayFallbackChain, $this->sourceData );
113
114        if ( !$this->haveMatch ) {
115            reset( $highlightData );
116            $key = key( $highlightData );
117            if ( $key && preg_match( '/^(\w+)\.([^.]+)\.plain$/', $key, $match ) ) {
118                $this->extraDisplay = [
119                    'language' => $match[2],
120                    // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
121                    'value' => new HtmlArmor( $this->processHighlighting( $highlightData[$key][0],
122                        $match[1] === 'labels' ) )
123                ];
124            }
125        }
126    }
127
128    /**
129     * Extract field value from highlighting or source data.
130     * @param string $field
131     * @param string $displayLanguage
132     * @param TermLanguageFallbackChain $displayFallbackChain
133     * @param array $highlightData
134     * @param array $sourceData
135     * @param bool $useOffsets Is highlighter using offsets option?
136     * @return array [ $language, $text ] or [null, null] if nothing found
137     */
138    private function getHighlightOrField( $field, $displayLanguage,
139            TermLanguageFallbackChain $displayFallbackChain,
140            $highlightData, $sourceData, $useOffsets = false
141    ) {
142        // Try highlights first, if we have needed language there, use highlighted data
143        if ( !empty( $highlightData["{$field}.{$displayLanguage}.plain"] ) ) {
144            $this->haveMatch = true;
145            return [
146                'language' => $displayLanguage,
147                'value' => new HtmlArmor( $this->processHighlighting( $highlightData["{$field}.{$displayLanguage}.plain"][0],
148                    $useOffsets ) )
149            ];
150        }
151        // If that failed, try source data
152        $source = $this->getSourceField( $field, $displayLanguage, $displayFallbackChain, $sourceData );
153        // But if we actually have highlight for this one, use it!
154        if ( $source && !empty( $highlightData["{$field}.{$source['language']}.plain"] ) ) {
155            $this->haveMatch = true;
156            return [
157                'language' => $source['language'],
158                // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
159                'value' => new HtmlArmor( $this->processHighlighting( $highlightData["{$field}.{$source['language']}.plain"][0],
160                    $useOffsets ) )
161            ];
162        }
163        return $source;
164    }
165
166    /**
167     * Get data from source fields, using fallback chain if necessary.
168     * @param string $field Field in source data where we're looking.
169     *                      The field will contain subfield by language names.
170     * @param string $displayLanguage
171     * @param TermLanguageFallbackChain $displayFallbackChain
172     * @param array $sourceData The source data as returned by Elastic.
173     * @return array
174     */
175    private function getSourceField( $field, $displayLanguage,
176            TermLanguageFallbackChain $displayFallbackChain,
177            $sourceData
178    ) {
179        $term = EntitySearchUtils::findTermForDisplay( $sourceData, $field, $displayFallbackChain );
180        if ( $term ) {
181            return [ 'language' => $term->getLanguageCode(), 'value' => $term->getText() ];
182        }
183        // OK, we don't have much here
184        return [ 'language' => $displayLanguage, 'value' => '' ];
185    }
186
187    /**
188     * Process highlighted string from search results.
189     * @param string $snippet
190     * @param bool $useOffsets Is highlighter using offsets option?
191     * @return string Highlighted and HTML-encoded string
192     */
193    private function processHighlighting( $snippet, $useOffsets = false ) {
194        if ( $useOffsets && preg_match( ElasticTermResult::HIGHLIGHT_PATTERN, $snippet, $match ) ) {
195            $snippet = $match[1];
196        }
197        return strtr( htmlspecialchars( $snippet ), [
198            Searcher::HIGHLIGHT_PRE_MARKER => Searcher::HIGHLIGHT_PRE,
199            Searcher::HIGHLIGHT_POST_MARKER => Searcher::HIGHLIGHT_POST
200        ] );
201    }
202
203    /**
204     * @return string[] ['language' => LANG, 'value' => TEXT]
205     */
206    public function getLabelData() {
207        return $this->labelData;
208    }
209
210    /**
211     * @return string[] ['language' => LANG, 'value' => TEXT]
212     */
213    public function getDescriptionData() {
214        return $this->descriptionData;
215    }
216
217    /**
218     * @return string[] ['language' => LANG, 'value' => TEXT]
219     */
220    public function getLabelHighlightedData() {
221        return $this->labelHighlightedData;
222    }
223
224    /**
225     * @return string[] ['language' => LANG, 'value' => TEXT]
226     */
227    public function getDescriptionHighlightedData() {
228        return $this->descriptionHighlightedData;
229    }
230
231    /**
232     * @return string
233     */
234    public function getTitleSnippet() {
235        return HtmlArmor::getHtml( $this->labelHighlightedData['value'] );
236    }
237
238    /**
239     * @param array $terms
240     * @return string
241     */
242    public function getTextSnippet( $terms = [] ) {
243        return HtmlArmor::getHtml( $this->descriptionHighlightedData['value'] );
244    }
245
246    /**
247     * @return string[] ['language' => LANG, 'value' => TEXT]
248     */
249    public function getExtraDisplay() {
250        return $this->extraDisplay;
251    }
252
253    /**
254     * Get number of statements
255     * @return int
256     */
257    public function getStatementCount() {
258        if ( !isset( $this->sourceData['statement_count'] ) ) {
259            return 0;
260        }
261        return (int)$this->sourceData['statement_count'];
262    }
263
264    /**
265     * Get number of sitelinks
266     * @return int
267     */
268    public function getSitelinkCount() {
269        if ( !isset( $this->sourceData['sitelink_count'] ) ) {
270            return 0;
271        }
272        return (int)$this->sourceData['sitelink_count'];
273    }
274
275    /**
276     * Augment extension data with extraDisplay data.
277     * @return array[]
278     */
279    public function getExtensionData() {
280        $data = parent::getExtensionData();
281        if ( $this->extraDisplay ) {
282            $data[self::WIKIBASE_EXTRA_DATA] = [
283                'extrasnippet' => HtmlArmor::getHtml( $this->extraDisplay['value'] ),
284                'extrasnippet-language' => $this->extraDisplay['language'],
285            ];
286        }
287        return $data;
288    }
289
290}