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