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