Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
BaseTemplate
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 13
3306
0.00% covered (danger)
0.00%
0 / 1
 getMsg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPersonalTools
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSidebar
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
702
 makeLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeListItem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeSearchInput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeSearchButton
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFooterLinks
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 unsetIconsWithoutImages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 getFooter
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
56
 getClear
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndicators
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Skin;
22
23use MediaWiki\Html\Html;
24use MediaWiki\Message\Message;
25use MediaWiki\Parser\Sanitizer;
26use Wikimedia\Message\MessageParam;
27use Wikimedia\Message\MessageSpecifier;
28
29/**
30 * Extended QuickTemplate with additional MediaWiki-specific helper methods.
31 *
32 * @todo Phase this class out and make it an alias for QuickTemplate. Move methods
33 *  individually as-appropriate either down to QuickTemplate, or (with deprecation)
34 *  up to SkinTemplate.
35 *
36 * @stable to extend
37 */
38abstract class BaseTemplate extends QuickTemplate {
39
40    /**
41     * Get a Message object with its context set
42     *
43     * @param string $name Message name
44     * @phpcs:ignore Generic.Files.LineLength
45     * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
46     *   See Message::params()
47     * @return Message
48     */
49    public function getMsg( $name, ...$params ) {
50        return $this->getSkin()->msg( $name, ...$params );
51    }
52
53    public function msg( $str ) {
54        echo $this->getMsg( $str )->escaped();
55    }
56
57    /**
58     * @return array
59     */
60    public function getPersonalTools() {
61        return $this->getSkin()->getPersonalToolsForMakeListItem( $this->get( 'personal_urls' ) );
62    }
63
64    /**
65     * @param array $options (optional) allows disabling certain sidebar elements.
66     *  The keys `search`, `toolbox` and `languages` are accepted.
67     * @return array representing the sidebar
68     */
69    protected function getSidebar( $options = [] ) {
70        // Force the rendering of the following portals
71        $sidebar = $this->data['sidebar'];
72        if ( !isset( $sidebar['SEARCH'] ) ) {
73            // @phan-suppress-next-line PhanTypeMismatchDimAssignment False positive
74            $sidebar['SEARCH'] = true;
75        }
76        if ( !isset( $sidebar['TOOLBOX'] ) ) {
77            $sidebar['TOOLBOX'] = true;
78        }
79        if ( !isset( $sidebar['LANGUAGES'] ) ) {
80            $sidebar['LANGUAGES'] = true;
81        }
82
83        if ( !isset( $options['search'] ) || $options['search'] !== true ) {
84            unset( $sidebar['SEARCH'] );
85        }
86        if ( isset( $options['toolbox'] ) && $options['toolbox'] === false ) {
87            unset( $sidebar['TOOLBOX'] );
88        }
89        if ( isset( $options['languages'] ) && $options['languages'] === false ) {
90            unset( $sidebar['LANGUAGES'] );
91        }
92
93        $boxes = [];
94        foreach ( $sidebar as $boxName => $content ) {
95            if ( $content === false ) {
96                continue;
97            }
98            switch ( $boxName ) {
99                case 'SEARCH':
100                    // Search is a special case, skins should custom implement this
101                    $boxes[$boxName] = [
102                        'id' => 'p-search',
103                        'header' => $this->getMsg( 'search' )->text(),
104                        'generated' => false,
105                        'content' => true,
106                    ];
107                    break;
108                case 'TOOLBOX':
109                    $msgObj = $this->getMsg( 'toolbox' );
110                    $boxes[$boxName] = [
111                        'id' => 'p-tb',
112                        'header' => $msgObj->exists() ? $msgObj->text() : 'toolbox',
113                        'generated' => false,
114                        'content' => $content,
115                    ];
116                    break;
117                case 'LANGUAGES':
118                    if ( $this->data['language_urls'] !== false ) {
119                        $msgObj = $this->getMsg( 'otherlanguages' );
120                        $boxes[$boxName] = [
121                            'id' => 'p-lang',
122                            'header' => $msgObj->exists() ? $msgObj->text() : 'otherlanguages',
123                            'generated' => false,
124                            'content' => $this->data['language_urls'] ?: [],
125                        ];
126                    }
127                    break;
128                default:
129                    $msgObj = $this->getMsg( $boxName );
130                    $boxes[$boxName] = [
131                        'id' => "p-$boxName",
132                        'header' => $msgObj->exists() ? $msgObj->text() : $boxName,
133                        'generated' => true,
134                        'content' => $content,
135                    ];
136                    break;
137            }
138        }
139
140        if ( isset( $options['htmlOnly'] ) && $options['htmlOnly'] === true ) {
141            foreach ( $boxes as $boxName => $box ) {
142                if ( is_array( $box['content'] ) ) {
143                    $content = '<ul>';
144                    foreach ( $box['content'] as $key => $val ) {
145                        $content .= "\n    " . $this->getSkin()->makeListItem( $key, $val );
146                    }
147                    $content .= "\n</ul>\n";
148                    $boxes[$boxName]['content'] = $content;
149                }
150            }
151        }
152
153        return $boxes;
154    }
155
156    /**
157     * Wrapper for Skin method.
158     *
159     * @param string $key of link
160     * @param array $item to render
161     * @param array $options for link
162     * @return string
163     */
164    protected function makeLink( $key, $item, $options = [] ) {
165        return $this->getSkin()->makeLink( $key, $item, $options );
166    }
167
168    /**
169     * Wrapper for Skin method.
170     *
171     * @param string $key of list item
172     * @param array $item to render
173     * @param array $options for list item
174     * @return string
175     */
176    public function makeListItem( $key, $item, $options = [] ) {
177        return $this->getSkin()->makeListItem( $key, $item, $options );
178    }
179
180    /**
181     * Wrapper for Skin method.
182     *
183     * @param array $attrs
184     * @return string
185     */
186    protected function makeSearchInput( $attrs = [] ) {
187        return $this->getSkin()->makeSearchInput( $attrs );
188    }
189
190    /**
191     * Wrapper for Skin method.
192     *
193     * @param string $mode
194     * @param array $attrs
195     * @return string
196     */
197    protected function makeSearchButton( $mode, $attrs = [] ) {
198        return $this->getSkin()->makeSearchButton( $mode, $attrs );
199    }
200
201    /**
202     * Returns an array of footerlinks trimmed down to only those footer links that
203     * are valid.
204     * If you pass "flat" as an option then the returned array will be a flat array
205     * of footer icons instead of a key/value array of footerlinks arrays broken
206     * up into categories.
207     * @param string|null $option
208     * @return array
209     */
210    protected function getFooterLinks( $option = null ) {
211        $footerlinks = $this->get( 'footerlinks' );
212
213        // Reduce footer links down to only those which are being used
214        $validFooterLinks = [];
215        foreach ( $footerlinks as $category => $links ) {
216            $validFooterLinks[$category] = [];
217            foreach ( $links as $link ) {
218                if ( isset( $this->data[$link] ) && $this->data[$link] ) {
219                    $validFooterLinks[$category][] = $link;
220                }
221            }
222            if ( count( $validFooterLinks[$category] ) <= 0 ) {
223                unset( $validFooterLinks[$category] );
224            }
225        }
226
227        if ( $option == 'flat' && count( $validFooterLinks ) ) {
228            // fold footerlinks into a single array using a bit of trickery
229            $validFooterLinks = array_merge( ...array_values( $validFooterLinks ) );
230        }
231
232        return $validFooterLinks;
233    }
234
235    /**
236     * Unsets any elements in an array of icon definitions which do
237     * not have src attributes or are not strings.
238     */
239    private function unsetIconsWithoutImages( array &$icons ) {
240        // Unset any icons which don't have an image
241        foreach ( $icons as $iconsKey => &$iconsBlock ) {
242            foreach ( $iconsBlock as $iconKey => $icon ) {
243                if ( !is_string( $icon ) && !isset( $icon['src'] ) ) {
244                    unset( $iconsBlock[$iconKey] );
245                }
246            }
247            if ( $iconsBlock === [] ) {
248                unset( $icons[$iconsKey] );
249            }
250        }
251    }
252
253    /**
254     * Renderer for getFooterLinks
255     *
256     * @param string $iconStyle $option when set to "icononly" will not display any
257     *  icons which do not have images.
258     * @param string $linkStyle $option for getFooterLinks: "flat"
259     *
260     * @return string html
261     * @since 1.29
262     */
263    protected function getFooter( $iconStyle = 'icononly', $linkStyle = 'flat' ) {
264        $validFooterIcons = $this->get( 'footericons' );
265        if ( $iconStyle === 'icononly' ) {
266            $this->unsetIconsWithoutImages( $validFooterIcons );
267        }
268        $validFooterLinks = $this->getFooterLinks( $linkStyle );
269
270        $html = '';
271
272        if ( count( $validFooterIcons ) + count( $validFooterLinks ) > 0 ) {
273            $html .= Html::openElement( 'div', [
274                'id' => 'footer-bottom',
275                'class' => 'mw-footer',
276                'role' => 'contentinfo',
277                'lang' => $this->get( 'userlang' ),
278                'dir' => $this->get( 'dir' )
279            ] );
280            $footerEnd = Html::closeElement( 'div' );
281        } else {
282            $footerEnd = '';
283        }
284        foreach ( $validFooterIcons as $blockName => $footerIcons ) {
285            $html .= Html::openElement( 'div', [
286                'id' => Sanitizer::escapeIdForAttribute( "f-{$blockName}ico" ),
287                'class' => 'footer-icons'
288            ] );
289            foreach ( $footerIcons as $icon ) {
290                $html .= $this->getSkin()->makeFooterIcon( $icon );
291            }
292            $html .= Html::closeElement( 'div' );
293        }
294        if ( count( $validFooterLinks ) > 0 ) {
295            $html .= Html::openElement( 'ul', [ 'id' => 'f-list', 'class' => 'footer-places' ] );
296            foreach ( $validFooterLinks as $aLink ) {
297                $html .= Html::rawElement(
298                    'li',
299                    [ 'id' => Sanitizer::escapeIdForAttribute( $aLink ) ],
300                    $this->get( $aLink )
301                );
302            }
303            $html .= Html::closeElement( 'ul' );
304        }
305
306        $html .= $this->getClear() . $footerEnd;
307
308        return $html;
309    }
310
311    /**
312     * Get a div with the core visualClear class, for clearing floats
313     *
314     * @return string html
315     * @since 1.29
316     */
317    protected function getClear() {
318        return Html::element( 'div', [ 'class' => 'visualClear' ] );
319    }
320
321    /**
322     * Get the suggested HTML for page status indicators: icons (or short text snippets) usually
323     * displayed in the top-right corner of the page, outside of the main content.
324     *
325     * Your skin may implement this differently, for example by handling some indicator names
326     * specially with a different UI. However, it is recommended to use a `<div class="mw-indicator"
327     * id="mw-indicator-<id>" />` as a wrapper element for each indicator, for better compatibility
328     * with extensions and user scripts.
329     *
330     * The raw data is available in `$this->data['indicators']` as an associative array (keys:
331     * identifiers, values: contents) internally ordered by keys.
332     *
333     * @return string HTML
334     * @since 1.25
335     */
336    public function getIndicators() {
337        $out = "<div class=\"mw-indicators\">\n";
338        foreach ( $this->data['indicators'] as $id => $content ) {
339            $out .= Html::rawElement(
340                'div',
341                [
342                    'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
343                    'class' => 'mw-indicator',
344                ],
345                $content
346            ) .
347            // Add whitespace between the <div>s because
348            // they get displayed with display: inline-block
349            "\n";
350        }
351        $out .= "</div>\n";
352        return $out;
353    }
354}
355
356/** @deprecated class alias since 1.44 */
357class_alias( BaseTemplate::class, 'BaseTemplate' );