mediawiki-skins-Vector/VectorTemplate.php
Timo Tijhof bc47b4fb3e VectorTemplate: Refactor literal HTML by using Html methods instead
To some degree the literal HTML was (maybe) useful and self-documenting
at some point when the template was really simple, but until and unless
we really use an Html template for this, it's probably a lot easier to
maintain, understand and review (incl. from security perspective) if
we consistently use the Html class abstraction.

For now, I'm only focussing on cases where there is mixed literal HTML
with embedded PHP statements. The cases where HTML is created plain without
embedded PHP I'm leaving untouched for now.

Any case where attribute or content comes from PHP, use the Html class
instead to clearly indicate which values are escaped, and which are not.

Change-Id: Ib2d6425994918b0c17ef29c1b5d0f9893f61a889
2017-11-10 19:37:38 +00:00

537 lines
15 KiB
PHP

<?php
/**
* Vector - Modern version of MonoBook with fresh look and many usability
* improvements.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Skins
*/
/**
* QuickTemplate class for Vector skin
* @ingroup Skins
*/
class VectorTemplate extends BaseTemplate {
/* Functions */
/**
* Outputs the entire contents of the (X)HTML page
*/
public function execute() {
$this->data['namespace_urls'] = $this->data['content_navigation']['namespaces'];
$this->data['view_urls'] = $this->data['content_navigation']['views'];
$this->data['action_urls'] = $this->data['content_navigation']['actions'];
$this->data['variant_urls'] = $this->data['content_navigation']['variants'];
// Move the watch/unwatch star outside of the collapsed "actions" menu to the main "views" menu
if ( $this->config->get( 'VectorUseIconWatch' ) ) {
$mode = $this->getSkin()->getUser()->isWatched( $this->getSkin()->getRelevantTitle() )
? 'unwatch'
: 'watch';
if ( isset( $this->data['action_urls'][$mode] ) ) {
$this->data['view_urls'][$mode] = $this->data['action_urls'][$mode];
unset( $this->data['action_urls'][$mode] );
}
}
$this->data['pageLanguage'] =
$this->getSkin()->getTitle()->getPageViewLanguage()->getHtmlCode();
// Output HTML Page
$this->html( 'headelement' );
?>
<div id="mw-page-base" class="noprint"></div>
<div id="mw-head-base" class="noprint"></div>
<div id="content" class="mw-body" role="main">
<a id="top"></a>
<?php
if ( $this->data['sitenotice'] ) {
echo Html::rawElement( 'div',
[ 'class' => 'mw-body-content' ],
// Raw HTML
$this->get( 'sitenotice' )
);
}
if ( is_callable( [ $this, 'getIndicators' ] ) ) {
echo $this->getIndicators();
}
// Loose comparison with '!=' is intentional, to catch null and false too, but not '0'
if ( $this->data['title'] != '' ) {
echo Html::rawElement( 'h1',
[
'id' => 'firstHeading',
'class' => 'firstHeading',
'lang' => $this->get( 'pageLanguage' ),
],
// Raw HTML
$this->get( 'title' )
);
}
$this->html( 'prebodyhtml' );
?>
<div id="bodyContent" class="mw-body-content">
<?php
if ( $this->data['isarticle'] ) {
echo Html::element( 'div',
[
'id' => 'siteSub',
'class' => 'noprint',
],
$this->getMsg( 'tagline' )->text()
);
}
?>
<div id="contentSub"<?php $this->html( 'userlangattributes' ) ?>><?php
$this->html( 'subtitle' )
?></div>
<?php
if ( $this->data['undelete'] ) {
echo Html::rawElement( 'div',
[ 'id' => 'contentSub2' ],
// Raw HTML
$this->get( 'undelete' )
);
}
if ( $this->data['newtalk'] ) {
echo Html::rawElement( 'div',
[ 'class' => 'usermessage' ],
// Raw HTML
$this->get( 'newtalk' )
);
}
?>
<div id="jump-to-nav" class="mw-jump">
<?php $this->msg( 'jumpto' ) ?>
<a href="#mw-head"><?php
$this->msg( 'jumptonavigation' )
?></a><?php $this->msg( 'comma-separator' ) ?>
<a href="#p-search"><?php $this->msg( 'jumptosearch' ) ?></a>
</div>
<?php
$this->html( 'bodycontent' );
if ( $this->data['printfooter'] ) {
?>
<div class="printfooter">
<?php $this->html( 'printfooter' ); ?>
</div>
<?php
}
if ( $this->data['catlinks'] ) {
$this->html( 'catlinks' );
}
if ( $this->data['dataAfterContent'] ) {
$this->html( 'dataAfterContent' );
}
?>
<div class="visualClear"></div>
<?php $this->html( 'debughtml' ); ?>
</div>
</div>
<div id="mw-navigation">
<h2><?php $this->msg( 'navigation-heading' ) ?></h2>
<div id="mw-head">
<?php $this->renderNavigation( 'PERSONAL' ); ?>
<div id="left-navigation">
<?php $this->renderNavigation( [ 'NAMESPACES', 'VARIANTS' ] ); ?>
</div>
<div id="right-navigation">
<?php $this->renderNavigation( [ 'VIEWS', 'ACTIONS', 'SEARCH' ] ); ?>
</div>
</div>
<div id="mw-panel">
<div id="p-logo" role="banner"><a class="mw-wiki-logo" href="<?php
echo htmlspecialchars( $this->data['nav_urls']['mainpage']['href'] )
?>" <?php
echo Xml::expandAttributes( Linker::tooltipAndAccesskeyAttribs( 'p-logo' ) )
?>></a></div>
<?php $this->renderPortals( $this->data['sidebar'] ); ?>
</div>
</div>
<div id="footer" role="contentinfo"<?php $this->html( 'userlangattributes' ) ?>>
<?php
foreach ( $this->getFooterLinks() as $category => $links ) {
?>
<ul id="footer-<?php echo $category ?>">
<?php
foreach ( $links as $link ) {
?>
<li id="footer-<?php echo $category ?>-<?php echo $link ?>"><?php $this->html( $link ) ?></li>
<?php
}
?>
</ul>
<?php
}
?>
<?php $footericons = $this->getFooterIcons( 'icononly' );
if ( count( $footericons ) > 0 ) {
?>
<ul id="footer-icons" class="noprint">
<?php
foreach ( $footericons as $blockName => $footerIcons ) {
?>
<li id="footer-<?php echo htmlspecialchars( $blockName ); ?>ico">
<?php
foreach ( $footerIcons as $icon ) {
echo $this->getSkin()->makeFooterIcon( $icon );
}
?>
</li>
<?php
}
?>
</ul>
<?php
}
?>
<div style="clear: both;"></div>
</div>
<?php $this->printTrail(); ?>
</body>
</html>
<?php
}
/**
* Render a series of portals
*
* @param array $portals
*/
protected function renderPortals( $portals ) {
// Force the rendering of the following portals
if ( !isset( $portals['SEARCH'] ) ) {
$portals['SEARCH'] = true;
}
if ( !isset( $portals['TOOLBOX'] ) ) {
$portals['TOOLBOX'] = true;
}
if ( !isset( $portals['LANGUAGES'] ) ) {
$portals['LANGUAGES'] = true;
}
// Render portals
foreach ( $portals as $name => $content ) {
if ( $content === false ) {
continue;
}
// Numeric strings gets an integer when set as key, cast back - T73639
$name = (string)$name;
switch ( $name ) {
case 'SEARCH':
break;
case 'TOOLBOX':
$this->renderPortal( 'tb', $this->getToolbox(), 'toolbox', 'SkinTemplateToolboxEnd' );
break;
case 'LANGUAGES':
if ( $this->data['language_urls'] !== false ) {
$this->renderPortal( 'lang', $this->data['language_urls'], 'otherlanguages' );
}
break;
default:
$this->renderPortal( $name, $content );
break;
}
}
}
/**
* @param string $name
* @param array $content
* @param null|string $msg
* @param null|string|array $hook
*/
protected function renderPortal( $name, $content, $msg = null, $hook = null ) {
if ( $msg === null ) {
$msg = $name;
}
$msgObj = wfMessage( $msg );
$labelId = Sanitizer::escapeId( "p-$name-label" );
?>
<div class="portal" role="navigation" id='<?php
echo Sanitizer::escapeId( "p-$name" )
?>'<?php
echo Linker::tooltip( 'p-' . $name )
?> aria-labelledby='<?php echo $labelId ?>'>
<h3<?php $this->html( 'userlangattributes' ) ?> id='<?php echo $labelId ?>'><?php
echo htmlspecialchars( $msgObj->exists() ? $msgObj->text() : $msg );
?></h3>
<div class="body">
<?php
if ( is_array( $content ) ) {
?>
<ul>
<?php
foreach ( $content as $key => $val ) {
echo $this->makeListItem( $key, $val );
}
if ( $hook !== null ) {
// Avoid PHP 7.1 warning
$skin = $this;
Hooks::run( $hook, [ &$skin, true ] );
}
?>
</ul>
<?php
} else {
// Allow raw HTML block to be defined by extensions
echo $content;
}
$this->renderAfterPortlet( $name );
?>
</div>
</div>
<?php
}
/**
* Render one or more navigations elements by name, automatically reversed by css
* when UI is in RTL mode
*
* @param array $elements
*/
protected function renderNavigation( $elements ) {
// If only one element was given, wrap it in an array, allowing more
// flexible arguments
if ( !is_array( $elements ) ) {
$elements = [ $elements ];
}
// Render elements
foreach ( $elements as $name => $element ) {
switch ( $element ) {
case 'NAMESPACES':
?>
<div id="p-namespaces" role="navigation" class="vectorTabs<?php
if ( count( $this->data['namespace_urls'] ) == 0 ) {
echo ' emptyPortlet';
}
?>" aria-labelledby="p-namespaces-label">
<h3 id="p-namespaces-label"><?php $this->msg( 'namespaces' ) ?></h3>
<ul<?php $this->html( 'userlangattributes' ) ?>>
<?php
foreach ( $this->data['namespace_urls'] as $key => $item ) {
echo $this->makeListItem( $key, $item, [
'vector-wrap' => true,
] );
}
?>
</ul>
</div>
<?php
break;
case 'VARIANTS':
?>
<div id="p-variants" role="navigation" class="vectorMenu<?php
if ( count( $this->data['variant_urls'] ) == 0 ) {
echo ' emptyPortlet';
}
?>" aria-labelledby="p-variants-label">
<?php
// Replace the label with the name of currently chosen variant, if any
$variantLabel = $this->getMsg( 'variants' )->text();
foreach ( $this->data['variant_urls'] as $item ) {
if ( isset( $item['class'] ) && stripos( $item['class'], 'selected' ) !== false ) {
$variantLabel = $item['text'];
break;
}
}
?>
<h3 id="p-variants-label">
<span><?php echo htmlspecialchars( $variantLabel ) ?></span>
</h3>
<div class="menu">
<ul>
<?php
foreach ( $this->data['variant_urls'] as $key => $item ) {
echo $this->makeListItem( $key, $item );
}
?>
</ul>
</div>
</div>
<?php
break;
case 'VIEWS':
?>
<div id="p-views" role="navigation" class="vectorTabs<?php
if ( count( $this->data['view_urls'] ) == 0 ) {
echo ' emptyPortlet';
}
?>" aria-labelledby="p-views-label">
<h3 id="p-views-label"><?php $this->msg( 'views' ) ?></h3>
<ul<?php $this->html( 'userlangattributes' ) ?>>
<?php
foreach ( $this->data['view_urls'] as $key => $item ) {
echo $this->makeListItem( $key, $item, [
'vector-wrap' => true,
'vector-collapsible' => true,
] );
}
?>
</ul>
</div>
<?php
break;
case 'ACTIONS':
?>
<div id="p-cactions" role="navigation" class="vectorMenu<?php
if ( count( $this->data['action_urls'] ) == 0 ) {
echo ' emptyPortlet';
}
?>" aria-labelledby="p-cactions-label">
<h3 id="p-cactions-label"><span><?php
$this->msg( 'vector-more-actions' )
?></span></h3>
<div class="menu">
<ul<?php $this->html( 'userlangattributes' ) ?>>
<?php
foreach ( $this->data['action_urls'] as $key => $item ) {
echo $this->makeListItem( $key, $item );
}
?>
</ul>
</div>
</div>
<?php
break;
case 'PERSONAL':
?>
<div id="p-personal" role="navigation" class="<?php
if ( count( $this->data['personal_urls'] ) == 0 ) {
echo ' emptyPortlet';
}
?>" aria-labelledby="p-personal-label">
<h3 id="p-personal-label"><?php $this->msg( 'personaltools' ) ?></h3>
<ul<?php $this->html( 'userlangattributes' ) ?>>
<?php
$notLoggedIn = '';
if ( !$this->getSkin()->getUser()->isLoggedIn() &&
User::groupHasPermission( '*', 'edit' )
) {
$notLoggedIn =
Html::rawElement( 'li',
[ 'id' => 'pt-anonuserpage' ],
$this->getMsg( 'notloggedin' )->escaped()
);
}
$personalTools = $this->getPersonalTools();
$langSelector = '';
if ( array_key_exists( 'uls', $personalTools ) ) {
$langSelector = $this->makeListItem( 'uls', $personalTools[ 'uls' ] );
unset( $personalTools[ 'uls' ] );
}
echo $langSelector;
echo $notLoggedIn;
foreach ( $personalTools as $key => $item ) {
echo $this->makeListItem( $key, $item );
}
?>
</ul>
</div>
<?php
break;
case 'SEARCH':
?>
<div id="p-search" role="search">
<h3<?php $this->html( 'userlangattributes' ) ?>>
<label for="searchInput"><?php $this->msg( 'search' ) ?></label>
</h3>
<form action="<?php $this->text( 'wgScript' ) ?>" id="searchform">
<div<?php echo $this->config->get( 'VectorUseSimpleSearch' ) ? ' id="simpleSearch"' : '' ?>>
<?php
echo $this->makeSearchInput( [ 'id' => 'searchInput' ] );
echo Html::hidden( 'title', $this->get( 'searchtitle' ) );
/* We construct two buttons (for 'go' and 'fulltext' search modes),
* but only one will be visible and actionable at a time (they are
* overlaid on top of each other in CSS).
* * Browsers will use the 'fulltext' one by default (as it's the
* first in tree-order), which is desirable when they are unable
* to show search suggestions (either due to being broken or
* having JavaScript turned off).
* * The mediawiki.searchSuggest module, after doing tests for the
* broken browsers, removes the 'fulltext' button and handles
* 'fulltext' search itself; this will reveal the 'go' button and
* cause it to be used.
*/
echo $this->makeSearchButton(
'fulltext',
[ 'id' => 'mw-searchButton', 'class' => 'searchButton mw-fallbackSearchButton' ]
);
echo $this->makeSearchButton(
'go',
[ 'id' => 'searchButton', 'class' => 'searchButton' ]
);
?>
</div>
</form>
</div>
<?php
break;
}
}
}
/**
* @inheritDoc
*/
public function makeLink( $key, $item, $options = [] ) {
$html = parent::makeLink( $key, $item, $options );
// Add an extra wrapper because our CSS is weird
if ( isset( $options['vector-wrap'] ) && $options['vector-wrap'] ) {
$html = Html::rawElement( 'span', [], $html );
}
return $html;
}
/**
* @inheritDoc
*/
public function makeListItem( $key, $item, $options = [] ) {
// For fancy styling of watch/unwatch star
if (
$this->config->get( 'VectorUseIconWatch' )
&& ( $key === 'watch' || $key === 'unwatch' )
) {
$item['class'] = rtrim( 'icon ' . $item['class'], ' ' );
$item['primary'] = true;
}
// Add CSS class 'collapsible' to links which are not marked as "primary"
if (
isset( $options['vector-collapsible'] ) && $options['vector-collapsible'] ) {
$item['class'] = rtrim( 'collapsible ' . $item['class'], ' ' );
}
// We don't use this, prevent it from popping up in HTML output
unset( $item['redundant'] );
return parent::makeListItem( $key, $item, $options );
}
}