Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.77% covered (success)
92.77%
77 / 83
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchHookHandler
92.77% covered (success)
92.77%
77 / 83
87.50% covered (warning)
87.50%
7 / 8
17.11
0.00% covered (danger)
0.00%
0 / 1
 newFromGlobalState
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 onSpecialSearchResultsAppend
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 addToSearch
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 getTermSearchResults
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 renderTermSearchResults
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 renderTermSearchResult
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 searchEntities
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace ArticlePlaceholder;
4
5use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
6use MediaWiki\Config\Config;
7use MediaWiki\Hook\SpecialSearchResultsAppendHook;
8use MediaWiki\MainConfigNames;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Output\OutputPage;
11use MediaWiki\Specials\SpecialSearch;
12use Wikibase\Client\WikibaseClient;
13use Wikibase\Lib\Interactors\TermSearchInteractor;
14use Wikibase\Lib\Interactors\TermSearchResult;
15use Wikibase\Lib\TermIndexEntry;
16use Wikimedia\Rdbms\SessionConsistentConnectionManager;
17
18/**
19 * Adding results from ArticlePlaceholder to search
20 *
21 * @license GPL-2.0-or-later
22 * @author Lucie-Aimée Kaffee
23 */
24class SearchHookHandler implements SpecialSearchResultsAppendHook {
25
26    /**
27     * @var TermSearchInteractor
28     */
29    private $termSearchInteractor;
30
31    /**
32     * @var string
33     */
34    private $languageCode;
35
36    /**
37     * @var ItemNotabilityFilter
38     */
39    private $itemNotabilityFilter;
40
41    /**
42     * @var StatsdDataFactoryInterface
43     */
44    private $statsdDataFactory;
45
46    /**
47     * @param Config $config
48     *
49     * @return self
50     */
51    public static function newFromGlobalState( Config $config ) {
52        // TODO inject services into hook handler instance
53        $mwServices = MediaWikiServices::getInstance();
54        $repoDB = WikibaseClient::getItemAndPropertySource()->getDatabaseName();
55        $lbFactory = $mwServices->getDBLoadBalancerFactory();
56        $clientSettings = WikibaseClient::getSettings( $mwServices );
57
58        $itemNotabilityFilter = new ItemNotabilityFilter(
59            new SessionConsistentConnectionManager( $lbFactory->getMainLB( $repoDB ), $repoDB ),
60            WikibaseClient::getEntityNamespaceLookup( $mwServices ),
61            WikibaseClient::getStore()->getSiteLinkLookup(),
62            $clientSettings->getSetting( 'siteGlobalID' )
63        );
64
65        $statsdDataFactory = $mwServices->getStatsdDataFactory();
66
67        $termSearchInteractor = new TermSearchApiInteractor(
68            new RepoApiInteractor(
69                $config->get( 'ArticlePlaceholderRepoApiUrl' ),
70                $statsdDataFactory,
71                $mwServices->getHttpRequestFactory()
72            ),
73            WikibaseClient::getEntityIdParser()
74        );
75
76        return new self(
77            $termSearchInteractor,
78            $config->get( MainConfigNames::LanguageCode ),
79            $itemNotabilityFilter,
80            $statsdDataFactory
81        );
82    }
83
84    /**
85     * @param TermSearchInteractor $termSearchInteractor
86     * @param string $languageCode content language
87     * @param ItemNotabilityFilter $itemNotabilityFilter
88     * @param StatsdDataFactoryInterface $statsdDataFactory
89     */
90    public function __construct(
91        TermSearchInteractor $termSearchInteractor,
92        $languageCode,
93        ItemNotabilityFilter $itemNotabilityFilter,
94        StatsdDataFactoryInterface $statsdDataFactory
95    ) {
96        $this->termSearchInteractor = $termSearchInteractor;
97        $this->languageCode = $languageCode;
98        $this->itemNotabilityFilter = $itemNotabilityFilter;
99        $this->statsdDataFactory = $statsdDataFactory;
100    }
101
102    /**
103     * @param SpecialSearch $specialSearch
104     * @param OutputPage $output
105     * @param string $term
106     */
107    public function onSpecialSearchResultsAppend(
108        $specialSearch,
109        $output,
110        $term
111    ) {
112        if ( trim( $term ) === '' ) {
113            return;
114        }
115
116        $config = $specialSearch->getConfig();
117        if ( !$config->get( 'ArticlePlaceholderSearchIntegrationEnabled' ) ) {
118            return;
119        }
120
121        $this->addToSearch( $output, $term );
122    }
123
124    /**
125     * @param OutputPage $output
126     * @param string $term
127     */
128    private function addToSearch( OutputPage $output, $term ): void {
129        $termSearchResults = $this->getTermSearchResults( $term );
130
131        if ( $termSearchResults ) {
132            $renderedTermSearchResults = $this->renderTermSearchResults( $termSearchResults );
133
134            if ( $renderedTermSearchResults !== '' ) {
135                $output->addWikiTextAsInterface(
136                    '==' .
137                    $output->msg( 'articleplaceholder-search-header' )->plain() .
138                    '=='
139                );
140
141                $output->addWikiTextAsInterface( $renderedTermSearchResults );
142
143                $this->statsdDataFactory->increment(
144                    'wikibase.articleplaceholder.search.has_results'
145                );
146
147                return;
148            }
149        }
150
151        $this->statsdDataFactory->increment(
152            'wikibase.articleplaceholder.search.no_results'
153        );
154    }
155
156    /**
157     * @param string $term
158     *
159     * @return TermSearchResult[]
160     */
161    private function getTermSearchResults( $term ) {
162        $termSearchResults = [];
163
164        foreach ( $this->searchEntities( $term ) as $searchResult ) {
165            $entityId = $searchResult->getEntityId()->getSerialization();
166
167            $termSearchResults[ $entityId ] = $searchResult;
168        }
169
170        return $termSearchResults;
171    }
172
173    /**
174     * Render search results, filtered for notability.
175     *
176     * @param TermSearchResult[] $termSearchResults
177     *
178     * @return string Wikitext
179     */
180    private function renderTermSearchResults( array $termSearchResults ) {
181        $wikitext = '';
182
183        $itemIds = [];
184        foreach ( $termSearchResults as $termSearchResult ) {
185            $itemIds[] = $termSearchResult->getEntityId();
186        }
187
188        $notableEntityIds = $this->itemNotabilityFilter->getNotableEntityIds( $itemIds );
189
190        foreach ( $notableEntityIds as $entityId ) {
191            $result = $this->renderTermSearchResult( $termSearchResults[ $entityId->getSerialization() ] );
192
193            $wikitext .= '<div class="article-placeholder-searchResult">'
194                        . $result
195                        . '</div>';
196        }
197
198        return $wikitext;
199    }
200
201    /**
202     * @param TermSearchResult $searchResult
203     *
204     * @return string Wikitext
205     */
206    private function renderTermSearchResult( TermSearchResult $searchResult ) {
207        $entityId = $searchResult->getEntityId();
208
209        $displayLabel = $searchResult->getDisplayLabel();
210        $displayDescription = $searchResult->getDisplayDescription();
211
212        $label = $displayLabel ? $displayLabel->getText() : $entityId->getSerialization();
213
214        // TODO: Properly construct the page name of the special page.
215        $wikitext = '[[Special:AboutTopic/' . wfEscapeWikiText( $entityId ) . '|'
216            . wfEscapeWikiText( $label ) . ']]';
217
218        if ( $displayDescription ) {
219            $wikitext .= ': ' . wfEscapeWikiText( $displayDescription->getText() );
220        }
221
222        return $wikitext;
223    }
224
225    /**
226     * @param string $term
227     *
228     * @return TermSearchResult[]
229     */
230    private function searchEntities( $term ) {
231        return $this->termSearchInteractor->searchForEntities(
232            $term,
233            $this->languageCode,
234            'item',
235            [ TermIndexEntry::TYPE_LABEL, TermIndexEntry::TYPE_ALIAS ]
236        );
237    }
238
239}