Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 94 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
InterwikiSearchResultSetWidget | |
0.00% |
0 / 94 |
|
0.00% |
0 / 8 |
650 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
render | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
72 | |||
headerHtml | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
footerHtml | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
loadCustomCaptions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
iwIcon | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
generateLogoName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
generateIconFromFavicon | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Search\SearchWidgets; |
4 | |
5 | use ISearchResultSet; |
6 | use MediaWiki\Html\Html; |
7 | use MediaWiki\Interwiki\InterwikiLookup; |
8 | use MediaWiki\Linker\LinkRenderer; |
9 | use MediaWiki\MainConfigNames; |
10 | use MediaWiki\Output\OutputPage; |
11 | use MediaWiki\Specials\SpecialSearch; |
12 | use MediaWiki\Title\Title; |
13 | use OOUI; |
14 | |
15 | /** |
16 | * Renders one or more ISearchResultSets into a sidebar grouped by |
17 | * interwiki prefix. Includes a per-wiki header indicating where |
18 | * the results are from. |
19 | */ |
20 | class InterwikiSearchResultSetWidget implements SearchResultSetWidget { |
21 | protected SpecialSearch $specialSearch; |
22 | protected SearchResultWidget $resultWidget; |
23 | protected LinkRenderer $linkRenderer; |
24 | protected InterwikiLookup $iwLookup; |
25 | protected OutputPage $output; |
26 | |
27 | /** @var bool */ |
28 | protected $showMultimedia; |
29 | /** @var array */ |
30 | protected $iwLogoOverrides; |
31 | |
32 | /** @var array<string,string>|null */ |
33 | protected $customCaptions; |
34 | |
35 | public function __construct( |
36 | SpecialSearch $specialSearch, |
37 | SearchResultWidget $resultWidget, |
38 | LinkRenderer $linkRenderer, |
39 | InterwikiLookup $iwLookup, |
40 | $showMultimedia = false |
41 | ) { |
42 | $this->specialSearch = $specialSearch; |
43 | $this->resultWidget = $resultWidget; |
44 | $this->linkRenderer = $linkRenderer; |
45 | $this->iwLookup = $iwLookup; |
46 | $this->output = $specialSearch->getOutput(); |
47 | $this->showMultimedia = $showMultimedia; |
48 | $this->iwLogoOverrides = $this->specialSearch->getConfig()->get( MainConfigNames::InterwikiLogoOverride ); |
49 | } |
50 | |
51 | /** |
52 | * @param string $term User provided search term |
53 | * @param ISearchResultSet|ISearchResultSet[] $resultSets List of interwiki |
54 | * results to render. |
55 | * @return string HTML |
56 | */ |
57 | public function render( $term, $resultSets ) { |
58 | if ( !is_array( $resultSets ) ) { |
59 | $resultSets = [ $resultSets ]; |
60 | } |
61 | |
62 | $this->loadCustomCaptions(); |
63 | |
64 | if ( $this->showMultimedia ) { |
65 | $this->output->addModules( 'mediawiki.special.search.commonsInterwikiWidget' ); |
66 | } |
67 | $this->output->addModuleStyles( 'mediawiki.special.search.interwikiwidget.styles' ); |
68 | $this->output->addModuleStyles( 'oojs-ui.styles.icons-wikimedia' ); |
69 | |
70 | $iwResults = []; |
71 | foreach ( $resultSets as $resultSet ) { |
72 | foreach ( $resultSet as $result ) { |
73 | if ( !$result->isBrokenTitle() ) { |
74 | $iwResults[$result->getTitle()->getInterwiki()][] = $result; |
75 | } |
76 | } |
77 | } |
78 | |
79 | $iwResultSetPos = 1; |
80 | $iwResultListOutput = ''; |
81 | |
82 | foreach ( $iwResults as $iwPrefix => $results ) { |
83 | // TODO: Assumes interwiki results are never paginated |
84 | $position = 0; |
85 | $iwResultItemOutput = ''; |
86 | |
87 | foreach ( $results as $result ) { |
88 | $iwResultItemOutput .= $this->resultWidget->render( $result, $position++ ); |
89 | } |
90 | |
91 | $headerHtml = $this->headerHtml( $term, $iwPrefix ); |
92 | $footerHtml = $this->footerHtml( $term, $iwPrefix ); |
93 | $iwResultListOutput .= Html::rawElement( 'li', |
94 | [ |
95 | 'class' => 'iw-resultset', |
96 | 'data-iw-resultset-pos' => $iwResultSetPos, |
97 | 'data-iw-resultset-source' => $iwPrefix |
98 | ], |
99 | |
100 | $headerHtml . |
101 | $iwResultItemOutput . |
102 | $footerHtml |
103 | ); |
104 | $iwResultSetPos++; |
105 | } |
106 | |
107 | return Html::rawElement( |
108 | 'div', |
109 | [ 'id' => 'mw-interwiki-results' ], |
110 | Html::rawElement( |
111 | 'ul', [ 'class' => 'iw-results', ], $iwResultListOutput |
112 | ) |
113 | ); |
114 | } |
115 | |
116 | /** |
117 | * Generates an HTML header for the given interwiki prefix |
118 | * |
119 | * @param string $term User provided search term |
120 | * @param string $iwPrefix Interwiki prefix of wiki to show heading for |
121 | * @return string HTML |
122 | */ |
123 | protected function headerHtml( $term, $iwPrefix ) { |
124 | $href = Title::makeTitle( NS_SPECIAL, 'Search', '', $iwPrefix )->getLocalURL( |
125 | [ 'search' => $term, 'fulltext' => 1 ] |
126 | ); |
127 | |
128 | $interwiki = $this->iwLookup->fetch( $iwPrefix ); |
129 | // This is false if the lookup fails, or if the other wiki is on the same |
130 | // domain name (i.e. /en-wiki/ and /de-wiki/) |
131 | $iwHost = $interwiki ? parse_url( $interwiki->getURL(), PHP_URL_HOST ) : false; |
132 | |
133 | $captionText = $this->customCaptions[$iwPrefix] ?? $iwHost ?: $iwPrefix; |
134 | $searchLink = Html::element( 'a', [ 'href' => $href, 'target' => '_blank' ], $captionText ); |
135 | |
136 | return Html::rawElement( 'div', |
137 | [ 'class' => 'iw-result__header' ], |
138 | $this->iwIcon( $iwPrefix ) . $searchLink ); |
139 | } |
140 | |
141 | /** |
142 | * Generates an HTML footer for the given interwiki prefix |
143 | * |
144 | * @param string $term User provided search term |
145 | * @param string $iwPrefix Interwiki prefix of wiki to show heading for |
146 | * @return string HTML |
147 | */ |
148 | protected function footerHtml( $term, $iwPrefix ) { |
149 | $href = Title::makeTitle( NS_SPECIAL, 'Search', '', $iwPrefix )->getLocalURL( |
150 | [ 'search' => $term, 'fulltext' => 1 ] |
151 | ); |
152 | |
153 | $captionText = $this->specialSearch->msg( 'search-interwiki-resultset-link' )->text(); |
154 | $searchLink = Html::element( 'a', [ 'href' => $href, 'target' => '_blank' ], $captionText ); |
155 | |
156 | return Html::rawElement( 'div', |
157 | [ 'class' => 'iw-result__footer' ], |
158 | $searchLink ); |
159 | } |
160 | |
161 | protected function loadCustomCaptions() { |
162 | if ( $this->customCaptions !== null ) { |
163 | return; |
164 | } |
165 | |
166 | $this->customCaptions = []; |
167 | $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->text() ); |
168 | foreach ( $customLines as $line ) { |
169 | $parts = explode( ':', $line, 2 ); |
170 | if ( count( $parts ) === 2 ) { |
171 | $this->customCaptions[$parts[0]] = $parts[1]; |
172 | } |
173 | } |
174 | } |
175 | |
176 | /** |
177 | * Generates a custom OOUI icon element. |
178 | * These icons are either generated by fetching the interwiki favicon. |
179 | * or by using config 'InterwikiLogoOverrides'. |
180 | * |
181 | * @param string $iwPrefix Interwiki prefix |
182 | * @return OOUI\IconWidget |
183 | */ |
184 | protected function iwIcon( $iwPrefix ) { |
185 | $logoName = $this->generateLogoName( $iwPrefix ); |
186 | // If the value is an URL we use the favicon |
187 | if ( filter_var( $logoName, FILTER_VALIDATE_URL ) || $logoName === "/" ) { |
188 | return $this->generateIconFromFavicon( $logoName ); |
189 | } |
190 | |
191 | $iwIcon = new OOUI\IconWidget( [ |
192 | 'icon' => $logoName |
193 | ] ); |
194 | |
195 | return $iwIcon; |
196 | } |
197 | |
198 | /** |
199 | * Generates the logo name used to render the interwiki icon. |
200 | * The logo name can be defined in two ways: |
201 | * 1) The logo is generated using interwiki getURL to fetch the site favicon |
202 | * 2) The logo name is defined using config `wgInterwikiLogoOverride`. This accept |
203 | * Codex icon names and URLs. |
204 | * |
205 | * @param string $prefix Interwiki prefix |
206 | * @return string logoName |
207 | */ |
208 | protected function generateLogoName( $prefix ) { |
209 | $logoOverridesKeys = array_keys( $this->iwLogoOverrides ); |
210 | if ( in_array( $prefix, $logoOverridesKeys ) ) { |
211 | return $this->iwLogoOverrides[ $prefix ]; |
212 | } |
213 | |
214 | $interwiki = $this->iwLookup->fetch( $prefix ); |
215 | return $interwiki ? $interwiki->getURL() : '/'; |
216 | } |
217 | |
218 | /** |
219 | * Fetches the favicon of the provided URL. |
220 | * |
221 | * @param string $logoUrl |
222 | * @return OOUI\IconWidget |
223 | */ |
224 | protected function generateIconFromFavicon( $logoUrl ) { |
225 | $parsed = wfGetUrlUtils()->parse( (string)wfGetUrlUtils()->expand( $logoUrl, PROTO_CURRENT ) ); |
226 | '@phan-var array $parsed'; // Valid URL |
227 | $iwIconUrl = $parsed['scheme'] . |
228 | $parsed['delimiter'] . |
229 | $parsed['host'] . |
230 | ( isset( $parsed['port'] ) ? ':' . $parsed['port'] : '' ) . |
231 | '/favicon.ico'; |
232 | |
233 | $iwIcon = new OOUI\IconWidget( [ |
234 | 'icon' => 'favicon' |
235 | ] ); |
236 | |
237 | return $iwIcon->setAttributes( [ 'style' => "background-image:url($iwIconUrl);" ] ); |
238 | } |
239 | } |