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