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