Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1426502
SkinTemplate.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
55 KB
Referenced Files
None
Subscribers
None
SkinTemplate.php
View Options
<?php
/**
* Copyright © Gabriel Wicke -- http://www.aulinx.de/
*
* 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
*/
use
MediaWiki\Debug\MWDebug
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Language\LanguageCode
;
use
MediaWiki\Linker\Linker
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Message\Message
;
use
MediaWiki\Permissions\Authority
;
use
MediaWiki\ResourceLoader
as
RL
;
use
MediaWiki\Skin\SkinComponentUtils
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Specials\Contribute\ContributeFactory
;
use
MediaWiki\Title\Title
;
use
Wikimedia\Message\MessageSpecifier
;
/**
* Base class for QuickTemplate-based skins.
*
* The template data is filled in SkinTemplate::prepareQuickTemplate.
*
* @stable to extend
* @ingroup Skins
*/
class
SkinTemplate
extends
Skin
{
/**
* @var string For QuickTemplate, the name of the subclass which will
* actually fill the template.
*/
public
$template
;
/** @var string */
public
$thispage
;
/** @var string */
public
$titletxt
;
/** @var string */
public
$userpage
;
/** @var bool TODO: Rename this to $isRegistered (but that's a breaking change) */
public
$loggedin
;
/** @var string */
public
$username
;
/** @var array */
public
$userpageUrlDetails
;
/** @var bool */
private
$isTempUser
;
/** @var bool */
private
$isNamedUser
;
/** @var bool */
private
$isAnonUser
;
/** @var bool */
private
$templateContextSet
=
false
;
/** @var array|null */
private
$contentNavigationCached
;
/** @var array|null */
private
$portletsCached
;
/**
* Create the template engine object; we feed it a bunch of data
* and eventually it spits out some HTML. Should have interface
* roughly equivalent to PHPTAL 0.7.
*
* @param string $classname
* @return QuickTemplate
*/
protected
function
setupTemplate
(
$classname
)
{
return
new
$classname
(
$this
->
getConfig
()
);
}
/**
* @return QuickTemplate
*/
protected
function
setupTemplateForOutput
()
{
$this
->
setupTemplateContext
();
$template
=
$this
->
options
[
'template'
]
??
$this
->
template
;
if
(
!
$template
)
{
throw
new
RuntimeException
(
'SkinTemplate skins must define a `template` either as a public'
.
' property of by passing in a`template` option to the constructor.'
);
}
$tpl
=
$this
->
setupTemplate
(
$template
);
return
$tpl
;
}
/**
* Setup class properties that are necessary prior to calling
* setupTemplateForOutput. It must be called inside
* prepareQuickTemplate.
* This function may set local class properties that will be used
* by other methods, but should not make assumptions about the
* implementation of setupTemplateForOutput
* @since 1.35
*/
final
protected
function
setupTemplateContext
()
{
if
(
$this
->
templateContextSet
)
{
return
;
}
$request
=
$this
->
getRequest
();
$user
=
$this
->
getUser
();
$title
=
$this
->
getTitle
();
$this
->
thispage
=
$title
->
getPrefixedDBkey
();
$this
->
titletxt
=
$title
->
getPrefixedText
();
$userpageTitle
=
$user
->
getUserPage
();
$this
->
userpage
=
$userpageTitle
->
getPrefixedText
();
$this
->
loggedin
=
$user
->
isRegistered
();
$this
->
username
=
$user
->
getName
();
$this
->
isTempUser
=
$user
->
isTemp
();
$this
->
isNamedUser
=
$this
->
loggedin
&&
!
$this
->
isTempUser
;
$this
->
isAnonUser
=
$user
->
isAnon
();
if
(
$this
->
isNamedUser
)
{
$this
->
userpageUrlDetails
=
self
::
makeUrlDetails
(
$userpageTitle
);
}
else
{
# This won't be used in the standard skins, but we define it to preserve the interface
# To save time, we check for existence
$this
->
userpageUrlDetails
=
self
::
makeKnownUrlDetails
(
$userpageTitle
);
}
$this
->
templateContextSet
=
true
;
}
/**
* Subclasses not wishing to use the QuickTemplate
* render method can rewrite this method, for example to use
* TemplateParser::processTemplate
* @since 1.35
* @return string HTML is the contents of the body tag e.g. <body>...</body>
*/
public
function
generateHTML
()
{
$tpl
=
$this
->
prepareQuickTemplate
();
$options
=
$this
->
getOptions
();
$out
=
$this
->
getOutput
();
// execute template
ob_start
();
$tpl
->
execute
();
$html
=
ob_get_contents
();
ob_end_clean
();
return
$html
;
}
/**
* Initialize various variables and generate the template
* @stable to override
*/
public
function
outputPage
()
{
Profiler
::
instance
()->
setAllowOutput
();
$out
=
$this
->
getOutput
();
$this
->
initPage
(
$out
);
$out
->
addJsConfigVars
(
$this
->
getJsConfigVars
()
);
// result may be an error
echo
$this
->
generateHTML
();
}
/**
* @inheritDoc
*/
public
function
getTemplateData
()
{
return
parent
::
getTemplateData
()
+
$this
->
getPortletsTemplateData
();
}
/**
* initialize various variables and generate the template
*
* @since 1.23
* @return QuickTemplate The template to be executed by outputPage
*/
protected
function
prepareQuickTemplate
()
{
$title
=
$this
->
getTitle
();
$request
=
$this
->
getRequest
();
$out
=
$this
->
getOutput
();
$config
=
$this
->
getConfig
();
$tpl
=
$this
->
setupTemplateForOutput
();
$tpl
->
set
(
'title'
,
$out
->
getPageTitle
()
);
$tpl
->
set
(
'pagetitle'
,
$out
->
getHTMLTitle
()
);
$tpl
->
set
(
'thispage'
,
$this
->
thispage
);
$tpl
->
set
(
'titleprefixeddbkey'
,
$this
->
thispage
);
$tpl
->
set
(
'titletext'
,
$title
->
getText
()
);
$tpl
->
set
(
'articleid'
,
$title
->
getArticleID
()
);
$tpl
->
set
(
'isarticle'
,
$out
->
isArticle
()
);
$tpl
->
set
(
'subtitle'
,
$this
->
prepareSubtitle
()
);
$tpl
->
set
(
'undelete'
,
$this
->
prepareUndeleteLink
()
);
$tpl
->
set
(
'catlinks'
,
$this
->
getCategories
()
);
$feeds
=
$this
->
buildFeedUrls
();
$tpl
->
set
(
'feeds'
,
count
(
$feeds
)
?
$feeds
:
false
);
$tpl
->
set
(
'mimetype'
,
$config
->
get
(
MainConfigNames
::
MimeType
)
);
$tpl
->
set
(
'charset'
,
'UTF-8'
);
$tpl
->
set
(
'wgScript'
,
$config
->
get
(
MainConfigNames
::
Script
)
);
$tpl
->
set
(
'skinname'
,
$this
->
skinname
);
$tpl
->
set
(
'skinclass'
,
static
::
class
);
$tpl
->
set
(
'skin'
,
$this
);
$tpl
->
set
(
'printable'
,
$out
->
isPrintable
()
);
$tpl
->
set
(
'handheld'
,
$request
->
getBool
(
'handheld'
)
);
$tpl
->
set
(
'loggedin'
,
$this
->
loggedin
);
$tpl
->
set
(
'notspecialpage'
,
!
$title
->
isSpecialPage
()
);
$searchTitle
=
SpecialPage
::
newSearchPage
(
$this
->
getUser
()
);
$searchLink
=
$searchTitle
->
getLocalURL
();
$tpl
->
set
(
'searchaction'
,
$searchLink
);
$tpl
->
deprecate
(
'searchaction'
,
'1.36'
);
$tpl
->
set
(
'searchtitle'
,
$searchTitle
->
getPrefixedDBkey
()
);
$tpl
->
set
(
'search'
,
trim
(
$request
->
getVal
(
'search'
,
''
)
)
);
$tpl
->
set
(
'stylepath'
,
$config
->
get
(
MainConfigNames
::
StylePath
)
);
$tpl
->
set
(
'articlepath'
,
$config
->
get
(
MainConfigNames
::
ArticlePath
)
);
$tpl
->
set
(
'scriptpath'
,
$config
->
get
(
MainConfigNames
::
ScriptPath
)
);
$tpl
->
set
(
'serverurl'
,
$config
->
get
(
MainConfigNames
::
Server
)
);
$tpl
->
set
(
'sitename'
,
$config
->
get
(
MainConfigNames
::
Sitename
)
);
$userLang
=
$this
->
getLanguage
();
$userLangCode
=
$userLang
->
getHtmlCode
();
$userLangDir
=
$userLang
->
getDir
();
$tpl
->
set
(
'lang'
,
$userLangCode
);
$tpl
->
set
(
'dir'
,
$userLangDir
);
$tpl
->
set
(
'rtl'
,
$userLang
->
isRTL
()
);
$logos
=
RL\SkinModule
::
getAvailableLogos
(
$config
,
$userLangCode
);
$tpl
->
set
(
'logopath'
,
$logos
[
'1x'
]
);
$tpl
->
set
(
'showjumplinks'
,
true
);
// showjumplinks preference has been removed
$tpl
->
set
(
'username'
,
$this
->
loggedin
?
$this
->
username
:
null
);
$tpl
->
set
(
'userpage'
,
$this
->
userpage
);
$tpl
->
set
(
'userpageurl'
,
$this
->
userpageUrlDetails
[
'href'
]
);
$tpl
->
set
(
'userlang'
,
$userLangCode
);
// Users can have their language set differently than the
// content of the wiki. For these users, tell the web browser
// that interface elements are in a different language.
$tpl
->
set
(
'userlangattributes'
,
$this
->
prepareUserLanguageAttributes
()
);
$tpl
->
set
(
'specialpageattributes'
,
''
);
# obsolete
// Used by VectorBeta to insert HTML before content but after the
// heading for the page title. Defaults to empty string.
$tpl
->
set
(
'prebodyhtml'
,
''
);
$tpl
->
set
(
'newtalk'
,
$this
->
getNewtalks
()
);
$tpl
->
set
(
'logo'
,
$this
->
logoText
()
);
$footerData
=
$this
->
getComponent
(
'footer'
)->
getTemplateData
();
$tpl
->
set
(
'copyright'
,
$footerData
[
'info'
][
'copyright'
]
??
false
);
// No longer used
$tpl
->
set
(
'viewcount'
,
false
);
$tpl
->
set
(
'lastmod'
,
$footerData
[
'info'
][
'lastmod'
]
??
false
);
$tpl
->
set
(
'credits'
,
$footerData
[
'info'
][
'credits'
]
??
false
);
$tpl
->
set
(
'numberofwatchingusers'
,
false
);
$tpl
->
set
(
'disclaimer'
,
$footerData
[
'places'
][
'disclaimer'
]
??
false
);
$tpl
->
set
(
'privacy'
,
$footerData
[
'places'
][
'privacy'
]
??
false
);
$tpl
->
set
(
'about'
,
$footerData
[
'places'
][
'about'
]
??
false
);
// Flatten for compat with the 'footerlinks' key in QuickTemplate-based skins.
$flattenedfooterlinks
=
[];
foreach
(
$footerData
as
$category
=>
$data
)
{
if
(
$category
!==
'data-icons'
)
{
foreach
(
$data
[
'array-items'
]
as
$item
)
{
$key
=
str_replace
(
'data-'
,
''
,
$category
);
$flattenedfooterlinks
[
$key
][]
=
$item
[
'name'
];
// For full support with BaseTemplate we also need to
// copy over the keys.
$tpl
->
set
(
$item
[
'name'
],
$item
[
'html'
]
);
}
}
}
$tpl
->
set
(
'footerlinks'
,
$flattenedfooterlinks
);
$tpl
->
set
(
'footericons'
,
$this
->
getFooterIcons
()
);
$tpl
->
set
(
'indicators'
,
$out
->
getIndicators
()
);
$tpl
->
set
(
'sitenotice'
,
$this
->
getSiteNotice
()
);
$tpl
->
set
(
'printfooter'
,
$this
->
printSource
()
);
// Wrap the bodyText with #mw-content-text element
$tpl
->
set
(
'bodytext'
,
$this
->
wrapHTML
(
$title
,
$out
->
getHTML
()
)
);
$tpl
->
set
(
'language_urls'
,
$this
->
getLanguages
()
?:
false
);
$content_navigation
=
$this
->
buildContentNavigationUrlsInternal
();
# Personal toolbar
$tpl
->
set
(
'personal_urls'
,
$this
->
makeSkinTemplatePersonalUrls
(
$content_navigation
)
);
// The user-menu, notifications, and user-interface-preferences are new content navigation entries which aren't
// expected to be part of content_navigation or content_actions. Adding them in there breaks skins that do not
// expect it. (See T316196)
unset
(
$content_navigation
[
'user-menu'
],
$content_navigation
[
'notifications'
],
$content_navigation
[
'user-page'
],
$content_navigation
[
'user-interface-preferences'
],
$content_navigation
[
'category-normal'
],
$content_navigation
[
'category-hidden'
],
$content_navigation
[
'associated-pages'
]
);
$content_actions
=
$this
->
buildContentActionUrls
(
$content_navigation
);
$tpl
->
set
(
'content_navigation'
,
$content_navigation
);
$tpl
->
set
(
'content_actions'
,
$content_actions
);
$tpl
->
set
(
'sidebar'
,
$this
->
buildSidebar
()
);
$tpl
->
set
(
'nav_urls'
,
$this
->
buildNavUrls
()
);
$tpl
->
set
(
'debug'
,
''
);
$tpl
->
set
(
'debughtml'
,
MWDebug
::
getHTMLDebugLog
()
);
// Set the bodytext to another key so that skins can just output it on its own
// and output printfooter and debughtml separately
$tpl
->
set
(
'bodycontent'
,
$tpl
->
data
[
'bodytext'
]
);
// Append printfooter and debughtml onto bodytext so that skins that
// were already using bodytext before they were split out don't suddenly
// start not outputting information.
$tpl
->
data
[
'bodytext'
]
.=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'printfooter'
],
"
\n
{$tpl->data['printfooter']}"
)
.
"
\n
"
;
$tpl
->
data
[
'bodytext'
]
.=
$tpl
->
data
[
'debughtml'
];
// allow extensions adding stuff after the page content.
// See Skin::afterContentHook() for further documentation.
$tpl
->
set
(
'dataAfterContent'
,
$this
->
afterContentHook
()
);
return
$tpl
;
}
/**
* Get the HTML for the personal tools list
* @since 1.31
*
* @param array|null $personalTools
* @param array $options
* @return string
*/
public
function
makePersonalToolsList
(
$personalTools
=
null
,
$options
=
[]
)
{
$personalTools
??=
$this
->
getPersonalToolsForMakeListItem
(
$this
->
buildPersonalUrls
()
);
$html
=
''
;
foreach
(
$personalTools
as
$key
=>
$item
)
{
$html
.=
$this
->
makeListItem
(
$key
,
$item
,
$options
);
}
return
$html
;
}
/**
* Get personal tools for the user
*
* @since 1.31
*
* @return array[]
*/
public
function
getStructuredPersonalTools
()
{
return
$this
->
getPersonalToolsForMakeListItem
(
$this
->
buildPersonalUrls
()
);
}
/**
* Build array of urls for personal toolbar
*
* @param bool $includeNotifications Since 1.36, notifications are optional
* @return array
*/
protected
function
buildPersonalUrls
(
bool
$includeNotifications
=
true
)
{
$this
->
setupTemplateContext
();
$title
=
$this
->
getTitle
();
$authority
=
$this
->
getAuthority
();
$request
=
$this
->
getRequest
();
$pageurl
=
$title
->
getLocalURL
();
$services
=
MediaWikiServices
::
getInstance
();
$authManager
=
$services
->
getAuthManager
();
$groupPermissionsLookup
=
$services
->
getGroupPermissionsLookup
();
$tempUserConfig
=
$services
->
getTempUserConfig
();
$returnto
=
SkinComponentUtils
::
getReturnToParam
(
$title
,
$request
,
$authority
);
$shouldHideUserLinks
=
$this
->
isAnonUser
&&
$tempUserConfig
->
isKnown
();
/* set up the default links for the personal toolbar */
$personal_urls
=
[];
if
(
$this
->
loggedin
)
{
$this
->
addPersonalPageItem
(
$personal_urls
,
''
);
// Merge notifications into the personal menu for older skins.
if
(
$includeNotifications
)
{
$contentNavigation
=
$this
->
buildContentNavigationUrlsInternal
();
$personal_urls
+=
$contentNavigation
[
'notifications'
];
}
$usertalkUrlDetails
=
$this
->
makeTalkUrlDetails
(
$this
->
userpage
);
$personal_urls
[
'mytalk'
]
=
[
'text'
=>
$this
->
msg
(
'mytalk'
)->
text
(),
'href'
=>
&
$usertalkUrlDetails
[
'href'
],
'class'
=>
$usertalkUrlDetails
[
'exists'
]
?
false
:
'new'
,
'exists'
=>
$usertalkUrlDetails
[
'exists'
],
'active'
=>
(
$usertalkUrlDetails
[
'href'
]
==
$pageurl
),
'icon'
=>
'userTalk'
];
if
(
!
$this
->
isTempUser
)
{
$href
=
SkinComponentUtils
::
makeSpecialUrl
(
'Preferences'
);
$personal_urls
[
'preferences'
]
=
[
'text'
=>
$this
->
msg
(
'mypreferences'
)->
text
(),
'href'
=>
$href
,
'active'
=>
(
$href
==
$pageurl
),
'icon'
=>
'settings'
,
];
}
if
(
$authority
->
isAllowed
(
'viewmywatchlist'
)
)
{
$personal_urls
[
'watchlist'
]
=
self
::
buildWatchlistData
();
}
# We need to do an explicit check for Special:Contributions, as we
# have to match both the title, and the target, which could come
# from request values (Special:Contributions?target=Jimbo_Wales)
# or be specified in "subpage" form
# (Special:Contributions/Jimbo_Wales). The plot
# thickens, because the Title object is altered for special pages,
# so it doesn't contain the original alias-with-subpage.
$origTitle
=
Title
::
newFromText
(
$request
->
getText
(
'title'
)
);
if
(
$origTitle
instanceof
Title
&&
$origTitle
->
isSpecialPage
()
)
{
[
$spName
,
$spPar
]
=
MediaWikiServices
::
getInstance
()->
getSpecialPageFactory
()->
resolveAlias
(
$origTitle
->
getText
()
);
$active
=
$spName
==
'Contributions'
&&
(
(
$spPar
&&
$spPar
==
$this
->
username
)
||
$request
->
getText
(
'target'
)
==
$this
->
username
);
}
else
{
$active
=
false
;
}
$personal_urls
=
$this
->
makeContributionsLink
(
$personal_urls
,
'mycontris'
,
$this
->
username
,
$active
);
// if we can't set the user, we can't unset it either
if
(
$request
->
getSession
()->
canSetUser
()
)
{
$personal_urls
[
'logout'
]
=
$this
->
buildLogoutLinkData
();
}
}
elseif
(
!
$shouldHideUserLinks
)
{
$canEdit
=
$authority
->
isAllowed
(
'edit'
);
$canEditWithTemp
=
$tempUserConfig
->
isAutoCreateAction
(
'edit'
);
// No need to show Talk and Contributions to anons if they can't contribute!
if
(
$canEdit
||
$canEditWithTemp
)
{
// Non interactive placeholder for anonymous users.
// It's unstyled by default (black color). Skin that
// needs it, can style it using the 'pt-anonuserpage' id.
// Skin that does not need it should unset it.
$personal_urls
[
'anonuserpage'
]
=
[
'text'
=>
$this
->
msg
(
'notloggedin'
)->
text
(),
];
}
if
(
$canEdit
)
{
// Because of caching, we can't link directly to the IP talk and
// contributions pages. Instead we use the special page shortcuts
// (which work correctly regardless of caching). This means we can't
// determine whether these links are active or not, but since major
// skins (MonoBook, Vector) don't use this information, it's not a
// huge loss.
$personal_urls
[
'anontalk'
]
=
[
'text'
=>
$this
->
msg
(
'anontalk'
)->
text
(),
'href'
=>
SkinComponentUtils
::
makeSpecialUrlSubpage
(
'Mytalk'
,
false
),
'active'
=>
false
,
'icon'
=>
'userTalk'
,
];
$personal_urls
=
$this
->
makeContributionsLink
(
$personal_urls
,
'anoncontribs'
,
null
,
false
);
}
}
if
(
!
$this
->
loggedin
)
{
$useCombinedLoginLink
=
$this
->
useCombinedLoginLink
();
$login_url
=
$this
->
buildLoginData
(
$returnto
,
$useCombinedLoginLink
);
$createaccount_url
=
$this
->
buildCreateAccountData
(
$returnto
);
if
(
$authManager
->
canCreateAccounts
()
&&
$authority
->
isAllowed
(
'createaccount'
)
&&
!
$useCombinedLoginLink
)
{
$personal_urls
[
'createaccount'
]
=
$createaccount_url
;
}
if
(
$authManager
->
canAuthenticateNow
()
)
{
// TODO: easy way to get anon authority
$key
=
$groupPermissionsLookup
->
groupHasPermission
(
'*'
,
'read'
)
?
'login'
:
'login-private'
;
$personal_urls
[
$key
]
=
$login_url
;
}
}
return
$personal_urls
;
}
/**
* Returns if a combined login/signup link will be used
* @unstable
*
* @return bool
*/
protected
function
useCombinedLoginLink
()
{
$services
=
MediaWikiServices
::
getInstance
();
$authManager
=
$services
->
getAuthManager
();
$useCombinedLoginLink
=
$this
->
getConfig
()->
get
(
MainConfigNames
::
UseCombinedLoginLink
);
if
(
!
$authManager
->
canCreateAccounts
()
||
!
$authManager
->
canAuthenticateNow
()
)
{
// don't show combined login/signup link if one of those is actually not available
$useCombinedLoginLink
=
false
;
}
return
$useCombinedLoginLink
;
}
/**
* Build "Login" link
* @unstable
*
* @param string[] $returnto query params for the page to return to
* @param bool $useCombinedLoginLink when set a single link to login form will be created
* with alternative label.
* @return array
*/
protected
function
buildLoginData
(
$returnto
,
$useCombinedLoginLink
)
{
$title
=
$this
->
getTitle
();
$loginlink
=
$this
->
getAuthority
()->
isAllowed
(
'createaccount'
)
&&
$useCombinedLoginLink
?
'nav-login-createaccount'
:
'pt-login'
;
$login_url
=
[
'single-id'
=>
'pt-login'
,
'text'
=>
$this
->
msg
(
$loginlink
)->
text
(),
'href'
=>
SkinComponentUtils
::
makeSpecialUrl
(
'Userlogin'
,
$returnto
),
'active'
=>
$title
->
isSpecial
(
'Userlogin'
)
||
(
$title
->
isSpecial
(
'CreateAccount'
)
&&
$useCombinedLoginLink
),
'icon'
=>
'logIn'
];
return
$login_url
;
}
/**
* @param array $links return value from OutputPage::getCategoryLinks
* @return array of data
*/
private
function
getCategoryPortletsData
(
array
$links
):
array
{
$categories
=
[];
foreach
(
$links
as
$group
=>
$groupLinks
)
{
$allLinks
=
[];
$groupName
=
'category-'
.
$group
;
foreach
(
$groupLinks
as
$i
=>
$link
)
{
$allLinks
[
$groupName
.
'-'
.
$i
]
=
[
'html'
=>
$link
,
];
}
$categories
[
$groupName
]
=
$allLinks
;
}
return
$categories
;
}
/**
* Extends category links with Skin::getAfterPortlet functionality.
* @return string HTML
*/
public
function
getCategoryLinks
()
{
$afterPortlet
=
$this
->
getPortletsTemplateData
()[
'data-portlets'
][
'data-category-normal'
][
'html-after-portal'
]
??
''
;
return
parent
::
getCategoryLinks
()
.
$afterPortlet
;
}
/**
* @return array of portlet data for all portlets
*/
private
function
getPortletsTemplateData
()
{
if
(
$this
->
portletsCached
)
{
return
$this
->
portletsCached
;
}
$portlets
=
[];
$contentNavigation
=
$this
->
buildContentNavigationUrlsInternal
();
$sidebar
=
[];
$sidebarData
=
$this
->
buildSidebar
();
foreach
(
$sidebarData
as
$name
=>
$items
)
{
if
(
is_array
(
$items
)
)
{
// Numeric strings gets an integer when set as key, cast back - T73639
$name
=
(
string
)
$name
;
switch
(
$name
)
{
// ignore search
case
'SEARCH'
:
break
;
// Map toolbox to `tb` id.
case
'TOOLBOX'
:
$sidebar
[]
=
$this
->
getPortletData
(
'tb'
,
$items
);
break
;
// Languages is no longer be tied to sidebar
case
'LANGUAGES'
:
// The language portal will be added provided either
// languages exist or there is a value in html-after-portal
// for example to show the add language wikidata link (T252800)
$portal
=
$this
->
getPortletData
(
'lang'
,
$items
);
if
(
count
(
$items
)
||
$portal
[
'html-after-portal'
]
)
{
$portlets
[
'data-languages'
]
=
$portal
;
}
break
;
default
:
$sidebar
[]
=
$this
->
getPortletData
(
$name
,
$items
);
break
;
}
}
}
foreach
(
$contentNavigation
as
$name
=>
$items
)
{
if
(
$name
===
'user-menu'
)
{
$items
=
$this
->
getPersonalToolsForMakeListItem
(
$items
,
true
);
}
$portlets
[
'data-'
.
$name
]
=
$this
->
getPortletData
(
$name
,
$items
);
}
// A menu that includes the notifications. This will be deprecated in future versions
// of the skin API spec.
$portlets
[
'data-personal'
]
=
$this
->
getPortletData
(
'personal'
,
$this
->
getPersonalToolsForMakeListItem
(
$this
->
injectLegacyMenusIntoPersonalTools
(
$contentNavigation
)
)
);
$this
->
portletsCached
=
[
'data-portlets'
=>
$portlets
,
'data-portlets-sidebar'
=>
[
'data-portlets-first'
=>
$sidebar
[
0
]
??
null
,
'array-portlets-rest'
=>
array_slice
(
$sidebar
,
1
),
],
];
return
$this
->
portletsCached
;
}
/**
* Build data required for "Logout" link.
*
* @unstable
*
* @since 1.37
*
* @return array Array of data required to create a logout link.
*/
final
protected
function
buildLogoutLinkData
()
{
$title
=
$this
->
getTitle
();
$request
=
$this
->
getRequest
();
$authority
=
$this
->
getAuthority
();
$returnto
=
SkinComponentUtils
::
getReturnToParam
(
$title
,
$request
,
$authority
);
$isTemp
=
$this
->
isTempUser
;
$msg
=
$isTemp
?
'templogout'
:
'pt-userlogout'
;
return
[
'single-id'
=>
'pt-logout'
,
'text'
=>
$this
->
msg
(
$msg
)->
text
(),
'data-mw'
=>
'interface'
,
'href'
=>
SkinComponentUtils
::
makeSpecialUrl
(
'Userlogout'
,
// Note: userlogout link must always contain an & character, otherwise we might not be able
// to detect a buggy precaching proxy (T19790)
(
$title
->
isSpecial
(
'Preferences'
)
?
[]
:
$returnto
)
),
'active'
=>
false
,
'icon'
=>
'logOut'
];
}
/**
* Build "Create Account" link data.
* @unstable
*
* @param string[] $returnto query params for the page to return to
* @return array
*/
protected
function
buildCreateAccountData
(
$returnto
)
{
$title
=
$this
->
getTitle
();
return
[
'single-id'
=>
'pt-createaccount'
,
'text'
=>
$this
->
msg
(
'pt-createaccount'
)->
text
(),
'href'
=>
SkinComponentUtils
::
makeSpecialUrl
(
'CreateAccount'
,
$returnto
),
'active'
=>
$title
->
isSpecial
(
'CreateAccount'
),
'icon'
=>
'userAdd'
];
}
/**
* Add the userpage link to the array
*
* @param array &$links Links array to append to
* @param string $idSuffix Something to add to the IDs to make them unique
*/
private
function
addPersonalPageItem
(
&
$links
,
$idSuffix
)
{
if
(
$this
->
isNamedUser
)
{
// T340152
$links
[
'userpage'
]
=
$this
->
buildPersonalPageItem
(
'pt-userpage'
.
$idSuffix
);
}
}
/**
* Build a user page link data.
*
* @param string $id of user page item to be output in HTML attribute (optional)
* @return array
*/
protected
function
buildPersonalPageItem
(
$id
=
'pt-userpage'
):
array
{
$linkClasses
=
$this
->
userpageUrlDetails
[
'exists'
]
?
[]
:
[
'new'
];
// T335440 Temp accounts dont show a user page link
// But we still need to update the user icon, as its used by other UI elements
$icon
=
$this
->
isTempUser
?
'userTemporary'
:
'userAvatar'
;
$href
=
&
$this
->
userpageUrlDetails
[
'href'
];
return
[
'id'
=>
$id
,
'single-id'
=>
'pt-userpage'
,
'text'
=>
$this
->
username
,
'href'
=>
$href
,
'link-class'
=>
$linkClasses
,
'exists'
=>
$this
->
userpageUrlDetails
[
'exists'
],
'active'
=>
(
$this
->
userpageUrlDetails
[
'href'
]
==
$this
->
getTitle
()->
getLocalURL
()
),
'icon'
=>
$icon
,
];
}
/**
* Build a watchlist link data.
*
* @return array Array of data required to create a watchlist link.
*/
private
function
buildWatchlistData
()
{
$href
=
SkinComponentUtils
::
makeSpecialUrl
(
'Watchlist'
);
$pageurl
=
$this
->
getTitle
()->
getLocalURL
();
return
[
'single-id'
=>
'pt-watchlist'
,
'text'
=>
$this
->
msg
(
'mywatchlist'
)->
text
(),
'href'
=>
$href
,
'active'
=>
(
$href
==
$pageurl
),
'icon'
=>
'watchlist'
];
}
/**
* Builds an array with tab definition
*
* @param Title $title Page Where the tab links to
* @param string|string[]|MessageSpecifier $message Message or an array of message keys
* (will fall back)
* @param bool $selected Display the tab as selected
* @param string $query Query string attached to tab URL
* @param bool $checkEdit Check if $title exists and mark with .new if one doesn't
*
* @return array
* @param-taint $message tainted
*/
public
function
tabAction
(
$title
,
$message
,
$selected
,
$query
=
''
,
$checkEdit
=
false
)
{
$classes
=
[];
if
(
$selected
)
{
$classes
[]
=
'selected'
;
}
$exists
=
true
;
$services
=
MediaWikiServices
::
getInstance
();
$linkClass
=
$services
->
getLinkRenderer
()->
getLinkClasses
(
$title
);
if
(
$checkEdit
&&
!
$title
->
isKnown
()
)
{
// Selected tabs should not show as red link. It doesn't make sense
// to show a red link on a page the user has already navigated to.
// https://phabricator.wikimedia.org/T294129#7451549
if
(
!
$selected
)
{
// For historic reasons we add to the LI element
$classes
[]
=
'new'
;
// but adding the class to the A element is more appropriate.
$linkClass
.=
' new'
;
}
$exists
=
false
;
if
(
$query
!==
''
)
{
$query
=
'action=edit&redlink=1&'
.
$query
;
}
else
{
$query
=
'action=edit&redlink=1'
;
}
}
if
(
$message
instanceof
MessageSpecifier
)
{
$msg
=
new
Message
(
$message
);
}
else
{
// wfMessageFallback will nicely accept $message as an array of fallbacks
// or just a single key
$msg
=
wfMessageFallback
(
$message
);
}
$msg
->
setContext
(
$this
->
getContext
()
);
if
(
!
$msg
->
isDisabled
()
)
{
$text
=
$msg
->
text
();
}
else
{
$text
=
$services
->
getLanguageConverterFactory
()
->
getLanguageConverter
(
$services
->
getContentLanguage
()
)
->
convertNamespace
(
$services
->
getNamespaceInfo
()
->
getSubject
(
$title
->
getNamespace
()
)
);
}
$result
=
[
'class'
=>
implode
(
' '
,
$classes
),
'text'
=>
$text
,
'href'
=>
$title
->
getLocalURL
(
$query
),
'exists'
=>
$exists
,
'primary'
=>
true
];
if
(
$linkClass
!==
''
)
{
$result
[
'link-class'
]
=
trim
(
$linkClass
);
}
return
$result
;
}
/**
* Get a message label that skins can override.
*
* @param string $labelMessageKey
* @param mixed $param for the message
* @return string
*/
private
function
getSkinNavOverrideableLabel
(
$labelMessageKey
,
$param
=
null
)
{
$skname
=
$this
->
skinname
;
// The following messages can be used here:
// * skin-action-addsection
// * skin-action-delete
// * skin-action-move
// * skin-action-protect
// * skin-action-undelete
// * skin-action-unprotect
// * skin-action-viewdeleted
// * skin-action-viewsource
// * skin-view-create
// * skin-view-create-local
// * skin-view-edit
// * skin-view-edit-local
// * skin-view-foreign
// * skin-view-history
// * skin-view-view
$msg
=
wfMessageFallback
(
"$skname-$labelMessageKey"
,
"skin-$labelMessageKey"
)->
setContext
(
$this
->
getContext
()
);
if
(
$param
)
{
if
(
is_numeric
(
$param
)
)
{
$msg
->
numParams
(
$param
);
}
else
{
$msg
->
params
(
$param
);
}
}
return
$msg
->
text
();
}
/**
* @param string $name
* @param string|array $urlaction
* @return array
*/
private
function
makeTalkUrlDetails
(
$name
,
$urlaction
=
''
)
{
$title
=
Title
::
newFromTextThrow
(
$name
)->
getTalkPage
();
return
[
'href'
=>
$title
->
getLocalURL
(
$urlaction
),
'exists'
=>
$title
->
isKnown
(),
];
}
/**
* Get the attributes for the watch link.
* @param string $mode Either 'watch' or 'unwatch'
* @param Authority $performer
* @param Title $title
* @param string|null $action
* @param bool $onPage
* @return array
*/
private
function
getWatchLinkAttrs
(
string
$mode
,
Authority
$performer
,
Title
$title
,
?
string
$action
,
bool
$onPage
):
array
{
$isWatchMode
=
$action
==
'watch'
;
$class
=
'mw-watchlink '
.
(
$onPage
&&
(
$isWatchMode
||
$action
==
'unwatch'
)
?
'selected'
:
''
);
$services
=
MediaWikiServices
::
getInstance
();
$watchlistManager
=
$services
->
getWatchlistManager
();
$watchIcon
=
$watchlistManager
->
isWatched
(
$performer
,
$title
)
?
'unStar'
:
'star'
;
$watchExpiry
=
null
;
// Modify tooltip and add class identifying the page is temporarily watched, if applicable.
if
(
$this
->
getConfig
()->
get
(
MainConfigNames
::
WatchlistExpiry
)
&&
$watchlistManager
->
isTempWatched
(
$performer
,
$title
)
)
{
$class
.=
' mw-watchlink-temp'
;
$watchIcon
=
'halfStar'
;
$watchStore
=
$services
->
getWatchedItemStore
();
$watchedItem
=
$watchStore
->
getWatchedItem
(
$performer
->
getUser
(),
$title
);
$diffInDays
=
$watchedItem
->
getExpiryInDays
();
$watchExpiry
=
$watchedItem
->
getExpiry
(
TS_ISO_8601
);
if
(
$diffInDays
)
{
$msgParams
=
[
$diffInDays
];
// Resolves to tooltip-ca-unwatch-expiring message
$tooltip
=
'ca-unwatch-expiring'
;
}
else
{
// Resolves to tooltip-ca-unwatch-expiring-hours message
$tooltip
=
'ca-unwatch-expiring-hours'
;
}
}
return
[
'class'
=>
$class
,
'icon'
=>
$watchIcon
,
// uses 'watch' or 'unwatch' message
'text'
=>
$this
->
msg
(
$mode
)->
text
(),
'single-id'
=>
$tooltip
??
null
,
'tooltip-params'
=>
$msgParams
??
null
,
'href'
=>
$title
->
getLocalURL
(
[
'action'
=>
$mode
]
),
// Set a data-mw=interface attribute, which the mediawiki.page.ajax
// module will look for to make sure it's a trusted link
'data'
=>
[
'mw'
=>
'interface'
,
'mw-expiry'
=>
$watchExpiry
,
],
];
}
/**
* Run hooks relating to navigation menu data.
* Skins should extend this if they want to run opinionated transformations to the data after all
* hooks have been run. Note hooks are run in an arbitrary order.
*
* @param SkinTemplate $skin
* @param array &$content_navigation representing all menus.
* @since 1.37
*/
protected
function
runOnSkinTemplateNavigationHooks
(
SkinTemplate
$skin
,
&
$content_navigation
)
{
$beforeHookAssociatedPages
=
array_keys
(
$content_navigation
[
'associated-pages'
]
);
$beforeHookNamespaces
=
array_keys
(
$content_navigation
[
'namespaces'
]
);
// Equiv to SkinTemplateContentActions, run
$this
->
getHookRunner
()->
onSkinTemplateNavigation__Universal
(
$skin
,
$content_navigation
);
// The new `associatedPages` menu (added in 1.39)
// should be backwards compatibile with `namespaces`.
// To do this we look for hook modifications to both keys. If modifications are not
// made the new key, but are made to the old key, associatedPages reverts back to the
// links in the namespaces menu.
// It's expected in future that `namespaces` menu will become an alias for `associatedPages`
// at which point this code can be removed.
$afterHookNamespaces
=
array_keys
(
$content_navigation
[
'namespaces'
]
);
$afterHookAssociatedPages
=
array_keys
(
$content_navigation
[
'associated-pages'
]
);
$associatedPagesChanged
=
count
(
array_diff
(
$afterHookAssociatedPages
,
$beforeHookAssociatedPages
)
)
>
0
;
$namespacesChanged
=
count
(
array_diff
(
$afterHookNamespaces
,
$beforeHookNamespaces
)
)
>
0
;
// If some change occurred to namespaces via the hook, revert back to namespaces.
if
(
!
$associatedPagesChanged
&&
$namespacesChanged
)
{
$content_navigation
[
'associated-pages'
]
=
$content_navigation
[
'namespaces'
];
}
}
/**
* a structured array of links usually used for the tabs in a skin
*
* There are 4 standard sections
* namespaces: Used for namespace tabs like special, page, and talk namespaces
* views: Used for primary page views like read, edit, history
* actions: Used for most extra page actions like deletion, protection, etc...
* variants: Used to list the language variants for the page
*
* Each section's value is a key/value array of links for that section.
* The links themselves have these common keys:
* - class: The css classes to apply to the tab
* - text: The text to display on the tab
* - href: The href for the tab to point to
* - rel: An optional rel= for the tab's link
* - redundant: If true the tab will be dropped in skins using content_actions
* this is useful for tabs like "Read" which only have meaning in skins that
* take special meaning from the grouped structure of content_navigation
*
* Views also have an extra key which can be used:
* - primary: If this is not true skins like vector may try to hide the tab
* when the user has limited space in their browser window
*
* content_navigation using code also expects these ids to be present on the
* links, however these are usually automatically generated by SkinTemplate
* itself and are not necessary when using a hook. The only things these may
* matter to are people modifying content_navigation after it's initial creation:
* - id: A "preferred" id, most skins are best off outputting this preferred
* id for best compatibility.
* - tooltiponly: This is set to true for some tabs in cases where the system
* believes that the accesskey should not be added to the tab.
*
* @return array
*/
private
function
buildContentNavigationUrlsInternal
()
{
if
(
$this
->
contentNavigationCached
)
{
return
$this
->
contentNavigationCached
;
}
// Display tabs for the relevant title rather than always the title itself
$title
=
$this
->
getRelevantTitle
();
$onPage
=
$title
->
equals
(
$this
->
getTitle
()
);
$out
=
$this
->
getOutput
();
$request
=
$this
->
getRequest
();
$performer
=
$this
->
getAuthority
();
$action
=
$this
->
getContext
()->
getActionName
();
$services
=
MediaWikiServices
::
getInstance
();
$permissionManager
=
$services
->
getPermissionManager
();
$categoriesData
=
$this
->
getCategoryPortletsData
(
$this
->
getOutput
()->
getCategoryLinks
()
);
$userPageLink
=
[];
$this
->
addPersonalPageItem
(
$userPageLink
,
'-2'
);
$content_navigation
=
$categoriesData
+
[
// Modern keys: Please ensure these get unset inside Skin::prepareQuickTemplate
'user-interface-preferences'
=>
[],
'user-page'
=>
$userPageLink
,
'user-menu'
=>
$this
->
buildPersonalUrls
(
false
),
'notifications'
=>
[],
'associated-pages'
=>
[],
// Legacy keys
'namespaces'
=>
[],
'views'
=>
[],
'actions'
=>
[],
'variants'
=>
[]
];
$associatedPages
=
[];
$namespaces
=
[];
$userCanRead
=
$this
->
getAuthority
()->
probablyCan
(
'read'
,
$title
);
// Checks if page is some kind of content
if
(
$title
->
canExist
()
)
{
// Gets page objects for the associatedPages namespaces
$subjectPage
=
$title
->
getSubjectPage
();
$talkPage
=
$title
->
getTalkPage
();
// Determines if this is a talk page
$isTalk
=
$title
->
isTalkPage
();
// Generates XML IDs from namespace names
$subjectId
=
$title
->
getNamespaceKey
(
''
);
if
(
$subjectId
==
'main'
)
{
$talkId
=
'talk'
;
}
else
{
$talkId
=
"{$subjectId}_talk"
;
}
// Adds namespace links
if
(
$subjectId
===
'user'
)
{
$subjectMsg
=
$this
->
msg
(
'nstab-user'
,
$subjectPage
->
getRootText
()
);
}
else
{
// The following messages are used here:
// * nstab-main
// * nstab-media
// * nstab-special
// * nstab-project
// * nstab-image
// * nstab-mediawiki
// * nstab-template
// * nstab-help
// * nstab-category
// * nstab-<subject namespace key>
$subjectMsg
=
[
"nstab-$subjectId"
];
if
(
$subjectPage
->
isMainPage
()
)
{
array_unshift
(
$subjectMsg
,
'nstab-mainpage'
);
}
}
$associatedPages
[
$subjectId
]
=
$this
->
tabAction
(
$subjectPage
,
$subjectMsg
,
!
$isTalk
,
''
,
$userCanRead
);
$associatedPages
[
$subjectId
][
'context'
]
=
'subject'
;
// Use the following messages if defined or talk if not:
// * nstab-talk, nstab-user_talk, nstab-media_talk, nstab-project_talk
// * nstab-image_talk, nstab-mediawiki_talk, nstab-template_talk
// * nstab-help_talk, nstab-category_talk,
// * nstab-<subject namespace key>_talk
$associatedPages
[
$talkId
]
=
$this
->
tabAction
(
$talkPage
,
[
"nstab-$talkId"
,
"talk"
],
$isTalk
,
''
,
$userCanRead
);
$associatedPages
[
$talkId
][
'context'
]
=
'talk'
;
if
(
$userCanRead
)
{
// Adds "view" view link
if
(
$title
->
isKnown
()
)
{
$content_navigation
[
'views'
][
'view'
]
=
$this
->
tabAction
(
$isTalk
?
$talkPage
:
$subjectPage
,
'view-view'
,
(
$onPage
&&
(
$action
==
'view'
||
$action
==
'purge'
)
),
''
,
true
);
$content_navigation
[
'views'
][
'view'
][
'text'
]
=
$this
->
getSkinNavOverrideableLabel
(
'view-view'
);
// signal to hide this from simple content_actions
$content_navigation
[
'views'
][
'view'
][
'redundant'
]
=
true
;
}
$page
=
$this
->
canUseWikiPage
()
?
$this
->
getWikiPage
()
:
false
;
$isRemoteContent
=
$page
&&
!
$page
->
isLocal
();
// If it is a non-local file, show a link to the file in its own repository
// @todo abstract this for remote content that isn't a file
if
(
$isRemoteContent
)
{
$content_navigation
[
'views'
][
'view-foreign'
]
=
[
'class'
=>
''
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
'view-foreign'
,
$page
->
getWikiDisplayName
()
),
'href'
=>
$page
->
getSourceURL
(),
'primary'
=>
false
,
];
}
// Checks if user can edit the current page if it exists or create it otherwise
if
(
$this
->
getAuthority
()->
probablyCan
(
'edit'
,
$title
)
)
{
// Builds CSS class for talk page links
$isTalkClass
=
$isTalk
?
' istalk'
:
''
;
// Whether the user is editing the page
$isEditing
=
$onPage
&&
(
$action
==
'edit'
||
$action
==
'submit'
);
$isRedirect
=
$page
&&
$page
->
isRedirect
();
// Whether to show the "Add a new section" tab
// Checks if this is a current rev of talk page and is not forced to be hidden
$showNewSection
=
!
$out
->
forceHideNewSectionLink
()
&&
(
(
$isTalk
&&
!
$isRedirect
&&
$out
->
isRevisionCurrent
()
)
||
$out
->
showNewSectionLink
()
);
$section
=
$request
->
getVal
(
'section'
);
if
(
$title
->
exists
()
||
(
$title
->
inNamespace
(
NS_MEDIAWIKI
)
&&
$title
->
getDefaultMessageText
()
!==
false
)
)
{
$msgKey
=
$isRemoteContent
?
'edit-local'
:
'edit'
;
}
else
{
$msgKey
=
$isRemoteContent
?
'create-local'
:
'create'
;
}
$content_navigation
[
'views'
][
'edit'
]
=
[
'class'
=>
(
$isEditing
&&
(
$section
!==
'new'
||
!
$showNewSection
)
?
'selected'
:
''
)
.
$isTalkClass
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
"view-$msgKey"
),
'single-id'
=>
"ca-$msgKey"
,
'href'
=>
$title
->
getLocalURL
(
$this
->
editUrlOptions
()
),
'primary'
=>
!
$isRemoteContent
,
// don't collapse this in vector
];
// section link
if
(
$showNewSection
)
{
// Adds new section link
// $content_navigation['actions']['addsection']
$content_navigation
[
'views'
][
'addsection'
]
=
[
'class'
=>
(
$isEditing
&&
$section
==
'new'
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
"action-addsection"
),
'href'
=>
$title
->
getLocalURL
(
'action=edit§ion=new'
)
];
}
// Checks if the page has some kind of viewable source content
}
elseif
(
$title
->
hasSourceText
()
)
{
// Adds view source view link
$content_navigation
[
'views'
][
'viewsource'
]
=
[
'class'
=>
(
$onPage
&&
$action
==
'edit'
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
"action-viewsource"
),
'href'
=>
$title
->
getLocalURL
(
$this
->
editUrlOptions
()
),
'primary'
=>
true
,
// don't collapse this in vector
];
}
// Checks if the page exists
if
(
$title
->
exists
()
)
{
// Adds history view link
$content_navigation
[
'views'
][
'history'
]
=
[
'class'
=>
(
$onPage
&&
$action
==
'history'
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
'view-history'
),
'href'
=>
$title
->
getLocalURL
(
'action=history'
),
];
if
(
$this
->
getAuthority
()->
probablyCan
(
'delete'
,
$title
)
)
{
$content_navigation
[
'actions'
][
'delete'
]
=
[
'icon'
=>
'trash'
,
'class'
=>
(
$onPage
&&
$action
==
'delete'
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
'action-delete'
),
'href'
=>
$title
->
getLocalURL
(
[
'action'
=>
'delete'
,
'oldid'
=>
$out
->
getRevisionId
(),
]
)
];
}
if
(
$this
->
getAuthority
()->
probablyCan
(
'move'
,
$title
)
)
{
$moveTitle
=
SpecialPage
::
getTitleFor
(
'Movepage'
,
$title
->
getPrefixedDBkey
()
);
$content_navigation
[
'actions'
][
'move'
]
=
[
'class'
=>
$this
->
getTitle
()->
isSpecial
(
'Movepage'
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
'action-move'
),
'icon'
=>
'move'
,
'href'
=>
$moveTitle
->
getLocalURL
()
];
}
}
else
{
// article doesn't exist or is deleted
if
(
$this
->
getAuthority
()->
probablyCan
(
'deletedhistory'
,
$title
)
)
{
$n
=
$title
->
getDeletedEditsCount
();
if
(
$n
)
{
$undelTitle
=
SpecialPage
::
getTitleFor
(
'Undelete'
,
$title
->
getPrefixedDBkey
()
);
// If the user can't undelete but can view deleted
// history show them a "View .. deleted" tab instead.
$msgKey
=
$this
->
getAuthority
()->
probablyCan
(
'undelete'
,
$title
)
?
'undelete'
:
'viewdeleted'
;
$content_navigation
[
'actions'
][
'undelete'
]
=
[
'class'
=>
$this
->
getTitle
()->
isSpecial
(
'Undelete'
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
"action-$msgKey"
,
$n
),
'icon'
=>
'trash'
,
'href'
=>
$undelTitle
->
getLocalURL
()
];
}
}
}
$restrictionStore
=
$services
->
getRestrictionStore
();
if
(
$this
->
getAuthority
()->
probablyCan
(
'protect'
,
$title
)
&&
$restrictionStore
->
listApplicableRestrictionTypes
(
$title
)
&&
$permissionManager
->
getNamespaceRestrictionLevels
(
$title
->
getNamespace
(),
$performer
->
getUser
()
)
!==
[
''
]
)
{
$isProtected
=
$restrictionStore
->
isProtected
(
$title
);
$mode
=
$isProtected
?
'unprotect'
:
'protect'
;
$content_navigation
[
'actions'
][
$mode
]
=
[
'class'
=>
(
$onPage
&&
$action
==
$mode
)
?
'selected'
:
false
,
'text'
=>
$this
->
getSkinNavOverrideableLabel
(
"action-$mode"
),
'icon'
=>
$isProtected
?
'unLock'
:
'lock'
,
'href'
=>
$title
->
getLocalURL
(
"action=$mode"
)
];
}
if
(
$this
->
loggedin
&&
$this
->
getAuthority
()
->
isAllowedAll
(
'viewmywatchlist'
,
'editmywatchlist'
)
)
{
/**
* The following actions use messages which, if made particular to
* the any specific skins, would break the Ajax code which makes this
* action happen entirely inline. OutputPage::getJSVars
* defines a set of messages in a javascript object - and these
* messages are assumed to be global for all skins. Without making
* a change to that procedure these messages will have to remain as
* the global versions.
*/
$mode
=
MediaWikiServices
::
getInstance
()->
getWatchlistManager
()
->
isWatched
(
$performer
,
$title
)
?
'unwatch'
:
'watch'
;
// Add the watch/unwatch link.
$content_navigation
[
'actions'
][
$mode
]
=
$this
->
getWatchLinkAttrs
(
$mode
,
$performer
,
$title
,
$action
,
$onPage
);
}
}
// Add language variants
$languageConverterFactory
=
MediaWikiServices
::
getInstance
()->
getLanguageConverterFactory
();
if
(
$userCanRead
&&
!
$languageConverterFactory
->
isConversionDisabled
()
)
{
$pageLang
=
$title
->
getPageLanguage
();
$converter
=
$languageConverterFactory
->
getLanguageConverter
(
$pageLang
);
// Checks that language conversion is enabled and variants exist
// And if it is not in the special namespace
if
(
$converter
->
hasVariants
()
)
{
// Gets list of language variants
$variants
=
$converter
->
getVariants
();
// Gets preferred variant (note that user preference is
// only possible for wiki content language variant)
$preferred
=
$converter
->
getPreferredVariant
();
if
(
$action
===
'view'
)
{
$params
=
$request
->
getQueryValues
();
unset
(
$params
[
'title'
]
);
}
else
{
$params
=
[];
}
// Loops over each variant
foreach
(
$variants
as
$code
)
{
// Gets variant name from language code
$varname
=
$pageLang
->
getVariantname
(
$code
);
// Appends variant link
$content_navigation
[
'variants'
][]
=
[
'class'
=>
(
$code
==
$preferred
)
?
'selected'
:
false
,
'text'
=>
$varname
,
'href'
=>
$title
->
getLocalURL
(
[
'variant'
=>
$code
]
+
$params
),
'lang'
=>
LanguageCode
::
bcp47
(
$code
),
'hreflang'
=>
LanguageCode
::
bcp47
(
$code
),
];
}
}
}
$namespaces
=
$associatedPages
;
}
else
{
// If it's not content, and a request URL is set it's got to be a special page
try
{
$url
=
$request
->
getRequestURL
();
}
catch
(
MWException
$e
)
{
$url
=
false
;
}
$namespaces
[
'special'
]
=
[
'class'
=>
'selected'
,
'text'
=>
$this
->
msg
(
'nstab-special'
)->
text
(),
'href'
=>
$url
,
// @see: T4457, T4510
'context'
=>
'subject'
];
$associatedPages
+=
$this
->
getSpecialPageAssociatedNavigationLinks
(
$title
);
}
$content_navigation
[
'namespaces'
]
=
$namespaces
;
$content_navigation
[
'associated-pages'
]
=
$associatedPages
;
$this
->
runOnSkinTemplateNavigationHooks
(
$this
,
$content_navigation
);
// Setup xml ids and tooltip info
foreach
(
$content_navigation
as
$section
=>
&
$links
)
{
foreach
(
$links
as
$key
=>
&
$link
)
{
// Allow links to set their own id for backwards compatibility reasons.
if
(
isset
(
$link
[
'id'
]
)
||
isset
(
$link
[
'html'
]
)
)
{
continue
;
}
$xmlID
=
$key
;
if
(
isset
(
$link
[
'context'
]
)
&&
$link
[
'context'
]
==
'subject'
)
{
$xmlID
=
'ca-nstab-'
.
$xmlID
;
}
elseif
(
isset
(
$link
[
'context'
]
)
&&
$link
[
'context'
]
==
'talk'
)
{
$xmlID
=
'ca-talk'
;
$link
[
'rel'
]
=
'discussion'
;
}
elseif
(
$section
==
'variants'
)
{
$xmlID
=
'ca-varlang-'
.
$xmlID
;
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
$link
[
'class'
]
.=
' ca-variants-'
.
$link
[
'lang'
];
}
else
{
$xmlID
=
'ca-'
.
$xmlID
;
}
$link
[
'id'
]
=
$xmlID
;
}
}
# We don't want to give the watch tab an accesskey if the
# page is being edited, because that conflicts with the
# accesskey on the watch checkbox. We also don't want to
# give the edit tab an accesskey, because that's fairly
# superfluous and conflicts with an accesskey (Ctrl-E) often
# used for editing in Safari.
if
(
in_array
(
$action
,
[
'edit'
,
'submit'
]
)
)
{
if
(
isset
(
$content_navigation
[
'views'
][
'edit'
]
)
)
{
$content_navigation
[
'views'
][
'edit'
][
'tooltiponly'
]
=
true
;
}
if
(
isset
(
$content_navigation
[
'actions'
][
'watch'
]
)
)
{
$content_navigation
[
'actions'
][
'watch'
][
'tooltiponly'
]
=
true
;
}
if
(
isset
(
$content_navigation
[
'actions'
][
'unwatch'
]
)
)
{
$content_navigation
[
'actions'
][
'unwatch'
][
'tooltiponly'
]
=
true
;
}
}
$this
->
contentNavigationCached
=
$content_navigation
;
return
$content_navigation
;
}
/**
* Return a list of pages that have been marked as related to/associated with
* the special page for display.
*
* @param Title $title
* @return array
*/
private
function
getSpecialPageAssociatedNavigationLinks
(
Title
$title
):
array
{
$specialAssociatedNavigationLinks
=
[];
$specialFactory
=
MediaWikiServices
::
getInstance
()->
getSpecialPageFactory
();
$special
=
$specialFactory
->
getPage
(
$title
->
getText
()
);
if
(
$special
===
null
)
{
// not a valid special page
return
[];
}
$special
->
setContext
(
$this
);
$associatedNavigationLinks
=
$special
->
getAssociatedNavigationLinks
();
// If there are no subpages, we should not render
if
(
count
(
$associatedNavigationLinks
)
===
0
)
{
return
[];
}
foreach
(
$associatedNavigationLinks
as
$i
=>
$relatedTitleText
)
{
$relatedTitle
=
Title
::
newFromText
(
$relatedTitleText
);
$special
=
$specialFactory
->
getPage
(
$relatedTitle
->
getText
()
);
if
(
$special
===
null
)
{
$text
=
$relatedTitle
->
getText
();
}
else
{
$text
=
$special
->
getShortDescription
(
$relatedTitle
->
getSubpageText
()
);
}
$specialAssociatedNavigationLinks
[
'special-specialAssociatedNavigationLinks-link-'
.
$i
]
=
[
'text'
=>
$text
,
'href'
=>
$relatedTitle
->
getLocalURL
(),
'class'
=>
$relatedTitle
->
fixSpecialName
()->
equals
(
$title
->
fixSpecialName
()
)
?
'selected'
:
''
,
];
}
return
$specialAssociatedNavigationLinks
;
}
/**
* an array of edit links by default used for the tabs
* @param array $content_navigation
* @return array
*/
private
function
buildContentActionUrls
(
$content_navigation
)
{
// content_actions has been replaced with content_navigation for backwards
// compatibility and also for skins that just want simple tabs content_actions
// is now built by flattening the content_navigation arrays into one
$content_actions
=
[];
foreach
(
$content_navigation
as
$links
)
{
foreach
(
$links
as
$key
=>
$value
)
{
if
(
isset
(
$value
[
'redundant'
]
)
&&
$value
[
'redundant'
]
)
{
// Redundant tabs are dropped from content_actions
continue
;
}
// content_actions used to have ids built using the "ca-$key" pattern
// so the xmlID based id is much closer to the actual $key that we want
// for that reason we'll just strip out the ca- if present and use
// the latter potion of the "id" as the $key
if
(
isset
(
$value
[
'id'
]
)
&&
substr
(
$value
[
'id'
],
0
,
3
)
==
'ca-'
)
{
$key
=
substr
(
$value
[
'id'
],
3
);
}
if
(
isset
(
$content_actions
[
$key
]
)
)
{
wfDebug
(
__METHOD__
.
": Found a duplicate key for $key while flattening "
.
"content_navigation into content_actions."
);
continue
;
}
$content_actions
[
$key
]
=
$value
;
}
}
return
$content_actions
;
}
/**
* Insert legacy menu items from content navigation into the personal toolbar.
*
* @internal
*
* @param array $contentNavigation
* @return array
*/
final
protected
function
injectLegacyMenusIntoPersonalTools
(
array
$contentNavigation
):
array
{
$userMenu
=
$contentNavigation
[
'user-menu'
]
??
[];
// userpage is only defined for logged-in users, and wfArrayInsertAfter requires the
// $after parameter to be a known key in the array.
if
(
isset
(
$contentNavigation
[
'user-menu'
][
'userpage'
]
)
&&
isset
(
$contentNavigation
[
'notifications'
]
)
)
{
$userMenu
=
wfArrayInsertAfter
(
$userMenu
,
$contentNavigation
[
'notifications'
],
'userpage'
);
}
if
(
isset
(
$contentNavigation
[
'user-interface-preferences'
]
)
)
{
return
array_merge
(
$contentNavigation
[
'user-interface-preferences'
],
$userMenu
);
}
return
$userMenu
;
}
/**
* Build the personal urls array.
*
* @internal
*
* @param array $contentNavigation
* @return array
*/
private
function
makeSkinTemplatePersonalUrls
(
array
$contentNavigation
):
array
{
if
(
isset
(
$contentNavigation
[
'user-menu'
]
)
)
{
return
$this
->
injectLegacyMenusIntoPersonalTools
(
$contentNavigation
);
}
return
[];
}
/**
* @since 1.35
* @param array $attrs (optional) will be passed to tooltipAndAccesskeyAttribs
* and decorate the resulting input
* @return string of HTML input
*/
public
function
makeSearchInput
(
$attrs
=
[]
)
{
// It's possible that getTemplateData might be calling
// Skin::makeSearchInput. To avoid infinite recursion create a
// new instance of the search component here.
$searchBox
=
$this
->
getComponent
(
'search-box'
);
$data
=
$searchBox
->
getTemplateData
();
return
Html
::
element
(
'input'
,
$data
[
'array-input-attributes'
]
+
$attrs
);
}
/**
* @since 1.35
* @param string $mode representing the type of button wanted
* either `go`, `fulltext` or `image`
* @param array $attrs (optional)
* @return string of HTML button
*/
final
public
function
makeSearchButton
(
$mode
,
$attrs
=
[]
)
{
// It's possible that getTemplateData might be calling
// Skin::makeSearchInput. To avoid infinite recursion create a
// new instance of the search component here.
$searchBox
=
$this
->
getComponent
(
'search-box'
);
$searchData
=
$searchBox
->
getTemplateData
();
switch
(
$mode
)
{
case
'go'
:
$attrs
[
'value'
]
??=
$this
->
msg
(
'searcharticle'
)->
text
();
return
Html
::
element
(
'input'
,
array_merge
(
$searchData
[
'array-button-go-attributes'
],
$attrs
)
);
case
'fulltext'
:
$attrs
[
'value'
]
??=
$this
->
msg
(
'searchbutton'
)->
text
();
return
Html
::
element
(
'input'
,
array_merge
(
$searchData
[
'array-button-fulltext-attributes'
],
$attrs
)
);
case
'image'
:
$buttonAttrs
=
[
'type'
=>
'submit'
,
'name'
=>
'button'
,
];
$buttonAttrs
=
array_merge
(
$buttonAttrs
,
Linker
::
tooltipAndAccesskeyAttribs
(
'search-fulltext'
),
$attrs
);
unset
(
$buttonAttrs
[
'src'
]
);
unset
(
$buttonAttrs
[
'alt'
]
);
unset
(
$buttonAttrs
[
'width'
]
);
unset
(
$buttonAttrs
[
'height'
]
);
$imgAttrs
=
[
'src'
=>
$attrs
[
'src'
],
'alt'
=>
$attrs
[
'alt'
]
??
$this
->
msg
(
'searchbutton'
)->
text
(),
'width'
=>
$attrs
[
'width'
]
??
null
,
'height'
=>
$attrs
[
'height'
]
??
null
,
];
return
Html
::
rawElement
(
'button'
,
$buttonAttrs
,
Html
::
element
(
'img'
,
$imgAttrs
)
);
default
:
throw
new
InvalidArgumentException
(
'Unknown mode passed to '
.
__METHOD__
);
}
}
/**
* @return bool
*/
private
function
isSpecialContributeShowable
():
bool
{
return
ContributeFactory
::
isEnabledOnCurrentSkin
(
$this
,
$this
->
getConfig
()->
get
(
MainConfigNames
::
SpecialContributeSkinsEnabled
)
);
}
/**
* @param array &$personal_urls
* @param string $key
* @param string|null $userName
* @param bool $active
*
* @return array
*/
private
function
makeContributionsLink
(
array
&
$personal_urls
,
string
$key
,
?
string
$userName
=
null
,
bool
$active
=
false
):
array
{
$isSpecialContributeShowable
=
$this
->
isSpecialContributeShowable
();
$subpage
=
$userName
??
false
;
$user
=
$this
->
getUser
();
// If the "Contribute" page is showable and the user is anon. or has no edit count,
// direct them to the "Contribute" page instead of the "Contributions" or "Mycontributions" pages.
// Explanation:
// a. For logged-in users: In wikis where the "Contribute" page is enabled, we only want
// to navigate logged-in users to the "Contribute", when they have done no edits. Otherwise, we
// want to navigate them to the "Mycontributions" page to easily access their edits/contributions.
// Currently, the "Contribute" page is used as target for all logged-in users.
// b. For anon. users: In wikis where the "Contribute" page is enabled, we still navigate the
// anonymous users to the "Contribute" page.
// Task: T369041
if
(
$isSpecialContributeShowable
&&
(
int
)
$user
->
getEditCount
()
===
0
)
{
$href
=
SkinComponentUtils
::
makeSpecialUrlSubpage
(
'Contribute'
,
false
);
$personal_urls
[
'contribute'
]
=
[
'text'
=>
$this
->
msg
(
'contribute'
)->
text
(),
'href'
=>
$href
,
'active'
=>
$href
==
$this
->
getTitle
()->
getLocalURL
(),
'icon'
=>
'edit'
];
}
else
{
$href
=
SkinComponentUtils
::
makeSpecialUrlSubpage
(
$subpage
!==
false
?
'Contributions'
:
'Mycontributions'
,
$subpage
);
$personal_urls
[
$key
]
=
[
'text'
=>
$this
->
msg
(
$key
)->
text
(),
'href'
=>
$href
,
'active'
=>
$active
,
'icon'
=>
'userContributions'
];
}
return
$personal_urls
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 13:18 (1 d, 18 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
45/1d/0141bb72413dc7b78d808bfffa1b
Default Alt Text
SkinTemplate.php (55 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment