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