Files
mediawiki-theme/includes/Hooks.php
D. Rimron-Soutter ea5dbcb05a Initial commit
2025-11-13 16:59:49 +00:00

1203 lines
38 KiB
PHP

<?php
use MediaWiki\Html\Html;
use MediaWiki\Linker\Linker;
use MediaWiki\MediaWikiServices;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Title\Title;
/**
* Hooks for NextBestNetwork skin
*
* 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
*/
class NextBestNetworkHooks {
protected static $anchorID = 0;
protected static $realnames = [];
/**
* Is NextBestNetwork used as a skin for the current page?
*
* @return boolean
*/
public static function getSkinNextBestNetworkSkin() {
return $GLOBALS['wgOut']->getSkin()->getSkinName() === 'nextbestnetwork';
}
/**
* Expose NextBestNetworkSkinUseTooltips configuration variable
*
* @param array $vars
*/
public static function onResourceLoaderGetConfigVars( array &$vars ) {
$vars['wgNextBestNetworkSkinUseTooltips'] = $GLOBALS['wgNextBestNetworkSkinUseTooltips'];
}
/**
* Setting up parser functions
*
* @param Parser $parser Parser object being initialized
*/
public static function onParserFirstCallInit( Parser $parser ) {
$parser->setHook( 'TOC', 'NextBestNetworkHooks::TOC' );
$parser->setHook( 'legend', 'NextBestNetworkHooks::legend' );
$parser->setHook( 'footer', 'NextBestNetworkHooks::footer' );
$parser->setHook( 'accordion', 'NextBestNetworkHooks::buildAccordion' );
$parser->setHook( 'label', 'NextBestNetworkHooks::buildLabel' );
if ( $GLOBALS['wgNextBestNetworkSkinUseBtnParser'] === true ) {
$parser->setHook( 'btn', 'NextBestNetworkHooks::buildButtons' );
}
$parser->setFunctionHook( 'nextbestnetworkhide', 'NextBestNetworkHooks::setHiddenElements' );
$parser->setFunctionHook( 'nextbestnetworknav', 'NextBestNetworkHooks::setCustomNavElement' );
$parser->setFunctionHook( 'nextbestnetworkhideexcept', 'NextBestNetworkHooks::setHiddenElementsGroups' );
$parser->setFunctionHook( 'nextbestnetworkbodyclass', 'NextBestNetworkHooks::addBodyclass' );
$parser->setFunctionHook( 'nextbestnetworkrealname', 'NextBestNetworkHooks::renderRealname' );
}
/**
* Adding modules
*
* @param OutputPage $out The OutputPage object.
* @param Skin $skin Skin object that will be used to generate the page
*/
public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
if( $skin->getSkinName() == 'nextbestnetwork' ) {
$config = \MediaWiki\MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'nextbestnetwork' );
$styles = [];
// load mediawiki styles
$styles[] = 'skins.nextbestnetwork.mediawiki.styles';
// load font awesome
$styles[] = 'skins.nextbestnetwork.awesome.styles';
// load externally defined style module
if( $config->get( 'NextBestNetworkSkinCustomStyleModule' ) ) {
$styles[] = $config->get( 'NextBestNetworkSkinCustomStyleModule' );
// or: load modules defined by nextbestnetwork
} else {
if( !$config->get( 'NextBestNetworkSkinUseCustomFiles' ) ) {
$styles[] = 'skins.nextbestnetwork.styles';
} else {
$styles[] = 'skins.nextbestnetwork.custom.styles';
}
}
// load external link styles
if( $config->get( 'NextBestNetworkSkinUseExternallinkStyles' ) ) {
$styles[] = 'skins.nextbestnetwork.externallinks.styles';
}
// load additional modules
foreach( $GLOBALS['wgNextBestNetworkSkinCustomCSS'] as $customstyle ) {
$styles[] = $customstyle;
}
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->run( 'SkinNextBestNetworkStyleModules', [ $skin, &$styles ] );
$out->addModuleStyles( $styles );
}
}
/**
* GetPreferences hook
*
* Adds NextBestNetwork-releated items to the preferences
*
* @param User $user User whose preferences are being modified
* @param array $preferences Preferences description array, to be fed to an HTMLForm object
*/
public static function onGetPreferences( User $user, array &$preferences ) {
$preferences['nextbestnetwork-advanced'] = [
'type' => 'toggle',
'label-message' => 'prefs-nextbestnetwork-advanced-desc',
'section' => 'rendering/nextbestnetwork-advanced',
'help-message' => 'prefs-nextbestnetwork-advanced-help'
];
}
/**
* Pages could be hidden for anonymous users or only be shown for specific groups
* so we put a user's group memberships into the page rendering hash
*
* @param string $confstr Reference to a hash key string which can be modified
* @param User $user User object that is requesting the page
* @param array $options Array of options used to generate the $confstr hash key
*/
static function onPageRenderingHash( &$confstr, User $user, array &$options ) {
$userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
$groups = $userGroupManager->getUserEffectiveGroups($user);
sort( $groups );
$confstr .= "!groups=" . join(',', $groups );
}
/**
* Add body classes
*
* @param OutputPage $out The OutputPage which called the hook, can be used to get the real title
* @param Skin $sk The Skin that called OutputPage::headElement
* @param array $bodyAttrs An array of attributes for the body tag passed to Html::openElement
*/
static function onOutputPageBodyAttributes( OutputPage $out, Skin $sk, array &$bodyAttrs ) {
if( $sk->getSkinName() == 'nextbestnetwork' ) {
$additionalBodyClasses = [ 'nextbestnetwork-animateLayout' ];
$user = $out->getUser();
$userAdvanced = MediaWikiServices::getInstance()->getUserOptionsLookup()->getOption( $user, 'nextbestnetwork-advanced' );
$additionalBodyClasses[] = $userAdvanced ? 'nextbestnetwork-advanced' : 'nextbestnetwork-non-advanced';
$additionalBodyClasses[] = $user->isRegistered() ? 'nextbestnetwork-user-logged-in' : 'nextbestnetwork-user-anon';
$additionalBodyClasses = array_merge( $additionalBodyClasses, $GLOBALS['wgNextBestNetworkSkinAdditionalBodyClasses'] );
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->run( 'SkinNextBestNetworkAdditionalBodyClasses', [ $sk, &$additionalBodyClasses ] );
if( count( $additionalBodyClasses ) > 0 ) {
$bodyAttrs['class'] = $bodyAttrs['class'] . ' ' . preg_replace( "/[^a-zA-Z0-9_\s-]/", "", implode( " ", $additionalBodyClasses ) );
}
}
}
/**
* Use real names instead of user names
*
* @param LinkRenderer $linkRenderer the LinkRenderer object
* @param LinkTarget $target the LinkTarget that the link is pointing to
* @param boolean $isKnown boolean indicating whether the page is known or not
* @param string|HtmlArmor $text the contents that the <a> tag should have; either a plain, unescaped string or a HtmlArmor object.
* @param array $attribs the final HTML attributes of the <a> tag, after processing, in associative array form.
* @param string $ret the value to return if your hook returns false.
*/
public static function onHtmlPageLinkRendererEnd( LinkRenderer $linkRenderer, LinkTarget $target, $isKnown, &$text, array &$attribs, &$ret ) {
if( $GLOBALS['wgNextBestNetworkSkinUseRealnames'] == true && $target->getNamespace() === 2 ) {
$userkey = $target->getDBKey();
// use real name if link text hadn't been set explicitly to be different from the page name
$title = Title::newFromText( HtmlArmor::getHtml( $text ) );
if(
$title &&
(
$title->getPrefixedText() == $target->getPrefixedText()
|| $title->getText() == $target->getText()
)
) {
$text = self::getRealname( $userkey );
}
}
}
/**
* Use real names instead of user names in selflinks
*
* @param Title $nt the title object of the page
* @param string $html Link text
* @param string $trail Text after link
* @param string $prefix Text before link
* @param string $ret Self link text to be used if the hook returns false
*/
public static function onSelfLinkBegin( Title $nt, &$html, &$trail, &$prefix, &$ret ) {
if( $GLOBALS['wgNextBestNetworkSkinUseRealnames'] == true && $nt->getNamespace() === 2 ) {
$userkey = $nt->getDBKey();
// use real name if link text hadn't been set explicitly to be different from the page name
$title = Title::newFromText( HtmlArmor::getHtml( $html ) );
if(
$title &&
(
$title->getPrefixedText() == $nt->getPrefixedText()
|| $title->getText() == $nt->getText()
)
) {
$html = self::getRealname( $userkey );
}
}
}
/**
* Customize edit section links
*
* @param Skin $skin Skin object rendering the UI
* @param Title $title Title object for the title being linked to
* @param string $section The designation of the section being pointed to, to be included in the link
* @param string $tooltip The default tooltip.
* @param array $links Array containing all link detail arrays.
* @param string $lang The language code to use for the link in the wfMessage function
*
* @todo: make this work with VisualEditor
*/
static function onSkinEditSectionLinks( Skin $skin, Title $title, $section, $tooltip, array &$links, $lang = false ) {
if(
$skin->getSkinName() == 'nextbestnetwork'
&& $GLOBALS['wgNextBestNetworkSkinCustomEditSectionLink'] == true
) {
if ( version_compare( MW_VERSION, '1.40', '<' ) ) {
$icon = wfMessage( 'nextbestnetwork-editsection-icon' )->inLanguage( $lang )->parse();
$text = wfMessage( 'nextbestnetwork-editsection-text' )->inLanguage( $lang )->parse();
$class = wfMessage( 'nextbestnetwork-editsection-class' )->inLanguage( $lang )->parse();
$text = new HtmlArmor( $icon . ( ( $icon != '' ) ? ' ' : '' ) . $text );
$links['editsection']['text'] = $text;
$links['editsection']['attribs']['class'] = $class;
} else {
$links['editsection']['link-html'] = wfMessage( 'nextbestnetwork-editsection-icon' )->inLanguage( $lang )->parse();
$links['editsection']['text'] = wfMessage( 'nextbestnetwork-editsection-text' )->inLanguage( $lang )->parse();
$links['editsection']['attribs']['class'] = wfMessage( 'nextbestnetwork-editsection-class' )->inLanguage( $lang )->parse();
}
}
return false;
}
/**
* Change TOC and page content of file pages to togglable tabs
*
* @param OutputPage $outputPage
*/
public static function onAfterFinalPageOutput( OutputPage $outputPage ) {
if(
$outputPage->getSkin()->getSkinName() == 'nextbestnetwork'
&& $outputPage->getTitle()->getNamespace() == 6
&& $GLOBALS['wgNextBestNetworkSkinImagePageTOCTabs'] == true
) {
$out = ob_get_clean();
$out = str_replace( '<ul id="filetoc">', '<ul id="tw-filetoc" class="nav nav-tabs nav-justified" role="tablist">', $out );
$out = str_replace( '<li><a href="#file">', '<li class="nav-item"><a href="#file" id="file-tab" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="file" aria-selected="true">', $out );
$out = str_replace( '<li><a href="#filehistory">', '<li class="nav-item"><a href="#filehistory" id="filehistory-tab" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="filehistory" aria-selected="false">', $out );
$out = str_replace( '<li><a href="#filelinks">', '<li class="nav-item"><a href="#filelinks" id="filelinks-tab" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="filelinks" aria-selected="false">', $out );
$out = str_replace( '<li><a href="#metadata">', '<li class="nav-item"><a href="#metadata" id="metadata-tab" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="metadata" aria-selected="false">', $out );
$out = str_replace( '<div class="fullImageLink" id="file"', '<div class="tab-content"><div id="file" class="tab-pane fade show active" role="tabpanel" aria-labelledby="file-tab"><div class="fullImageLink"', $out );
$out = str_replace( '<h2 id="filehistory"', '</div><div id="filehistory" class="tab-pane fade" role="tabpanel" aria-labelledby="filehistory-tab"><h2', $out );
$out = str_replace( '<h2 id="filelinks"', '</div><div id="filelinks" class="tab-pane fade" role="tabpanel" aria-labelledby="filelinks-tab"><h2', $out );
$out = str_replace( '<h2 id="metadata"', '</div><div id="metadata" class="tab-pane fade" role="tabpanel" aria-labelledby="metadata-tab"><h2', $out );
$out = $out . '</div></div>';
ob_start();
echo $out;
}
}
/**
* Enable use of <toc> tag
*/
public static function TOC( $input, array $args, Parser $parser, PPFrame $frame ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return [];
}
return [ '<div class="nextbestnetwork-toc">' . $parser->recursiveTagParse( $input ) . '</div>' ];
}
/**
* Enable use of <legend> tag
*/
public static function legend( $input, array $args, Parser $parser, PPFrame $frame ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return [];
}
return [ '<legend>' . $parser->recursiveTagParse( $input ) . '</legend>', "markerType" => 'nowiki' ];
}
/**
* Enable use of <footer> tag
*/
public static function footer( $input, array $args, Parser $parser, PPFrame $frame ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return [];
}
return [ '<footer>' . $parser->recursiveTagParse( $input ) . '</footer>', "markerType" => 'nowiki' ];
}
/**
* Enable use of <accordion> tag
*/
public static function buildAccordion( $input, array $args, Parser $parser, PPFrame $frame ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
static::$anchorID++;
$parent = $parser->recursiveTagParse( $args['parent'], $frame );
$card = '
<div class="accordion-item">
<h2 class="accordion-header" id="' . $parent . static::$anchorID . 'Heading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#' . $parent . static::$anchorID . '" aria-expanded="' . ( isset( $args['class'] ) && $args['class'] == 'show' ? 'true' : 'false' ) . '" aria-controls="' . $parent . static::$anchorID . '">
' . $parser->recursiveTagParse( $args['heading'], $frame ) . '
</button>
</h2>
<div id="' . $parent . static::$anchorID . '" class="accordion-collapse collapse ' . ( isset( $args['class'] ) ? htmlentities( $args['class'] ) : '' ) . '" ' . (!isset( $args['alwaysopen'] ) ? 'data-bs-parent="#' . $parent . '"' : '' ) . '>
<div class="accordion-body">' . $parser->recursiveTagParse( $input, $frame ) . '</div>
</div>
</div>';
return $card;
}
/**
* Enable use of <label> tag
*/
public static function buildLabel( $input, array $args, Parser $parser, PPFrame $frame ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
return '<label>' . $parser->recursiveTagParse( $input ) . '</label>';
}
/**
* Enable use of <btn> tag
*/
public static function buildButtons( $input, array $args, Parser $parser, PPFrame $frame ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
$sizes = [
'large' => 'btn-lg',
'lg' => 'btn-lg',
'small' => 'btn-sm',
'sm' => 'btn-sm',
'mini' => 'btn-xs',
'xs' => 'btn-xs'
];
$renderedButtons = '';
$buttongroups = preg_split( '/\n{2,}/', $input );
// set standard classes for all buttons in the group
if ( !isset( $args['class'] ) ) {
$args['class'][] = 'btn btn-secondary';
}
else {
$args['class'] = explode( ' ', $args['class'] );
}
if ( isset( $args['size'] ) ) {
if ( isset( $sizes[$args['size']] ) ) {
$args['class'][] = $sizes[$args['size']];
}
}
foreach ( $buttongroups as $buttongroup ) {
$buttons = [];
$buttons = NextBestNetworkHooks::parseButtons( $buttongroup, $parser, $frame );
$renderedButtons .= NextBestNetworkHooks::renderButtons( $buttons, $args );
}
// more than one buttongroup build a toolbar
if ( count( $buttongroups ) > 1 ) {
$renderedButtons = '<div class="btn-toolbar">' . $renderedButtons . '</div>';
}
return $renderedButtons;
}
/**
* Set elements that should be hidden
*/
static function setHiddenElements( Parser $parser ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
$parser->getOutput()->updateCacheExpiry(0);
for ( $i = 1; $i < func_num_args(); $i++ ) {
if ( in_array ( func_get_arg( $i ), $GLOBALS['wgNextBestNetworkSkinHideable'] ) ) {
$GLOBALS['wgNextBestNetworkSkinHideAll'][] = func_get_arg( $i );
}
}
return '';
}
/**
* Set elements that should be hidden except for members of specific groups
*/
static function setHiddenElementsGroups( Parser $parser ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
$parser->getOutput()->updateCacheExpiry(0);
$groups_except = explode( ',', func_get_arg( 1 ) );
$userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
$groups_user = $userGroupManager->getUserEffectiveGroups($parser->getUserIdentity());
if( count( array_intersect( $groups_except, $groups_user ) ) == 0 ) {
for ( $i = 2; $i < func_num_args(); $i++ ) {
if ( in_array ( func_get_arg( $i ), $GLOBALS['wgNextBestNetworkSkinHideable'] ) ) {
$GLOBALS['wgNextBestNetworkSkinHideAll'][] = func_get_arg( $i );
}
}
}
return '';
}
/**
* Customize a navigational element's content
*/
static function setCustomNavElement( Parser $parser, $element, ... $content ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
$parser->getOutput()->updateCacheExpiry(0);
$GLOBALS['wgNextBestNetworkSkinCustomNav'][$element] = join( '|', $content );
return '';
}
/**
* Add classes to body
*/
static function addBodyclass( Parser $parser ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return '';
}
$parser->getOutput()->updateCacheExpiry(0);
for ( $i = 1; $i < func_num_args(); $i++ ) {
$GLOBALS['wgNextBestNetworkSkinAdditionalBodyClasses'][] = func_get_arg( $i );
}
return '';
}
/**
* Render a user's real name
*
* @param Parser $parser
* @param String $user User name
* @return array User's real name
*/
static function renderRealName( Parser $parser, $user ) {
if ( !self::getSkinNextBestNetworkSkin() ) {
return [];
}
$realname = self::getRealname( $user );
return [ $realname ];
}
/**
* Parse string input into array
*
* @param string $buttongroup one or more buttons
* @param Parser $parser current parser
* @param PPFrame $frame current frame
* @return array
*/
static function parseButtons( $buttongroup, Parser $parser, $frame ) {
$buttons = [];
$lines = explode( "\n", $buttongroup );
foreach ( $lines as $line ) {
// empty line
if ( trim( $line ) == "" ) {
continue;
}
// simple buttons
if ( strpos( $line, '*' ) !== 0 ) {
$buttons = array_merge( $buttons, NextBestNetworkHooks::parseButtonLink( trim( $line ), $parser, $frame ) );
end( $buttons );
$currentparentkey = key($buttons);
}
// dropdown menus
else {
// no parent set?
if ( count( $buttons ) == 0 ) {
continue;
}
$cleanline = ltrim( $line, '*' );
$cleanline = trim( $cleanline );
if ( !isset( $buttons[$currentparentkey]['items'] ) ) {
$buttons[$currentparentkey]['items'] = [];
}
// dropdown-headers (dropdown-lines that start with a colon)
if ( strpos( $cleanline, ':' ) === 0 ) {
$buttons[$currentparentkey]['items'][] = [ 'text' => ltrim( $cleanline, ':' ), 'header' => true ];
} else {
$buttons[$currentparentkey]['items'] = array_merge( $buttons[$currentparentkey]['items'], NextBestNetworkHooks::parseButtonLink( $cleanline, $parser, $frame ) );
}
}
}
return $buttons;
}
/**
* Parse specific link
*
* @param $line string
* @param Parser $parser current parser
* @param PPFrame $frame current frame
* @return array
*/
static function parseButtonLink( $line, Parser $parser, $frame ) {
$extraAttribs = [];
$href_implicit = false;
$active = false;
$current_title = $parser instanceof Parser ? $parser->getTitle() : null;
// semantic queries
if ( strpos( $line, '{{#ask:' ) === 0 ) {
if ( !is_null( $current_title ) && $current_title instanceof Title ) {
$semanticQuery = substr( $line, 7, -2 );
$semanticHitNumber = $parser->recursiveTagParse( '{{#ask:' . $semanticQuery . '|format=count}}', false );
if ( !is_numeric( $semanticHitNumber ) ) {
return [
[
'text' => $semanticQuery,
'href' => 'INVALID QUERY',
]
];
}
if( $semanticHitNumber < 1 ) {
return [
[
'html' => '<span class="dropdown-item-text">' . wfMessage( 'nextbestnetwork-no-entries' )->plain() . '</span>',
]
];
}
$semanticHits = $parser->recursiveTagParse( '{{#ask:' . $semanticQuery . '|link=none|format=array}}', false );
$semanticHits = explode( ',', $semanticHits );
$semanticLinks = [];
foreach ( $semanticHits as $semanticHit ) {
$semanticHit = rtrim( str_replace( '&lt;PROP&gt;', '|', $semanticHit ), '|' );
$semanticLink = NextBestNetworkHooks::parseButtonLink( $semanticHit, $parser, $frame );
$semanticLinks[] = $semanticLink[0];
}
return $semanticLinks;
}
else {
$text = 'INVALID-TITLE/QUERY-BROKEN';
}
}
$line = explode( '|', $line );
foreach ( $line as &$single_line ) {
$single_line = trim( $single_line );
}
// is the text explicitly set?
$href = $line[0];
if ( isset( $line[1] ) && $line[1] != "" ) {
$text = $line[1];
}
else {
$href_implicit = true;
$text = $line[0];
}
// parse text
$msgText = wfMessage( $text )->inContentLanguage();
if ( $msgText->exists() ) {
$text = $msgText->parse();
}
else {
if ( !is_null( $current_title ) && $current_title instanceof Title ) {
$text = $parser->recursiveTagParse( $text, $frame );
}
else {
$text = 'INVALID-TITLE/PARSER-BROKEN';
}
}
// parse href
$msgLink = wfMessage( $href )->inContentLanguage();
if ( $msgLink->exists() ) {
$href = $msgLink->parse();
}
else {
if ( !is_null( $current_title ) && $current_title instanceof Title ) {
$href = trim( $parser->replaceVariables( $href, $frame ) );
}
else {
$href = 'INVALID-HREF/PARSER-BROKEN';
}
}
if ( preg_match( '/^(?i:' . MediaWikiServices::getInstance()->getUrlUtils()->validProtocols() . ')/', $href ) ) {
// Parser::getExternalLinkAttribs won't work here because of the Namespace things
if ( $GLOBALS['wgNoFollowLinks'] && !wfMatchesDomainList( $href, $GLOBALS['wgNoFollowDomainExceptions'] ) ) {
$extraAttribs['rel'] = 'nofollow';
}
if ( $GLOBALS['wgExternalLinkTarget'] ) {
$extraAttribs['target'] = $GLOBALS['wgExternalLinkTarget'];
}
} else {
$title = Title::newFromText( $href );
if ( $title ) {
if( !is_null( $current_title ) && $current_title instanceof Title && $title->equals( $current_title ) ) {
$active = true;
}
$title = $title->fixSpecialName();
$href = $title->getLinkURL();
if( $GLOBALS['wgNextBestNetworkSkinUseRealnames'] == true && $title->exists() && $title->getNamespace() === 2 ) {
$userkey = $title->getDBKey();
// use real name if link text hadn't been set explicitly
if( $href_implicit ) {
$title_from_text = Title::newFromText( HtmlArmor::getHtml( $text ) );
if(
$title_from_text &&
(
$title->getPrefixedText() == $title_from_text->getPrefixedText()
|| $title->getText() == $title_from_text->getText()
)
) {
$text = self::getRealname( $userkey );
}
}
}
} else {
// allow empty first argument
if( $href != '' ) {
$href = 'INVALID-TITLE:' . $href;
}
}
}
if ( isset( $line[2] ) && $line[2] != "" ) {
$extraAttribs['class'] = $line[2];
}
$link = [
'html' => $text,
'href' => $href,
'href_implicit' => $href_implicit,
'active' => $active
];
if( $line[0] != '' ) {
$link['id'] = urlencode( strtolower( strtr( $line[0], ' ', '-' ) ) );
$link['id'] = 'n-' . Sanitizer::escapeIdForAttribute( $link['id'] );
}
$link = array_merge( $link, $extraAttribs );
return [ $link ];
}
/**
* Get a user's real name
*
* @param String $user User name
*
* @return String User's real name
*/
static function getRealname( $userkey ) {
if( !$userkey ) {
return $userkey;
}
if( !isset( self::$realnames[$userkey] ) ) {
$user = User::newFromName( $userkey );
self::$realnames[$userkey] = $user->getRealName() ?: $user->getName();
}
return self::$realnames[$userkey];
}
/**
* Render Buttons
*
* @param $buttons array
* @param $options Array
* @return String
*/
static function renderButtons( $buttons, $options = [] ) {
$renderedButtons = '';
$groupclass = [];
if ( isset( $options['class'] ) ) {
if ( !is_array( $options['class'] ) ) {
$options['class'] = explode( ' ', $options['class'] );
}
$groupclass = $options['class'];
}
$currentwrapperclass = '';
// set wrapper
$wrapper = 'div';
if ( isset( $options['wrapper'] ) ) {
$wrapper = $options['wrapper'];
}
foreach ( $buttons as $button ) {
$btnoptions = [];
// set classes for specific button
// explicit classes for the specific line?
if ( isset( $button['class'] ) ) {
$button['class'] = explode( ' ', $button['class'] );
}
else {
$button['class'] = $groupclass;
}
foreach ( $button['class'] as $btnclass ) {
if ( strpos( $btnclass, 'btn' ) === 0 ) {
$button['class'][] = 'btn';
break;
}
}
// set wrapper class
if ( isset( $options['wrapperclass'] ) ) {
$wrapperclass = $options['wrapperclass'];
}
else {
if ( in_array( 'btn', $button['class'] ) === false ) {
$wrapperclass = 'dropdown';
}
else {
$wrapperclass = 'btn-group me-2';
}
}
$button['class'] = implode( ' ', array_unique( $button['class'] ) );
if( isset( $button['links'] ) ) {
foreach( $button['links'] as &$link ) {
$link['class'] = $button['class'];
}
}
// set attributes
$allowed_attributes = [
'aria-controls',
'aria-expanded',
'aria-selected',
'aria-label',
'data-bs-target',
'data-bs-dismiss',
'data-bs-placement',
'data-bs-slide',
'title',
'role',
];
foreach( $allowed_attributes as $attribute ) {
if ( isset( $options[$attribute] ) ) {
$button[$attribute] = $options[$attribute];
}
}
// if data-bs-toggle attribute is set, unset wrapper and add attribute and toggle-class
if ( isset( $options['data-bs-toggle'] ) ) {
$wrapper = '';
$button['data-bs-toggle'] = $options['data-bs-toggle'];
$button['class'] .= ' ' . $options['data-bs-toggle'] . '-toggle';
}
// if html is not set, use text and sanitize it
if ( !isset( $button['html'] ) ) {
if( isset( $button['text'] ) ) {
$button['html'] = htmlspecialchars( $button['text'] );
}
else {
$button['html'] = '#';
}
}
// if glyphicon, fa or icon attribute is set, add icon to buttons
if ( isset( $options['icon'] ) ) {
$options['fa'] = $options['icon'];
}
if ( isset( $options['fa'] ) ) {
$button['html'] = '<span class="fa fa-' . $options['fa'] . '"></span> ' . $button['html'];
}
if ( isset( $options['glyphicon'] ) ) {
$button['html'] = '<span class="glyphicon glyphicon-' . $options['glyphicon'] . '"></span> ' . $button['html'];
}
// render wrapper
if (
( ( $currentwrapperclass != $wrapperclass || isset( $button['items'] ) ) && $wrapper != '' )
|| $wrapper == 'li'
) {
if ( $currentwrapperclass != '' ) {
$renderedButtons .= '</' . $wrapper . '>';
}
$renderedButtons .= '<' . $wrapper . ' class="' . $wrapperclass;
if ( isset( $button['active'] ) && $button['active'] === true ) {
$renderedButtons .= ' active';
}
if( isset( $button['items'] ) ) {
$renderedButtons .= ' dropdown';
}
if ( isset( $options['wrapperid'] ) ) {
$renderedButtons .= '" id="' . $options['wrapperid'];
}
$renderedButtons .= '">';
$currentwrapperclass = $wrapperclass;
}
// dropdown
if ( isset( $button['items'] ) ) {
if ( isset( $options['dropdownclass'] ) ) {
$renderedButtons .= NextBestNetworkHooks::buildDropdown( $button, $options['dropdownclass'] );
}
else {
$renderedButtons .= NextBestNetworkHooks::buildDropdown( $button );
}
}
// simple button
else {
$renderedButtons .= NextBestNetworkHooks::makeLink( $button, $btnoptions );
}
}
// close wrapper
if ( $wrapper != '' ) {
$renderedButtons .= '</' . $wrapper . '>';
}
return $renderedButtons;
}
/**
* Build dropdown
*
* @param array $dropdown
* @param string $dropdownclass
* @return String
*/
static function buildDropdown( $dropdown, $dropdownclass = '' ) {
$renderedDropdown = '';
// split dropdown
if ( isset( $dropdown['href_implicit'] ) && $dropdown['href_implicit'] === false ) {
$renderedDropdown .= NextBestNetworkHooks::makeLink( $dropdown );
$caret = [
'class' => 'dropdown-toggle dropdown-toggle-split ' . $dropdown['class'],
'href' => '#',
'data-bs-toggle' => 'dropdown',
'html' => '<span class="sr-only">Toggle Dropdown</span>',
'aria-haspopup' => 'true'
];
$renderedDropdown .= NextBestNetworkHooks::makeLink( $caret );
}
// ordinary dropdown
else {
$dropdown['class'] .= ' dropdown-toggle';
$dropdown['data-bs-toggle'] = 'dropdown';
$dropdown['href'] = '#';
$dropdown['aria-haspopup'] = 'true';
$renderedDropdown .= NextBestNetworkHooks::makeLink( $dropdown );
}
$renderedDropdown .= NextBestNetworkHooks::buildDropdownMenu( $dropdown['items'], $dropdownclass );
return $renderedDropdown;
}
/**
* Build dropdown-menu (ul)
*
* @param array $dropdownmenu
* @param string $dropdownclass
* @return String
*/
static function buildDropdownMenu( array $dropdownmenu, $dropdownclass ) {
$renderedMenu = '<div class="dropdown-menu ' . $dropdownclass . '">';
foreach ( $dropdownmenu as $entry ) {
// divider
if ( ( !isset( $entry['text'] ) || $entry['text'] == "" ) // no 'text'
&& ( !isset( $entry['html'] ) || $entry['html'] == "" ) // and no 'html'
) {
$renderedMenu .= '<div class="dropdown-divider"></div>';
}
// header
elseif ( isset( $entry['text'] ) && isset( $entry['header'] ) && $entry['header'] ) {
$renderedMenu .= '<h6 class="dropdown-header">' . $entry['text'] . '</h6>';
}
// standard menu entry
else {
$entry['tabindex'] = '-1';
$entry['class'] = 'dropdown-item';
$renderedMenu .= NextBestNetworkHooks::makeLink( $entry );
}
}
$renderedMenu .= '</div>';
return $renderedMenu;
}
/**
* Produce HTML for a link
*
* This is a slightly adapted copy of the makeLink function in Skin.php
* -> some of the changed parts are marked by comments //
*
* @param array $item
* @param array $options
*/
static function makeLink( array $item, array $options = [] ) {
// nextbestnetwork: nested links?
if ( isset( $item['links'] ) ) {
if( isset( $item['links'][0] ) ) {
$item = $item['links'][0];
} else {
return false;
}
}
// nextbestnetwork: get text
$text = '';
if ( isset( $item['text'] ) ) {
$text = $item['text'];
} else {
if( isset( $item['msg'] ) ) {
$msgText = wfMessage( $item['msg'] )->inContentLanguage();
if ( $msgText->exists() ) {
$text = $msgText->parse();
}
}
}
$html = htmlspecialchars( $text );
// nextbestnetwork: set raw html
if ( isset( $item['html'] )) {
$html = $item['html'];
}
// nextbestnetwork: set icons for individual buttons (used by some navigational elements)
if ( isset( $item['icon'] )) {
$html = '<span class="fa fa-' . $item['icon'] . '"></span> ' . $html;
}
if ( isset( $options['text-wrapper'] ) ) {
$wrapper = $options['text-wrapper'];
if ( isset( $wrapper['tag'] ) ) {
$wrapper = [ $wrapper ];
}
while ( count( $wrapper ) > 0 ) {
$element = array_pop( $wrapper );
$html = Html::rawElement( $element['tag'], $element['attributes'] ?? null, $html );
}
}
// nextbestnetwork: allow empty first argument in the <btn> tag
if( isset( $item['href'] ) && $item['href'] == '' ) {
unset( $item['href'] );
$options['link-fallback'] = 'span';
}
if ( isset( $item['href'] ) || isset( $options['link-fallback'] ) ) {
$attrs = $item;
foreach ( [ 'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary', 'href_implicit', 'items', 'icon', 'html', 'tooltip-params', 'exists', 'active' ] as $k ) {
unset( $attrs[$k] );
}
if ( isset( $attrs['data'] ) ) {
foreach ( $attrs['data'] as $key => $value ) {
$attrs[ 'data-' . $key ] = $value;
}
unset( $attrs[ 'data' ] );
}
if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
$item['single-id'] = $item['id'];
}
$tooltipParams = [];
if ( isset( $item['tooltip-params'] ) ) {
$tooltipParams = $item['tooltip-params'];
}
if ( isset( $item['single-id'] ) ) {
$tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;
if ( isset( $item['tooltiponly'] ) && $item['tooltiponly'] ) {
$title = Linker::titleAttrib( $item['single-id'], $tooltipOption, $tooltipParams );
if ( $title !== false ) {
$attrs['title'] = $title;
}
} else {
$tip = Linker::tooltipAndAccesskeyAttribs(
$item['single-id'],
$tooltipParams,
$tooltipOption
);
if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
$attrs['title'] = $tip['title'];
}
if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
$attrs['accesskey'] = $tip['accesskey'];
}
}
}
if ( isset( $options['link-class'] ) ) {
if ( isset( $attrs['class'] ) ) {
// In the future, this should accept an array of classes, not a string
if ( is_array( $attrs['class'] ) ) {
$attrs['class'][] = $options['link-class'];
} else {
$attrs['class'] .= " {$options['link-class']}";
}
} else {
$attrs['class'] = $options['link-class'];
}
}
// nextbestnetwork: pass on active class
if ( isset( $item['active'] ) && $item['active'] ) {
if ( !isset( $attrs['class'] ) ) {
$attrs['class'] = 'active';
} else {
$attrs['class'] = trim( $attrs['class'] . ' active' );
}
}
$html = Html::rawElement( isset( $attrs['href'] )
? 'a'
: $options['link-fallback'], $attrs, $html );
}
return $html;
}
/**
* Produce HTML for a list item
*
* This is a slightly adapted copy of the makeListItem function in SkinTemplate.php
* -> some of the changed parts are marked by comments //
*
* @param array $item
* @param array $options
*/
static function makeListItem( array $item, array $options = [] ) {
if ( isset( $item['links'] ) ) {
$html = '';
foreach ( $item['links'] as $linkKey => $link ) {
$html .= NextBestNetworkHooks::makeLink( $link, $options );
}
} else {
$link = $item;
// These keys are used by makeListItem and shouldn't be passed on to the link
foreach ( [ 'id', 'class', 'tag' ] as $k ) {
unset( $link[$k] );
}
if ( isset( $item['id'] ) && !isset( $item['single-id'] ) ) {
// The id goes on the <li> not on the <a> for single links
// but makeSidebarLink still needs to know what id to use when
// generating tooltips and accesskeys.
$link['single-id'] = $item['id'];
}
$html = NextBestNetworkHooks::makeLink( $link, $options );
}
$attrs = [];
foreach ( [ 'id', 'class' ] as $attr ) {
if ( isset( $item[$attr] ) ) {
$attrs[$attr] = $item[$attr];
}
}
if ( isset( $item['active'] ) && $item['active'] ) {
if ( !isset( $attrs['class'] ) ) {
$attrs['class'] = '';
}
$attrs['class'] .= ' active';
$attrs['class'] = trim( $attrs['class'] );
}
return Html::rawElement( isset( $options['tag'] ) ? $options['tag'] : 'li', $attrs, $html );
}
/**
* Implement numbered headings
*
* code from https://www.mediawiki.org/wiki/Extension:MagicNumberedHeadings
*/
/**
* @copyright Copyright © 2007, Purodha Blissenabch.
* @license GPL-2.0-or-later
*
* 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, version 2
* of the License.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* See the GNU General Public License for more details.
*/
public static function onMagicWordMagicWords( &$magicWords ) {
$magicWords[] = 'MAG_NUMBEREDHEADINGS';
return true;
}
public static function onMagicWordwgVariableIDs( &$wgVariableIDs ) {
$wgVariableIDs[] = 'MAG_NUMBEREDHEADINGS';
return true;
}
public static function onInternalParseBeforeLinks( &$parser, &$text, &$strip_state ) {
$id = 'MAG_NUMBEREDHEADINGS';
$magicWord = MediaWikiServices::getInstance()
->getMagicWordFactory()
->get( $id );
if ( $magicWord->matchAndRemove( $text ) ) {
return true; // needs reimplementation (with JS?), feature was removed from MW
// $parser->mOptions->setNumberHeadings( true );
}
return true;
}
}