Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
InterwikiSearchResultSetWidget
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 8
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
72
 headerHtml
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 footerHtml
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 loadCustomCaptions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 iwIcon
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 generateLogoName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 generateIconFromFavicon
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Search\SearchWidgets;
4
5use ISearchResultSet;
6use MediaWiki\Html\Html;
7use MediaWiki\Interwiki\InterwikiLookup;
8use MediaWiki\Linker\LinkRenderer;
9use MediaWiki\MainConfigNames;
10use MediaWiki\Output\OutputPage;
11use MediaWiki\Specials\SpecialSearch;
12use MediaWiki\Title\Title;
13use 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 */
20class 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}