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    private function unsetIconsWithoutImages( array &$icons ) {
267        // Unset any icons which don't have an image
268        foreach ( $icons as $iconsKey => &$iconsBlock ) {
269            foreach ( $iconsBlock as $iconKey => $icon ) {
270                if ( !is_string( $icon ) && !isset( $icon['src'] ) ) {
271                    unset( $iconsBlock[$iconKey] );
272                }
273            }
274            if ( $iconsBlock === [] ) {
275                unset( $icons[$iconsKey] );
276            }
277        }
278    }
279
280    /**
281     * Renderer for getFooterIcons and getFooterLinks
282     *
283     * @param string $iconStyle $option for getFooterIcons: "icononly", "nocopyright"
284     *   the "nocopyright" option is deprecated in 1.35 because of its association with getFooterIcons
285     * @param string $linkStyle $option for getFooterLinks: "flat"
286     *
287     * @return string html
288     * @since 1.29
289     */
290    protected function getFooter( $iconStyle = 'icononly', $linkStyle = 'flat' ) {
291        $validFooterIcons = $this->get( 'footericons' );
292        if ( $iconStyle === 'icononly' ) {
293            $this->unsetIconsWithoutImages( $validFooterIcons );
294        } else {
295            // take a deprecated unsupported path
296            $validFooterIcons = $this->getFooterIcons( $iconStyle );
297        }
298        $validFooterLinks = $this->getFooterLinks( $linkStyle );
299
300        $html = '';
301
302        if ( count( $validFooterIcons ) + count( $validFooterLinks ) > 0 ) {
303            $html .= Html::openElement( 'div', [
304                'id' => 'footer-bottom',
305                'class' => 'mw-footer',
306                'role' => 'contentinfo',
307                'lang' => $this->get( 'userlang' ),
308                'dir' => $this->get( 'dir' )
309            ] );
310            $footerEnd = Html::closeElement( 'div' );
311        } else {
312            $footerEnd = '';
313        }
314        foreach ( $validFooterIcons as $blockName => $footerIcons ) {
315            $html .= Html::openElement( 'div', [
316                'id' => Sanitizer::escapeIdForAttribute( "f-{$blockName}ico" ),
317                'class' => 'footer-icons'
318            ] );
319            foreach ( $footerIcons as $icon ) {
320                $html .= $this->getSkin()->makeFooterIcon( $icon );
321            }
322            $html .= Html::closeElement( 'div' );
323        }
324        if ( count( $validFooterLinks ) > 0 ) {
325            $html .= Html::openElement( 'ul', [ 'id' => 'f-list', 'class' => 'footer-places' ] );
326            foreach ( $validFooterLinks as $aLink ) {
327                $html .= Html::rawElement(
328                    'li',
329                    [ 'id' => Sanitizer::escapeIdForAttribute( $aLink ) ],
330                    $this->get( $aLink )
331                );
332            }
333            $html .= Html::closeElement( 'ul' );
334        }
335
336        $html .= $this->getClear() . $footerEnd;
337
338        return $html;
339    }
340
341    /**
342     * Get a div with the core visualClear class, for clearing floats
343     *
344     * @return string html
345     * @since 1.29
346     */
347    protected function getClear() {
348        return Html::element( 'div', [ 'class' => 'visualClear' ] );
349    }
350
351    /**
352     * Get the suggested HTML for page status indicators: icons (or short text snippets) usually
353     * displayed in the top-right corner of the page, outside of the main content.
354     *
355     * Your skin may implement this differently, for example by handling some indicator names
356     * specially with a different UI. However, it is recommended to use a `<div class="mw-indicator"
357     * id="mw-indicator-<id>" />` as a wrapper element for each indicator, for better compatibility
358     * with extensions and user scripts.
359     *
360     * The raw data is available in `$this->data['indicators']` as an associative array (keys:
361     * identifiers, values: contents) internally ordered by keys.
362     *
363     * @return string HTML
364     * @since 1.25
365     */
366    public function getIndicators() {
367        $out = "<div class=\"mw-indicators\">\n";
368        foreach ( $this->data['indicators'] as $id => $content ) {
369            $out .= Html::rawElement(
370                'div',
371                [
372                    'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
373                    'class' => 'mw-indicator',
374                ],
375                $content
376            ) .
377            // Add whitespace between the <div>s because
378            // they get displayed with display: inline-block
379            "\n";
380        }
381        $out .= "</div>\n";
382        return $out;
383    }
384}