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