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    /** @var SpecialSearch */
22    protected $specialSearch;
23    /** @var SearchResultWidget */
24    protected $resultWidget;
25    /** @var array<string,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        // This is false if the lookup fails, or if the other wiki is on the same
133        // domain name (i.e. /en-wiki/ and /de-wiki/)
134        $iwHost = $interwiki ? parse_url( $interwiki->getURL(), PHP_URL_HOST ) : false;
135
136        $captionText = $this->customCaptions[$iwPrefix] ?? $iwHost ?: $iwPrefix;
137        $searchLink = Html::element( 'a', [ 'href' => $href, 'target' => '_blank' ], $captionText );
138
139        return Html::rawElement( 'div',
140            [ 'class' => 'iw-result__header' ],
141            $this->iwIcon( $iwPrefix ) . $searchLink );
142    }
143
144    /**
145     * Generates an HTML footer for the given interwiki prefix
146     *
147     * @param string $term User provided search term
148     * @param string $iwPrefix Interwiki prefix of wiki to show heading for
149     * @return string HTML
150     */
151    protected function footerHtml( $term, $iwPrefix ) {
152        $href = Title::makeTitle( NS_SPECIAL, 'Search', '', $iwPrefix )->getLocalURL(
153            [ 'search' => $term, 'fulltext' => 1 ]
154        );
155
156        $captionText = $this->specialSearch->msg( 'search-interwiki-resultset-link' )->text();
157        $searchLink = Html::element( 'a', [ 'href' => $href, 'target' => '_blank' ], $captionText );
158
159        return Html::rawElement( 'div',
160            [ 'class' => 'iw-result__footer' ],
161            $searchLink );
162    }
163
164    protected function loadCustomCaptions() {
165        if ( $this->customCaptions !== null ) {
166            return;
167        }
168
169        $this->customCaptions = [];
170        $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->text() );
171        foreach ( $customLines as $line ) {
172            $parts = explode( ':', $line, 2 );
173            if ( count( $parts ) === 2 ) {
174                $this->customCaptions[$parts[0]] = $parts[1];
175            }
176        }
177    }
178
179    /**
180     * Generates a custom OOUI icon element.
181     * These icons are either generated by fetching the interwiki favicon.
182     * or by using config 'InterwikiLogoOverrides'.
183     *
184     * @param string $iwPrefix Interwiki prefix
185     * @return OOUI\IconWidget
186     */
187    protected function iwIcon( $iwPrefix ) {
188        $logoName = $this->generateLogoName( $iwPrefix );
189        // If the value is an URL we use the favicon
190        if ( filter_var( $logoName, FILTER_VALIDATE_URL ) || $logoName === "/" ) {
191            return $this->generateIconFromFavicon( $logoName );
192        }
193
194        $iwIcon = new OOUI\IconWidget( [
195            'icon' => $logoName
196        ] );
197
198        return $iwIcon;
199    }
200
201    /**
202     * Generates the logo name used to render the interwiki icon.
203     * The logo name can be defined in two ways:
204     * 1) The logo is generated using interwiki getURL to fetch the site favicon
205     * 2) The logo name is defined using config `wgInterwikiLogoOverride`. This accept
206     * Codex icon names and URLs.
207     *
208     * @param string $prefix Interwiki prefix
209     * @return string logoName
210     */
211    protected function generateLogoName( $prefix ) {
212        $logoOverridesKeys = array_keys( $this->iwLogoOverrides );
213        if ( in_array( $prefix, $logoOverridesKeys ) ) {
214            return $this->iwLogoOverrides[ $prefix ];
215        }
216
217        $interwiki = $this->iwLookup->fetch( $prefix );
218        return $interwiki ? $interwiki->getURL() : '/';
219    }
220
221    /**
222     * Fetches the favicon of the provided URL.
223     *
224     * @param string $logoUrl
225     * @return OOUI\IconWidget
226     */
227    protected function generateIconFromFavicon( $logoUrl ) {
228        $parsed = wfGetUrlUtils()->parse( (string)wfGetUrlUtils()->expand( $logoUrl, PROTO_CURRENT ) );
229        '@phan-var array $parsed'; // Valid URL
230        $iwIconUrl = $parsed['scheme'] .
231            $parsed['delimiter'] .
232            $parsed['host'] .
233            ( isset( $parsed['port'] ) ? ':' . $parsed['port'] : '' ) .
234            '/favicon.ico';
235
236        $iwIcon = new OOUI\IconWidget( [
237            'icon' => 'favicon'
238        ] );
239
240        return $iwIcon->setAttributes( [ 'style' => "background-image:url($iwIconUrl);" ] );
241    }
242}