Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
51.49% |
431 / 837 |
|
17.65% |
6 / 34 |
CRAP | |
0.00% |
0 / 1 |
| SkinTemplate | |
51.56% |
431 / 836 |
|
17.65% |
6 / 34 |
5671.99 | |
0.00% |
0 / 1 |
| setupTemplate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setupTemplateForOutput | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
2.35 | |||
| setupTemplateContext | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
4.00 | |||
| generateHTML | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| outputPage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| getTemplateData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| prepareQuickTemplate | |
100.00% |
115 / 115 |
|
100.00% |
1 / 1 |
10 | |||
| makePersonalToolsList | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| getStructuredPersonalTools | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| buildPersonalUrls | |
35.06% |
27 / 77 |
|
0.00% |
0 / 1 |
167.84 | |||
| useCombinedLoginLink | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| buildLoginData | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
| getCategoryPortletsData | |
30.00% |
3 / 10 |
|
0.00% |
0 / 1 |
6.09 | |||
| getCategoryLinks | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| getPortletsTemplateData | |
94.87% |
37 / 39 |
|
0.00% |
0 / 1 |
12.02 | |||
| buildLogoutLinkData | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
| buildCreateAccountData | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| addPersonalPageItem | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
| buildPersonalPageItem | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| buildWatchlistData | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
| tabAction | |
61.36% |
27 / 44 |
|
0.00% |
0 / 1 |
27.98 | |||
| getSkinNavOverrideableLabel | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
3.24 | |||
| makeTalkUrlDetails | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| getWatchLinkAttrs | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
72 | |||
| runOnSkinTemplateNavigationHooks | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
| buildContentNavigationUrlsInternal | |
51.79% |
130 / 251 |
|
0.00% |
0 / 1 |
778.18 | |||
| getSpecialPageAssociatedNavigationLinks | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
| buildContentActionUrls | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
9.86 | |||
| injectLegacyMenusIntoPersonalTools | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
| makeSkinTemplatePersonalUrls | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| makeSearchInput | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| makeSearchButton | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
30 | |||
| isSpecialContributeShowable | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| makeContributionsLink | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Copyright © Gabriel Wicke -- http://www.aulinx.de/ |
| 4 | * |
| 5 | * @license GPL-2.0-or-later |
| 6 | * @file |
| 7 | */ |
| 8 | |
| 9 | namespace MediaWiki\Skin; |
| 10 | |
| 11 | use InvalidArgumentException; |
| 12 | use MediaWiki\Debug\MWDebug; |
| 13 | use MediaWiki\Exception\MWException; |
| 14 | use MediaWiki\Html\Html; |
| 15 | use MediaWiki\Language\LanguageCode; |
| 16 | use MediaWiki\Linker\Linker; |
| 17 | use MediaWiki\MainConfigNames; |
| 18 | use MediaWiki\MediaWikiServices; |
| 19 | use MediaWiki\Message\Message; |
| 20 | use MediaWiki\Parser\ParserOutputFlags; |
| 21 | use MediaWiki\Permissions\Authority; |
| 22 | use MediaWiki\ResourceLoader as RL; |
| 23 | use MediaWiki\Skin\Components\SkinComponentUtils; |
| 24 | use MediaWiki\SpecialPage\SpecialPage; |
| 25 | use MediaWiki\Specials\Contribute\ContributeFactory; |
| 26 | use MediaWiki\Title\Title; |
| 27 | use Profiler; |
| 28 | use RuntimeException; |
| 29 | use Wikimedia\Message\MessageParam; |
| 30 | use Wikimedia\Message\MessageSpecifier; |
| 31 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 32 | |
| 33 | /** |
| 34 | * Base class for QuickTemplate-based skins. |
| 35 | * |
| 36 | * The template data is filled in SkinTemplate::prepareQuickTemplate. |
| 37 | * |
| 38 | * @stable to extend |
| 39 | * @ingroup Skins |
| 40 | */ |
| 41 | class SkinTemplate extends Skin { |
| 42 | /** |
| 43 | * @var string For QuickTemplate, the name of the subclass which will |
| 44 | * actually fill the template. |
| 45 | */ |
| 46 | public $template; |
| 47 | |
| 48 | /** @var string */ |
| 49 | public $thispage; |
| 50 | /** @var string */ |
| 51 | public $titletxt; |
| 52 | /** @var string */ |
| 53 | public $userpage; |
| 54 | /** @var bool TODO: Rename this to $isRegistered (but that's a breaking change) */ |
| 55 | public $loggedin; |
| 56 | /** @var string */ |
| 57 | public $username; |
| 58 | /** @var array */ |
| 59 | public $userpageUrlDetails; |
| 60 | |
| 61 | /** @var bool */ |
| 62 | private $isTempUser; |
| 63 | |
| 64 | /** @var bool */ |
| 65 | private $isNamedUser; |
| 66 | |
| 67 | /** @var bool */ |
| 68 | private $isAnonUser; |
| 69 | |
| 70 | /** @var bool */ |
| 71 | private $templateContextSet = false; |
| 72 | /** @var array|null */ |
| 73 | private $contentNavigationCached; |
| 74 | /** @var array|null */ |
| 75 | private $portletsCached; |
| 76 | |
| 77 | /** |
| 78 | * Create the template engine object; we feed it a bunch of data |
| 79 | * and eventually it spits out some HTML. Should have interface |
| 80 | * roughly equivalent to PHPTAL 0.7. |
| 81 | * |
| 82 | * @param class-string<QuickTemplate> $classname |
| 83 | * @return QuickTemplate |
| 84 | */ |
| 85 | protected function setupTemplate( $classname ) { |
| 86 | return new $classname( $this->getConfig() ); |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * @return QuickTemplate |
| 91 | */ |
| 92 | protected function setupTemplateForOutput() { |
| 93 | $this->setupTemplateContext(); |
| 94 | $template = $this->options['template'] ?? $this->template; |
| 95 | if ( !$template ) { |
| 96 | throw new RuntimeException( |
| 97 | 'SkinTemplate skins must define a `template` either as a public' |
| 98 | . ' property of by passing in a`template` option to the constructor.' |
| 99 | ); |
| 100 | } |
| 101 | $tpl = $this->setupTemplate( $template ); |
| 102 | return $tpl; |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Setup class properties that are necessary prior to calling |
| 107 | * setupTemplateForOutput. It must be called inside |
| 108 | * prepareQuickTemplate. |
| 109 | * This function may set local class properties that will be used |
| 110 | * by other methods, but should not make assumptions about the |
| 111 | * implementation of setupTemplateForOutput |
| 112 | * @since 1.35 |
| 113 | */ |
| 114 | final protected function setupTemplateContext() { |
| 115 | if ( $this->templateContextSet ) { |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | $request = $this->getRequest(); |
| 120 | $user = $this->getUser(); |
| 121 | $title = $this->getTitle(); |
| 122 | $this->thispage = $title->getPrefixedDBkey(); |
| 123 | $this->titletxt = $title->getPrefixedText(); |
| 124 | $userpageTitle = $user->getUserPage(); |
| 125 | $this->userpage = $userpageTitle->getPrefixedText(); |
| 126 | $this->loggedin = $user->isRegistered(); |
| 127 | $this->username = $user->getName(); |
| 128 | $this->isTempUser = $user->isTemp(); |
| 129 | $this->isNamedUser = $this->loggedin && !$this->isTempUser; |
| 130 | $this->isAnonUser = $user->isAnon(); |
| 131 | |
| 132 | if ( $this->isNamedUser ) { |
| 133 | $this->userpageUrlDetails = self::makeUrlDetails( $userpageTitle ); |
| 134 | } else { |
| 135 | # This won't be used in the standard skins, but we define it to preserve the interface |
| 136 | # To save time, we check for existence |
| 137 | $this->userpageUrlDetails = self::makeKnownUrlDetails( $userpageTitle ); |
| 138 | } |
| 139 | |
| 140 | $this->templateContextSet = true; |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Subclasses not wishing to use the QuickTemplate |
| 145 | * render method can rewrite this method, for example to use |
| 146 | * TemplateParser::processTemplate |
| 147 | * @since 1.35 |
| 148 | * @return string HTML is the contents of the body tag e.g. <body>...</body> |
| 149 | */ |
| 150 | public function generateHTML() { |
| 151 | $tpl = $this->prepareQuickTemplate(); |
| 152 | $options = $this->getOptions(); |
| 153 | $out = $this->getOutput(); |
| 154 | // execute template |
| 155 | ob_start(); |
| 156 | $tpl->execute(); |
| 157 | $html = ob_get_contents(); |
| 158 | ob_end_clean(); |
| 159 | |
| 160 | return $html; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Initialize various variables and generate the template |
| 165 | * @stable to override |
| 166 | */ |
| 167 | public function outputPage() { |
| 168 | Profiler::instance()->setAllowOutput(); |
| 169 | $out = $this->getOutput(); |
| 170 | |
| 171 | $this->initPage( $out ); |
| 172 | $out->addJsConfigVars( $this->getJsConfigVars() ); |
| 173 | |
| 174 | // result may be an error |
| 175 | echo $this->generateHTML(); |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * @inheritDoc |
| 180 | */ |
| 181 | public function getTemplateData() { |
| 182 | return parent::getTemplateData() + $this->getPortletsTemplateData(); |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * initialize various variables and generate the template |
| 187 | * |
| 188 | * @since 1.23 |
| 189 | * @return QuickTemplate The template to be executed by outputPage |
| 190 | */ |
| 191 | protected function prepareQuickTemplate() { |
| 192 | $title = $this->getTitle(); |
| 193 | $request = $this->getRequest(); |
| 194 | $out = $this->getOutput(); |
| 195 | $config = $this->getConfig(); |
| 196 | $tpl = $this->setupTemplateForOutput(); |
| 197 | |
| 198 | $tpl->set( 'title', $out->getPageTitle() ); |
| 199 | $tpl->set( 'pagetitle', $out->getHTMLTitle() ); |
| 200 | |
| 201 | $tpl->set( 'thispage', $this->thispage ); |
| 202 | $tpl->set( 'titleprefixeddbkey', $this->thispage ); |
| 203 | $tpl->set( 'titletext', $title->getText() ); |
| 204 | $tpl->set( 'articleid', $title->getArticleID() ); |
| 205 | |
| 206 | $tpl->set( 'isarticle', $out->isArticle() ); |
| 207 | |
| 208 | $tpl->set( 'subtitle', $this->prepareSubtitle() ); |
| 209 | $tpl->set( 'undelete', $this->prepareUndeleteLink() ); |
| 210 | |
| 211 | $tpl->set( 'catlinks', $this->getCategories() ); |
| 212 | $feeds = $this->buildFeedUrls(); |
| 213 | $tpl->set( 'feeds', count( $feeds ) ? $feeds : false ); |
| 214 | |
| 215 | $tpl->set( 'mimetype', $config->get( MainConfigNames::MimeType ) ); |
| 216 | $tpl->set( 'charset', 'UTF-8' ); |
| 217 | $tpl->set( 'wgScript', $config->get( MainConfigNames::Script ) ); |
| 218 | $tpl->set( 'skinname', $this->skinname ); |
| 219 | $tpl->set( 'skinclass', static::class ); |
| 220 | $tpl->set( 'skin', $this ); |
| 221 | $tpl->set( 'printable', $out->isPrintable() ); |
| 222 | $tpl->set( 'handheld', $request->getBool( 'handheld' ) ); |
| 223 | $tpl->set( 'loggedin', $this->loggedin ); |
| 224 | $tpl->set( 'notspecialpage', !$title->isSpecialPage() ); |
| 225 | |
| 226 | $searchTitle = SpecialPage::newSearchPage( $this->getUser() ); |
| 227 | $searchLink = $searchTitle->getLocalURL(); |
| 228 | $tpl->set( 'searchaction', $searchLink ); |
| 229 | $tpl->deprecate( 'searchaction', '1.36' ); |
| 230 | |
| 231 | $tpl->set( 'searchtitle', $searchTitle->getPrefixedDBkey() ); |
| 232 | $tpl->set( 'search', trim( $request->getVal( 'search', '' ) ) ); |
| 233 | $tpl->set( 'stylepath', $config->get( MainConfigNames::StylePath ) ); |
| 234 | $tpl->set( 'articlepath', $config->get( MainConfigNames::ArticlePath ) ); |
| 235 | $tpl->set( 'scriptpath', $config->get( MainConfigNames::ScriptPath ) ); |
| 236 | $tpl->set( 'serverurl', $config->get( MainConfigNames::Server ) ); |
| 237 | $tpl->set( 'sitename', $config->get( MainConfigNames::Sitename ) ); |
| 238 | |
| 239 | $userLang = $this->getLanguage(); |
| 240 | $userLangCode = $userLang->getHtmlCode(); |
| 241 | $userLangDir = $userLang->getDir(); |
| 242 | |
| 243 | $tpl->set( 'lang', $userLangCode ); |
| 244 | $tpl->set( 'dir', $userLangDir ); |
| 245 | $tpl->set( 'rtl', $userLang->isRTL() ); |
| 246 | |
| 247 | $logos = RL\SkinModule::getAvailableLogos( $config, $userLangCode ); |
| 248 | $tpl->set( 'logopath', $logos['1x'] ); |
| 249 | |
| 250 | $tpl->set( 'showjumplinks', true ); // showjumplinks preference has been removed |
| 251 | $tpl->set( 'username', $this->loggedin ? $this->username : null ); |
| 252 | $tpl->set( 'userpage', $this->userpage ); |
| 253 | $tpl->set( 'userpageurl', $this->userpageUrlDetails['href'] ); |
| 254 | $tpl->set( 'userlang', $userLangCode ); |
| 255 | |
| 256 | // Users can have their language set differently than the |
| 257 | // content of the wiki. For these users, tell the web browser |
| 258 | // that interface elements are in a different language. |
| 259 | $tpl->set( 'userlangattributes', $this->prepareUserLanguageAttributes() ); |
| 260 | $tpl->set( 'specialpageattributes', '' ); # obsolete |
| 261 | // Used by VectorBeta to insert HTML before content but after the |
| 262 | // heading for the page title. Defaults to empty string. |
| 263 | $tpl->set( 'prebodyhtml', '' ); |
| 264 | |
| 265 | $tpl->set( 'newtalk', $this->getNewtalks() ); |
| 266 | $tpl->set( 'logo', $this->logoText() ); |
| 267 | |
| 268 | $footerData = $this->getComponent( 'footer' )->getTemplateData(); |
| 269 | $tpl->set( 'copyright', $footerData['info']['copyright'] ?? false ); |
| 270 | // No longer used |
| 271 | $tpl->set( 'viewcount', false ); |
| 272 | $tpl->set( 'lastmod', $footerData['info']['lastmod'] ?? false ); |
| 273 | $tpl->set( 'credits', $footerData['info']['credits'] ?? false ); |
| 274 | $tpl->set( 'numberofwatchingusers', false ); |
| 275 | |
| 276 | $tpl->set( 'disclaimer', $footerData['places']['disclaimer'] ?? false ); |
| 277 | $tpl->set( 'privacy', $footerData['places']['privacy'] ?? false ); |
| 278 | $tpl->set( 'about', $footerData['places']['about'] ?? false ); |
| 279 | |
| 280 | // Flatten for compat with the 'footerlinks' key in QuickTemplate-based skins. |
| 281 | $flattenedfooterlinks = []; |
| 282 | foreach ( $footerData as $category => $data ) { |
| 283 | if ( $category !== 'data-icons' ) { |
| 284 | foreach ( $data['array-items'] as $item ) { |
| 285 | $key = str_replace( 'data-', '', $category ); |
| 286 | $flattenedfooterlinks[$key][] = $item['name']; |
| 287 | // For full support with BaseTemplate we also need to |
| 288 | // copy over the keys. |
| 289 | $tpl->set( $item['name'], $item['html'] ); |
| 290 | } |
| 291 | } |
| 292 | } |
| 293 | $tpl->set( 'footerlinks', $flattenedfooterlinks ); |
| 294 | $tpl->set( 'footericons', $this->getFooterIcons() ); |
| 295 | |
| 296 | $tpl->set( 'indicators', $out->getIndicators() ); |
| 297 | |
| 298 | $tpl->set( 'sitenotice', $this->getSiteNotice() ); |
| 299 | $tpl->set( 'printfooter', $this->printSource() ); |
| 300 | // Wrap the bodyText with #mw-content-text element |
| 301 | $tpl->set( 'bodytext', $this->wrapHTML( $title, $out->getHTML() ) ); |
| 302 | |
| 303 | $tpl->set( 'language_urls', $this->getLanguages() ?: false ); |
| 304 | |
| 305 | $content_navigation = $this->buildContentNavigationUrlsInternal(); |
| 306 | $requestedMenus = $this->getOptions()['menus']; |
| 307 | # Personal toolbar |
| 308 | if ( !in_array( 'user-menu', $requestedMenus ) ) { |
| 309 | $tpl->set( 'personal_urls', $this->makeSkinTemplatePersonalUrls( $content_navigation ) ); |
| 310 | } |
| 311 | // The user-menu, notifications, and user-interface-preferences are new content navigation entries which aren't |
| 312 | // expected to be part of content_navigation or content_actions. Adding them in there breaks skins that do not |
| 313 | // expect it. (See T316196) |
| 314 | $optInKeys = [ |
| 315 | 'user-menu', |
| 316 | 'notifications', |
| 317 | 'user-page', |
| 318 | 'user-interface-preferences', |
| 319 | 'category-normal', |
| 320 | 'category-hidden', |
| 321 | 'associated-pages', |
| 322 | // All historic menus are covered by requested menus so can be unset |
| 323 | // This should match the default for Skin::getOptions()['menus'] |
| 324 | 'namespaces', |
| 325 | 'views', |
| 326 | 'actions', |
| 327 | 'variants' |
| 328 | ]; |
| 329 | // We could iterate on keys of $requestedMenus but that might break skins making use of their own custom menus |
| 330 | // This is safer for backwards compatibility! |
| 331 | foreach ( $optInKeys as $key ) { |
| 332 | if ( !in_array( $key, $requestedMenus ) ) { |
| 333 | unset( $content_navigation[ $key ] ); |
| 334 | } |
| 335 | } |
| 336 | $content_actions = $this->buildContentActionUrls( $content_navigation ); |
| 337 | $tpl->set( 'content_navigation', $content_navigation ); |
| 338 | $tpl->set( 'content_actions', $content_actions ); |
| 339 | |
| 340 | $tpl->set( 'sidebar', $this->buildSidebar() ); |
| 341 | $tpl->set( 'nav_urls', $this->buildNavUrls() ); |
| 342 | |
| 343 | $tpl->set( 'debug', '' ); |
| 344 | $tpl->set( 'debughtml', MWDebug::getHTMLDebugLog() ); |
| 345 | |
| 346 | // Set the bodytext to another key so that skins can just output it on its own |
| 347 | // and output printfooter and debughtml separately |
| 348 | $tpl->set( 'bodycontent', $tpl->data['bodytext'] ); |
| 349 | |
| 350 | // Append printfooter and debughtml onto bodytext so that skins that |
| 351 | // were already using bodytext before they were split out don't suddenly |
| 352 | // start not outputting information. |
| 353 | $tpl->data['bodytext'] .= Html::rawElement( |
| 354 | 'div', |
| 355 | [ 'class' => 'printfooter' ], |
| 356 | "\n{$tpl->data['printfooter']}" |
| 357 | ) . "\n"; |
| 358 | $tpl->data['bodytext'] .= $tpl->data['debughtml']; |
| 359 | |
| 360 | // allow extensions adding stuff after the page content. |
| 361 | // See Skin::afterContentHook() for further documentation. |
| 362 | $tpl->set( 'dataAfterContent', $this->afterContentHook() ); |
| 363 | |
| 364 | return $tpl; |
| 365 | } |
| 366 | |
| 367 | /** |
| 368 | * Get the HTML for the personal tools list |
| 369 | * @since 1.31 |
| 370 | * |
| 371 | * @param array|null $personalTools |
| 372 | * @param array $options |
| 373 | * @return string |
| 374 | */ |
| 375 | public function makePersonalToolsList( $personalTools = null, $options = [] ) { |
| 376 | $personalTools ??= $this->getPersonalToolsForMakeListItem( |
| 377 | $this->buildPersonalUrls() |
| 378 | ); |
| 379 | |
| 380 | $html = ''; |
| 381 | foreach ( $personalTools as $key => $item ) { |
| 382 | $html .= $this->makeListItem( $key, $item, $options ); |
| 383 | } |
| 384 | return $html; |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * Get personal tools for the user |
| 389 | * |
| 390 | * @since 1.31 |
| 391 | * |
| 392 | * @return array[] |
| 393 | */ |
| 394 | public function getStructuredPersonalTools() { |
| 395 | return $this->getPersonalToolsForMakeListItem( |
| 396 | $this->buildPersonalUrls() |
| 397 | ); |
| 398 | } |
| 399 | |
| 400 | /** |
| 401 | * Build array of urls for personal toolbar |
| 402 | * |
| 403 | * @param bool $includeNotifications Since 1.36, notifications are optional |
| 404 | * @return array |
| 405 | */ |
| 406 | protected function buildPersonalUrls( bool $includeNotifications = true ) { |
| 407 | $this->setupTemplateContext(); |
| 408 | $title = $this->getTitle(); |
| 409 | $authority = $this->getAuthority(); |
| 410 | $request = $this->getRequest(); |
| 411 | $pageurl = $title->getLocalURL(); |
| 412 | $services = MediaWikiServices::getInstance(); |
| 413 | $authManager = $services->getAuthManager(); |
| 414 | $groupPermissionsLookup = $services->getGroupPermissionsLookup(); |
| 415 | $tempUserConfig = $services->getTempUserConfig(); |
| 416 | $returnto = SkinComponentUtils::getReturnToParam( $title, $request, $authority ); |
| 417 | $shouldHideUserLinks = $this->isAnonUser && $tempUserConfig->isKnown(); |
| 418 | |
| 419 | /* set up the default links for the personal toolbar */ |
| 420 | $personal_urls = []; |
| 421 | |
| 422 | if ( $this->loggedin ) { |
| 423 | $this->addPersonalPageItem( $personal_urls, '' ); |
| 424 | |
| 425 | // Merge notifications into the personal menu for older skins. |
| 426 | if ( $includeNotifications ) { |
| 427 | $contentNavigation = $this->buildContentNavigationUrlsInternal(); |
| 428 | |
| 429 | $personal_urls += $contentNavigation['notifications']; |
| 430 | } |
| 431 | |
| 432 | $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage ); |
| 433 | $personal_urls['mytalk'] = [ |
| 434 | 'text' => $this->msg( 'mytalk' )->text(), |
| 435 | 'href' => &$usertalkUrlDetails['href'], |
| 436 | 'class' => $usertalkUrlDetails['exists'] ? null : 'new', |
| 437 | 'exists' => $usertalkUrlDetails['exists'], |
| 438 | 'active' => ( $usertalkUrlDetails['href'] == $pageurl ), |
| 439 | 'icon' => 'userTalk' |
| 440 | ]; |
| 441 | if ( !$this->isTempUser ) { |
| 442 | $href = SkinComponentUtils::makeSpecialUrl( 'Preferences' ); |
| 443 | $personal_urls['preferences'] = [ |
| 444 | 'text' => $this->msg( 'mypreferences' )->text(), |
| 445 | 'href' => $href, |
| 446 | 'active' => ( $href == $pageurl ), |
| 447 | 'icon' => 'settings', |
| 448 | ]; |
| 449 | } |
| 450 | |
| 451 | if ( $authority->isAllowed( 'viewmywatchlist' ) ) { |
| 452 | $personal_urls['watchlist'] = self::buildWatchlistData(); |
| 453 | } |
| 454 | |
| 455 | # We need to do an explicit check for Special:Contributions, as we |
| 456 | # have to match both the title, and the target, which could come |
| 457 | # from request values (Special:Contributions?target=Jimbo_Wales) |
| 458 | # or be specified in "subpage" form |
| 459 | # (Special:Contributions/Jimbo_Wales). The plot |
| 460 | # thickens, because the Title object is altered for special pages, |
| 461 | # so it doesn't contain the original alias-with-subpage. |
| 462 | $origTitle = Title::newFromText( $request->getText( 'title' ) ); |
| 463 | if ( $origTitle instanceof Title && $origTitle->isSpecialPage() ) { |
| 464 | [ $spName, $spPar ] = |
| 465 | MediaWikiServices::getInstance()->getSpecialPageFactory()-> |
| 466 | resolveAlias( $origTitle->getText() ); |
| 467 | $active = $spName == 'Contributions' |
| 468 | && ( ( $spPar && $spPar == $this->username ) |
| 469 | || $request->getText( 'target' ) == $this->username ); |
| 470 | } else { |
| 471 | $active = false; |
| 472 | } |
| 473 | |
| 474 | $personal_urls = $this->makeContributionsLink( $personal_urls, 'mycontris', $this->username, $active ); |
| 475 | |
| 476 | // if we can't set the user, we can't unset it either |
| 477 | if ( $request->getSession()->canSetUser() ) { |
| 478 | $personal_urls['logout'] = $this->buildLogoutLinkData(); |
| 479 | } |
| 480 | } elseif ( !$shouldHideUserLinks ) { |
| 481 | $canEdit = $authority->isAllowed( 'edit' ); |
| 482 | $canEditWithTemp = $tempUserConfig->isAutoCreateAction( 'edit' ); |
| 483 | // No need to show Talk and Contributions to anons if they can't contribute! |
| 484 | if ( $canEdit || $canEditWithTemp ) { |
| 485 | // Non interactive placeholder for anonymous users. |
| 486 | // It's unstyled by default (black color). Skin that |
| 487 | // needs it, can style it using the 'pt-anonuserpage' id. |
| 488 | // Skin that does not need it should unset it. |
| 489 | $personal_urls['anonuserpage'] = [ |
| 490 | 'text' => $this->msg( 'notloggedin' )->text(), |
| 491 | ]; |
| 492 | } |
| 493 | if ( $canEdit ) { |
| 494 | // Because of caching, we can't link directly to the IP talk and |
| 495 | // contributions pages. Instead we use the special page shortcuts |
| 496 | // (which work correctly regardless of caching). This means we can't |
| 497 | // determine whether these links are active or not, but since major |
| 498 | // skins (MonoBook, Vector) don't use this information, it's not a |
| 499 | // huge loss. |
| 500 | $personal_urls['anontalk'] = [ |
| 501 | 'text' => $this->msg( 'anontalk' )->text(), |
| 502 | 'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Mytalk', false ), |
| 503 | 'active' => false, |
| 504 | 'icon' => 'userTalk', |
| 505 | ]; |
| 506 | $personal_urls = $this->makeContributionsLink( $personal_urls, 'anoncontribs', null, false ); |
| 507 | } |
| 508 | } |
| 509 | |
| 510 | if ( !$this->loggedin ) { |
| 511 | $useCombinedLoginLink = $this->useCombinedLoginLink(); |
| 512 | $login_url = $this->buildLoginData( $returnto, $useCombinedLoginLink ); |
| 513 | $createaccount_url = $this->buildCreateAccountData( $returnto ); |
| 514 | |
| 515 | if ( |
| 516 | $authManager->canCreateAccounts() |
| 517 | && $authority->isAllowed( 'createaccount' ) |
| 518 | && !$useCombinedLoginLink |
| 519 | ) { |
| 520 | $personal_urls['createaccount'] = $createaccount_url; |
| 521 | } |
| 522 | |
| 523 | if ( $authManager->canAuthenticateNow() ) { |
| 524 | // TODO: easy way to get anon authority |
| 525 | $key = $groupPermissionsLookup->groupHasPermission( '*', 'read' ) |
| 526 | ? 'login' |
| 527 | : 'login-private'; |
| 528 | $personal_urls[$key] = $login_url; |
| 529 | } |
| 530 | } |
| 531 | |
| 532 | return $personal_urls; |
| 533 | } |
| 534 | |
| 535 | /** |
| 536 | * Returns if a combined login/signup link will be used |
| 537 | * @unstable |
| 538 | * |
| 539 | * @return bool |
| 540 | */ |
| 541 | protected function useCombinedLoginLink() { |
| 542 | $services = MediaWikiServices::getInstance(); |
| 543 | $authManager = $services->getAuthManager(); |
| 544 | $useCombinedLoginLink = $this->getConfig()->get( MainConfigNames::UseCombinedLoginLink ); |
| 545 | if ( !$authManager->canCreateAccounts() || !$authManager->canAuthenticateNow() ) { |
| 546 | // don't show combined login/signup link if one of those is actually not available |
| 547 | $useCombinedLoginLink = false; |
| 548 | } |
| 549 | |
| 550 | return $useCombinedLoginLink; |
| 551 | } |
| 552 | |
| 553 | /** |
| 554 | * Build "Login" link |
| 555 | * @unstable |
| 556 | * |
| 557 | * @param string[] $returnto query params for the page to return to |
| 558 | * @param bool $useCombinedLoginLink when set a single link to login form will be created |
| 559 | * with alternative label. |
| 560 | * @return array |
| 561 | */ |
| 562 | protected function buildLoginData( $returnto, $useCombinedLoginLink ) { |
| 563 | $title = $this->getTitle(); |
| 564 | |
| 565 | $loginlink = $this->getAuthority()->isAllowed( 'createaccount' ) |
| 566 | && $useCombinedLoginLink ? 'nav-login-createaccount' : 'pt-login'; |
| 567 | |
| 568 | $login_url = [ |
| 569 | 'single-id' => 'pt-login', |
| 570 | 'text' => $this->msg( $loginlink )->text(), |
| 571 | 'href' => SkinComponentUtils::makeSpecialUrl( 'Userlogin', $returnto ), |
| 572 | 'active' => $title->isSpecial( 'Userlogin' ) |
| 573 | || ( $title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink ), |
| 574 | 'icon' => 'logIn' |
| 575 | ]; |
| 576 | |
| 577 | return $login_url; |
| 578 | } |
| 579 | |
| 580 | /** |
| 581 | * @param array $links return value from OutputPage::getCategoryLinks |
| 582 | * @return array of data |
| 583 | */ |
| 584 | private function getCategoryPortletsData( array $links ): array { |
| 585 | $categories = []; |
| 586 | foreach ( $links as $group => $groupLinks ) { |
| 587 | $allLinks = []; |
| 588 | $groupName = 'category-' . $group; |
| 589 | foreach ( $groupLinks as $i => $link ) { |
| 590 | $allLinks[$groupName . '-' . $i] = [ |
| 591 | 'html' => $link, |
| 592 | ]; |
| 593 | } |
| 594 | $categories[ $groupName ] = $allLinks; |
| 595 | } |
| 596 | return $categories; |
| 597 | } |
| 598 | |
| 599 | /** |
| 600 | * Extends category links with Skin::getAfterPortlet functionality. |
| 601 | * @return string HTML |
| 602 | */ |
| 603 | public function getCategoryLinks() { |
| 604 | $afterPortlet = $this->getPortletsTemplateData()['data-portlets']['data-category-normal']['html-after-portal'] |
| 605 | ?? ''; |
| 606 | return parent::getCategoryLinks() . $afterPortlet; |
| 607 | } |
| 608 | |
| 609 | /** |
| 610 | * @return array of portlet data for all portlets |
| 611 | */ |
| 612 | private function getPortletsTemplateData() { |
| 613 | if ( $this->portletsCached ) { |
| 614 | return $this->portletsCached; |
| 615 | } |
| 616 | $portlets = []; |
| 617 | $contentNavigation = $this->buildContentNavigationUrlsInternal(); |
| 618 | $sidebar = []; |
| 619 | $sidebarData = $this->buildSidebar(); |
| 620 | foreach ( $sidebarData as $name => $items ) { |
| 621 | if ( is_array( $items ) ) { |
| 622 | // Numeric strings gets an integer when set as key, cast back - T73639 |
| 623 | $name = (string)$name; |
| 624 | switch ( $name ) { |
| 625 | // ignore search |
| 626 | case 'SEARCH': |
| 627 | break; |
| 628 | // Map toolbox to `tb` id. |
| 629 | case 'TOOLBOX': |
| 630 | $sidebar[] = $this->getPortletData( 'tb', $items ); |
| 631 | break; |
| 632 | // Languages is no longer be tied to sidebar |
| 633 | case 'LANGUAGES': |
| 634 | // The language portal will be added provided either |
| 635 | // languages exist or there is a value in html-after-portal |
| 636 | // for example to show the add language wikidata link (T252800) |
| 637 | $portal = $this->getPortletData( 'lang', $items ); |
| 638 | if ( count( $items ) || $portal['html-after-portal'] ) { |
| 639 | $portlets['data-languages'] = $portal; |
| 640 | } |
| 641 | break; |
| 642 | default: |
| 643 | $sidebar[] = $this->getPortletData( $name, $items ); |
| 644 | break; |
| 645 | } |
| 646 | } |
| 647 | } |
| 648 | |
| 649 | foreach ( $contentNavigation as $name => $items ) { |
| 650 | if ( $name === 'user-menu' ) { |
| 651 | $items = $this->getPersonalToolsForMakeListItem( $items, true ); |
| 652 | } |
| 653 | |
| 654 | $portlets['data-' . $name] = $this->getPortletData( $name, $items ); |
| 655 | } |
| 656 | |
| 657 | // A menu that includes the notifications. This will be deprecated in future versions |
| 658 | // of the skin API spec. |
| 659 | $portlets['data-personal'] = $this->getPortletData( |
| 660 | 'personal', |
| 661 | $this->getPersonalToolsForMakeListItem( |
| 662 | $this->injectLegacyMenusIntoPersonalTools( $contentNavigation ) |
| 663 | ) |
| 664 | ); |
| 665 | |
| 666 | $this->portletsCached = [ |
| 667 | 'data-portlets' => $portlets, |
| 668 | 'data-portlets-sidebar' => [ |
| 669 | 'data-portlets-first' => $sidebar[0] ?? null, |
| 670 | 'array-portlets-rest' => array_slice( $sidebar, 1 ), |
| 671 | ], |
| 672 | ]; |
| 673 | return $this->portletsCached; |
| 674 | } |
| 675 | |
| 676 | /** |
| 677 | * Build data required for "Logout" link. |
| 678 | * |
| 679 | * @unstable |
| 680 | * |
| 681 | * @since 1.37 |
| 682 | * |
| 683 | * @return array Array of data required to create a logout link. |
| 684 | */ |
| 685 | final protected function buildLogoutLinkData() { |
| 686 | $title = $this->getTitle(); |
| 687 | $request = $this->getRequest(); |
| 688 | $authority = $this->getAuthority(); |
| 689 | $returnto = SkinComponentUtils::getReturnToParam( $title, $request, $authority ); |
| 690 | $isTemp = $this->isTempUser; |
| 691 | $msg = $isTemp ? 'templogout' : 'pt-userlogout'; |
| 692 | |
| 693 | return [ |
| 694 | 'single-id' => 'pt-logout', |
| 695 | 'text' => $this->msg( $msg )->text(), |
| 696 | 'data-mw-interface' => '1', |
| 697 | 'href' => SkinComponentUtils::makeSpecialUrl( 'Userlogout', $returnto ), |
| 698 | 'active' => false, |
| 699 | 'icon' => 'logOut' |
| 700 | ]; |
| 701 | } |
| 702 | |
| 703 | /** |
| 704 | * Build "Create Account" link data. |
| 705 | * @unstable |
| 706 | * |
| 707 | * @param string[] $returnto query params for the page to return to |
| 708 | * @return array |
| 709 | */ |
| 710 | protected function buildCreateAccountData( $returnto ) { |
| 711 | $title = $this->getTitle(); |
| 712 | |
| 713 | return [ |
| 714 | 'single-id' => 'pt-createaccount', |
| 715 | 'text' => $this->msg( 'pt-createaccount' )->text(), |
| 716 | 'href' => SkinComponentUtils::makeSpecialUrl( 'CreateAccount', $returnto ), |
| 717 | 'active' => $title->isSpecial( 'CreateAccount' ), |
| 718 | 'icon' => 'userAdd' |
| 719 | ]; |
| 720 | } |
| 721 | |
| 722 | /** |
| 723 | * Add the userpage link to the array |
| 724 | * |
| 725 | * @param array &$links Links array to append to |
| 726 | * @param string $idSuffix Something to add to the IDs to make them unique |
| 727 | */ |
| 728 | private function addPersonalPageItem( &$links, $idSuffix ) { |
| 729 | if ( $this->isNamedUser ) { // T340152 |
| 730 | $links['userpage'] = $this->buildPersonalPageItem( 'pt-userpage' . $idSuffix ); |
| 731 | } |
| 732 | } |
| 733 | |
| 734 | /** |
| 735 | * Build a user page link data. |
| 736 | * |
| 737 | * @param string $id of user page item to be output in HTML attribute (optional) |
| 738 | * @return array |
| 739 | */ |
| 740 | protected function buildPersonalPageItem( $id = 'pt-userpage' ): array { |
| 741 | $linkClasses = $this->userpageUrlDetails['exists'] ? [] : [ 'new' ]; |
| 742 | // T335440 Temp accounts dont show a user page link |
| 743 | // But we still need to update the user icon, as its used by other UI elements |
| 744 | $icon = $this->isTempUser ? 'userTemporary' : 'userAvatar'; |
| 745 | $href = &$this->userpageUrlDetails['href']; |
| 746 | return [ |
| 747 | 'id' => $id, |
| 748 | 'single-id' => 'pt-userpage', |
| 749 | 'text' => $this->username, |
| 750 | 'href' => $href, |
| 751 | 'link-class' => $linkClasses, |
| 752 | 'exists' => $this->userpageUrlDetails['exists'], |
| 753 | 'active' => ( $this->userpageUrlDetails['href'] == $this->getTitle()->getLocalURL() ), |
| 754 | 'icon' => $icon, |
| 755 | ]; |
| 756 | } |
| 757 | |
| 758 | /** |
| 759 | * Build a watchlist link data. |
| 760 | * |
| 761 | * @return array Array of data required to create a watchlist link. |
| 762 | */ |
| 763 | private function buildWatchlistData() { |
| 764 | $href = SkinComponentUtils::makeSpecialUrl( 'Watchlist' ); |
| 765 | $pageurl = $this->getTitle()->getLocalURL(); |
| 766 | |
| 767 | return [ |
| 768 | 'single-id' => 'pt-watchlist', |
| 769 | 'text' => $this->msg( 'mywatchlist' )->text(), |
| 770 | 'href' => $href, |
| 771 | 'active' => ( $href == $pageurl ), |
| 772 | 'icon' => 'watchlist' |
| 773 | ]; |
| 774 | } |
| 775 | |
| 776 | /** |
| 777 | * Builds an array with tab definition |
| 778 | * |
| 779 | * @param Title $title Page Where the tab links to |
| 780 | * @param string|string[]|MessageSpecifier $message Message or an array of message keys |
| 781 | * (will fall back) |
| 782 | * @param bool $selected Display the tab as selected |
| 783 | * @param string $query Query string attached to tab URL |
| 784 | * @param bool $checkEdit Check if $title exists and mark with .new if one doesn't |
| 785 | * |
| 786 | * @return array |
| 787 | * @param-taint $message tainted |
| 788 | */ |
| 789 | public function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) { |
| 790 | $classes = []; |
| 791 | if ( $selected ) { |
| 792 | $classes[] = 'selected'; |
| 793 | } |
| 794 | $exists = true; |
| 795 | $services = MediaWikiServices::getInstance(); |
| 796 | $linkClass = $services->getLinkRenderer()->getLinkClasses( $title ); |
| 797 | if ( $checkEdit && !$title->isKnown() ) { |
| 798 | // Selected tabs should not show as red link. It doesn't make sense |
| 799 | // to show a red link on a page the user has already navigated to. |
| 800 | // https://phabricator.wikimedia.org/T294129#7451549 |
| 801 | if ( !$selected ) { |
| 802 | // For historic reasons we add to the LI element |
| 803 | $classes[] = 'new'; |
| 804 | // but adding the class to the A element is more appropriate. |
| 805 | $linkClass .= ' new'; |
| 806 | } |
| 807 | $exists = false; |
| 808 | if ( $query !== '' ) { |
| 809 | $query = 'action=edit&redlink=1&' . $query; |
| 810 | } else { |
| 811 | $query = 'action=edit&redlink=1'; |
| 812 | } |
| 813 | } elseif ( $title->isRedirect() ) { |
| 814 | // Do not redirect on redirect pages, see T5324 |
| 815 | $origTitle = $this->getRelevantTitle(); |
| 816 | // FIXME: If T385340 is resolved, this check can be removed |
| 817 | $action = $this->getContext()->getActionName(); |
| 818 | $out = $this->getOutput(); |
| 819 | $notCurrentActionView = $action !== 'view' || !$out->isRevisionCurrent(); |
| 820 | |
| 821 | if ( $origTitle instanceof Title && $title->isSamePageAs( $origTitle ) && $notCurrentActionView ) { |
| 822 | if ( $query !== '' ) { |
| 823 | $query .= '&redirect=no'; |
| 824 | } else { |
| 825 | $query = 'redirect=no'; |
| 826 | } |
| 827 | } |
| 828 | } |
| 829 | |
| 830 | if ( $message instanceof MessageSpecifier ) { |
| 831 | $msg = new Message( $message ); |
| 832 | } else { |
| 833 | // wfMessageFallback will nicely accept $message as an array of fallbacks |
| 834 | // or just a single key |
| 835 | $msg = wfMessageFallback( $message ); |
| 836 | } |
| 837 | $msg->setContext( $this->getContext() ); |
| 838 | if ( !$msg->isDisabled() ) { |
| 839 | $text = $msg->text(); |
| 840 | } else { |
| 841 | $text = $services->getLanguageConverterFactory() |
| 842 | ->getLanguageConverter( $services->getContentLanguage() ) |
| 843 | ->convertNamespace( |
| 844 | $services->getNamespaceInfo() |
| 845 | ->getSubject( $title->getNamespace() ) |
| 846 | ); |
| 847 | } |
| 848 | |
| 849 | $result = [ |
| 850 | // Use a string instead of a class list for hook compatibility (T393504) |
| 851 | 'class' => implode( ' ', $classes ), |
| 852 | 'text' => $text, |
| 853 | 'href' => $title->getLocalURL( $query ), |
| 854 | 'exists' => $exists, |
| 855 | 'primary' => true ]; |
| 856 | if ( $linkClass !== '' ) { |
| 857 | $result['link-class'] = trim( $linkClass ); |
| 858 | } |
| 859 | |
| 860 | return $result; |
| 861 | } |
| 862 | |
| 863 | /** |
| 864 | * Get a message label that skins can override. |
| 865 | * |
| 866 | * @param string $labelMessageKey |
| 867 | * @param MessageParam|MessageSpecifier|string|int|float|null $param for the message |
| 868 | * @return string |
| 869 | */ |
| 870 | private function getSkinNavOverrideableLabel( $labelMessageKey, $param = null ) { |
| 871 | $skname = $this->skinname; |
| 872 | // The following messages can be used here: |
| 873 | // * skin-action-addsection |
| 874 | // * skin-action-delete |
| 875 | // * skin-action-move |
| 876 | // * skin-action-protect |
| 877 | // * skin-action-undelete |
| 878 | // * skin-action-unprotect |
| 879 | // * skin-action-viewdeleted |
| 880 | // * skin-action-viewsource |
| 881 | // * skin-view-create |
| 882 | // * skin-view-create-local |
| 883 | // * skin-view-edit |
| 884 | // * skin-view-edit-local |
| 885 | // * skin-view-foreign |
| 886 | // * skin-view-history |
| 887 | // * skin-view-view |
| 888 | $msg = wfMessageFallback( |
| 889 | "$skname-$labelMessageKey", |
| 890 | "skin-$labelMessageKey" |
| 891 | )->setContext( $this->getContext() ); |
| 892 | |
| 893 | if ( $param ) { |
| 894 | if ( is_numeric( $param ) ) { |
| 895 | $msg->numParams( $param ); |
| 896 | } else { |
| 897 | $msg->params( $param ); |
| 898 | } |
| 899 | } |
| 900 | return $msg->text(); |
| 901 | } |
| 902 | |
| 903 | /** |
| 904 | * @param string $name |
| 905 | * @param string|array $urlaction |
| 906 | * @return array |
| 907 | */ |
| 908 | private function makeTalkUrlDetails( $name, $urlaction = '' ) { |
| 909 | $title = Title::newFromTextThrow( $name )->getTalkPage(); |
| 910 | return [ |
| 911 | 'href' => $title->getLocalURL( $urlaction ), |
| 912 | 'exists' => $title->isKnown(), |
| 913 | ]; |
| 914 | } |
| 915 | |
| 916 | /** |
| 917 | * Get the attributes for the watch link. |
| 918 | * @param string $mode Either 'watch' or 'unwatch' |
| 919 | * @param Authority $performer |
| 920 | * @param Title $title |
| 921 | * @param string|null $action |
| 922 | * @param bool $onPage |
| 923 | * @return array |
| 924 | */ |
| 925 | private function getWatchLinkAttrs( |
| 926 | string $mode, Authority $performer, Title $title, ?string $action, bool $onPage |
| 927 | ): array { |
| 928 | $isWatchMode = $action == 'watch'; |
| 929 | $class = 'mw-watchlink ' . ( |
| 930 | $onPage && ( $isWatchMode || $action == 'unwatch' ) ? 'selected' : '' |
| 931 | ); |
| 932 | |
| 933 | $services = MediaWikiServices::getInstance(); |
| 934 | $watchlistManager = $services->getWatchlistManager(); |
| 935 | $watchIcon = $watchlistManager->isWatched( $performer, $title ) ? 'unStar' : 'star'; |
| 936 | $watchExpiry = null; |
| 937 | // Modify tooltip and add class identifying the page is temporarily watched, if applicable. |
| 938 | if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && |
| 939 | $watchlistManager->isTempWatched( $performer, $title ) |
| 940 | ) { |
| 941 | $class .= ' mw-watchlink-temp'; |
| 942 | $watchIcon = 'halfStar'; |
| 943 | |
| 944 | $watchStore = $services->getWatchedItemStore(); |
| 945 | $watchedItem = $watchStore->getWatchedItem( $performer->getUser(), $title ); |
| 946 | $diffInDays = $watchedItem->getExpiryInDays(); |
| 947 | $watchExpiry = $watchedItem->getExpiry( TS::ISO_8601 ); |
| 948 | if ( $diffInDays ) { |
| 949 | $msgParams = [ $diffInDays ]; |
| 950 | // Resolves to tooltip-ca-unwatch-expiring message |
| 951 | $tooltip = 'ca-unwatch-expiring'; |
| 952 | } else { |
| 953 | // Resolves to tooltip-ca-unwatch-expiring-hours message |
| 954 | $tooltip = 'ca-unwatch-expiring-hours'; |
| 955 | } |
| 956 | } |
| 957 | |
| 958 | return [ |
| 959 | 'class' => $class, |
| 960 | 'icon' => $watchIcon, |
| 961 | // uses 'watch' or 'unwatch' message |
| 962 | 'text' => $this->msg( $mode )->text(), |
| 963 | 'single-id' => $tooltip ?? null, |
| 964 | 'tooltip-params' => $msgParams ?? null, |
| 965 | 'href' => $title->getLocalURL( [ 'action' => $mode ] ), |
| 966 | // Set a data-mw=interface attribute, which the mediawiki.page.ajax |
| 967 | // module will look for to make sure it's a trusted link |
| 968 | 'data' => [ |
| 969 | 'mw-interface' => '1', |
| 970 | 'mw-expiry' => $watchExpiry, |
| 971 | ], |
| 972 | ]; |
| 973 | } |
| 974 | |
| 975 | /** |
| 976 | * Run hooks relating to navigation menu data. |
| 977 | * Skins should extend this if they want to run opinionated transformations to the data after all |
| 978 | * hooks have been run. Note hooks are run in an arbitrary order. |
| 979 | * |
| 980 | * @param SkinTemplate $skin |
| 981 | * @param array &$content_navigation representing all menus. |
| 982 | * @since 1.37 |
| 983 | */ |
| 984 | protected function runOnSkinTemplateNavigationHooks( SkinTemplate $skin, &$content_navigation ) { |
| 985 | $beforeHookAssociatedPages = array_keys( $content_navigation['associated-pages'] ); |
| 986 | $beforeHookNamespaces = array_keys( $content_navigation['namespaces'] ); |
| 987 | |
| 988 | // Equiv to SkinTemplateContentActions, run |
| 989 | $this->getHookRunner()->onSkinTemplateNavigation__Universal( |
| 990 | $skin, $content_navigation ); |
| 991 | // The new `associatedPages` menu (added in 1.39) |
| 992 | // should be backwards compatibile with `namespaces`. |
| 993 | // To do this we look for hook modifications to both keys. If modifications are not |
| 994 | // made the new key, but are made to the old key, associatedPages reverts back to the |
| 995 | // links in the namespaces menu. |
| 996 | // It's expected in future that `namespaces` menu will become an alias for `associatedPages` |
| 997 | // at which point this code can be removed. |
| 998 | $afterHookNamespaces = array_keys( $content_navigation[ 'namespaces' ] ); |
| 999 | $afterHookAssociatedPages = array_keys( $content_navigation[ 'associated-pages' ] ); |
| 1000 | $associatedPagesChanged = count( array_diff( $afterHookAssociatedPages, $beforeHookAssociatedPages ) ) > 0; |
| 1001 | $namespacesChanged = count( array_diff( $afterHookNamespaces, $beforeHookNamespaces ) ) > 0; |
| 1002 | // If some change occurred to namespaces via the hook, revert back to namespaces. |
| 1003 | if ( !$associatedPagesChanged && $namespacesChanged ) { |
| 1004 | $content_navigation['associated-pages'] = $content_navigation['namespaces']; |
| 1005 | } |
| 1006 | } |
| 1007 | |
| 1008 | /** |
| 1009 | * a structured array of links usually used for the tabs in a skin |
| 1010 | * |
| 1011 | * There are 4 standard sections |
| 1012 | * namespaces: Used for namespace tabs like special, page, and talk namespaces |
| 1013 | * views: Used for primary page views like read, edit, history |
| 1014 | * actions: Used for most extra page actions like deletion, protection, etc... |
| 1015 | * variants: Used to list the language variants for the page |
| 1016 | * |
| 1017 | * Each section's value is a key/value array of links for that section. |
| 1018 | * The links themselves have these common keys: |
| 1019 | * - class: The css classes to apply to the tab |
| 1020 | * - text: The text to display on the tab |
| 1021 | * - href: The href for the tab to point to |
| 1022 | * - rel: An optional rel= for the tab's link |
| 1023 | * - redundant: If true the tab will be dropped in skins using content_actions |
| 1024 | * this is useful for tabs like "Read" which only have meaning in skins that |
| 1025 | * take special meaning from the grouped structure of content_navigation |
| 1026 | * |
| 1027 | * Views also have an extra key which can be used: |
| 1028 | * - primary: If this is not true skins like vector may try to hide the tab |
| 1029 | * when the user has limited space in their browser window |
| 1030 | * |
| 1031 | * content_navigation using code also expects these ids to be present on the |
| 1032 | * links, however these are usually automatically generated by SkinTemplate |
| 1033 | * itself and are not necessary when using a hook. The only things these may |
| 1034 | * matter to are people modifying content_navigation after it's initial creation: |
| 1035 | * - id: A "preferred" id, most skins are best off outputting this preferred |
| 1036 | * id for best compatibility. |
| 1037 | * - tooltiponly: This is set to true for some tabs in cases where the system |
| 1038 | * believes that the accesskey should not be added to the tab. |
| 1039 | * |
| 1040 | * @return array |
| 1041 | */ |
| 1042 | private function buildContentNavigationUrlsInternal() { |
| 1043 | if ( $this->contentNavigationCached ) { |
| 1044 | return $this->contentNavigationCached; |
| 1045 | } |
| 1046 | // Display tabs for the relevant title rather than always the title itself |
| 1047 | $title = $this->getRelevantTitle(); |
| 1048 | $onPage = $title->equals( $this->getTitle() ); |
| 1049 | |
| 1050 | $out = $this->getOutput(); |
| 1051 | $request = $this->getRequest(); |
| 1052 | $performer = $this->getAuthority(); |
| 1053 | $action = $this->getContext()->getActionName(); |
| 1054 | $services = MediaWikiServices::getInstance(); |
| 1055 | $permissionManager = $services->getPermissionManager(); |
| 1056 | $categoriesData = $this->getCategoryPortletsData( $this->getOutput()->getCategoryLinks() ); |
| 1057 | $userPageLink = []; |
| 1058 | $this->addPersonalPageItem( $userPageLink, '-2' ); |
| 1059 | |
| 1060 | $content_navigation = $categoriesData + [ |
| 1061 | // Modern keys: Please ensure these get unset inside Skin::prepareQuickTemplate |
| 1062 | 'user-interface-preferences' => [], |
| 1063 | 'user-page' => $userPageLink, |
| 1064 | 'user-menu' => $this->buildPersonalUrls( false ), |
| 1065 | 'notifications' => [], |
| 1066 | 'associated-pages' => [], |
| 1067 | // Added in 1.44: a fixed position menu at bottom of page |
| 1068 | 'dock-bottom' => [], |
| 1069 | // Legacy keys |
| 1070 | 'namespaces' => [], |
| 1071 | 'views' => [], |
| 1072 | 'actions' => [], |
| 1073 | 'variants' => [] |
| 1074 | ]; |
| 1075 | |
| 1076 | $associatedPages = []; |
| 1077 | $namespaces = []; |
| 1078 | $userCanRead = $this->getAuthority()->probablyCan( 'read', $title ); |
| 1079 | |
| 1080 | // Checks if page is some kind of content |
| 1081 | if ( $title->canExist() ) { |
| 1082 | // Gets page objects for the associatedPages namespaces |
| 1083 | $subjectPage = $title->getSubjectPage(); |
| 1084 | $talkPage = $title->getTalkPage(); |
| 1085 | |
| 1086 | // Determines if this is a talk page |
| 1087 | $isTalk = $title->isTalkPage(); |
| 1088 | |
| 1089 | // Generates XML IDs from namespace names |
| 1090 | $subjectId = $title->getNamespaceKey( '' ); |
| 1091 | |
| 1092 | if ( $subjectId == 'main' ) { |
| 1093 | $talkId = 'talk'; |
| 1094 | } else { |
| 1095 | $talkId = "{$subjectId}_talk"; |
| 1096 | } |
| 1097 | |
| 1098 | // Adds namespace links |
| 1099 | if ( $subjectId === 'user' ) { |
| 1100 | $subjectMsg = $this->msg( 'nstab-user', $subjectPage->getRootText() ); |
| 1101 | } else { |
| 1102 | // The following messages are used here: |
| 1103 | // * nstab-main |
| 1104 | // * nstab-media |
| 1105 | // * nstab-special |
| 1106 | // * nstab-project |
| 1107 | // * nstab-image |
| 1108 | // * nstab-mediawiki |
| 1109 | // * nstab-template |
| 1110 | // * nstab-help |
| 1111 | // * nstab-category |
| 1112 | // * nstab-<subject namespace key> |
| 1113 | $subjectMsg = [ "nstab-$subjectId" ]; |
| 1114 | |
| 1115 | if ( $subjectPage->isMainPage() ) { |
| 1116 | array_unshift( $subjectMsg, 'nstab-mainpage' ); |
| 1117 | } |
| 1118 | } |
| 1119 | |
| 1120 | $associatedPages[$subjectId] = $this->tabAction( |
| 1121 | $subjectPage, $subjectMsg, !$isTalk, '', $userCanRead |
| 1122 | ); |
| 1123 | $associatedPages[$subjectId]['context'] = 'subject'; |
| 1124 | // Use the following messages if defined or talk if not: |
| 1125 | // * nstab-talk, nstab-user_talk, nstab-media_talk, nstab-project_talk |
| 1126 | // * nstab-image_talk, nstab-mediawiki_talk, nstab-template_talk |
| 1127 | // * nstab-help_talk, nstab-category_talk, |
| 1128 | // * nstab-<subject namespace key>_talk |
| 1129 | $associatedPages[$talkId] = $this->tabAction( |
| 1130 | $talkPage, [ "nstab-$talkId", "talk" ], $isTalk, '', $userCanRead |
| 1131 | ); |
| 1132 | $associatedPages[$talkId]['context'] = 'talk'; |
| 1133 | |
| 1134 | if ( $userCanRead ) { |
| 1135 | // Adds "view" view link |
| 1136 | if ( $title->isKnown() ) { |
| 1137 | $content_navigation['views']['view'] = $this->tabAction( |
| 1138 | $isTalk ? $talkPage : $subjectPage, |
| 1139 | 'view-view', |
| 1140 | ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true |
| 1141 | ); |
| 1142 | $content_navigation['views']['view']['text'] = $this->getSkinNavOverrideableLabel( |
| 1143 | 'view-view' |
| 1144 | ); |
| 1145 | $content_navigation['views']['view']['icon'] = 'eye'; |
| 1146 | // signal to hide this from simple content_actions |
| 1147 | $content_navigation['views']['view']['redundant'] = true; |
| 1148 | } |
| 1149 | |
| 1150 | $page = $this->canUseWikiPage() ? $this->getWikiPage() : false; |
| 1151 | $isRemoteContent = $page && !$page->isLocal(); |
| 1152 | |
| 1153 | // If it is a non-local file, show a link to the file in its own repository |
| 1154 | // @todo abstract this for remote content that isn't a file |
| 1155 | if ( $isRemoteContent ) { |
| 1156 | $content_navigation['views']['view-foreign'] = [ |
| 1157 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1158 | 'view-foreign', $page->getWikiDisplayName() |
| 1159 | ), |
| 1160 | 'icon' => 'eye', |
| 1161 | 'href' => $page->getSourceURL(), |
| 1162 | 'primary' => false, |
| 1163 | ]; |
| 1164 | } |
| 1165 | |
| 1166 | // Checks if user can edit the current page if it exists or create it otherwise |
| 1167 | if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) { |
| 1168 | // Builds CSS class for talk page links |
| 1169 | $isTalkClass = $isTalk ? ' istalk' : ''; |
| 1170 | // Whether the user is editing the page |
| 1171 | $isEditing = $onPage && ( $action == 'edit' || $action == 'submit' ); |
| 1172 | $isRedirect = $page && $page->isRedirect(); |
| 1173 | // Whether to show the "Add a new section" tab |
| 1174 | // Checks if this is a current rev of talk page and is not forced to be hidden |
| 1175 | $showNewSection = !$out->getOutputFlag( ParserOutputFlags::HIDE_NEW_SECTION ) && ( |
| 1176 | ( |
| 1177 | $isTalk && !$isRedirect && $out->isRevisionCurrent() |
| 1178 | ) || |
| 1179 | $out->getOutputFlag( ParserOutputFlags::NEW_SECTION ) |
| 1180 | ); |
| 1181 | $section = $request->getVal( 'section' ); |
| 1182 | |
| 1183 | if ( $title->exists() |
| 1184 | || ( $title->inNamespace( NS_MEDIAWIKI ) |
| 1185 | && $title->getDefaultMessageText() !== false |
| 1186 | ) |
| 1187 | ) { |
| 1188 | $msgKey = $isRemoteContent ? 'edit-local' : 'edit'; |
| 1189 | } else { |
| 1190 | $msgKey = $isRemoteContent ? 'create-local' : 'create'; |
| 1191 | } |
| 1192 | $content_navigation['views']['edit'] = [ |
| 1193 | 'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection ) |
| 1194 | ? 'selected' |
| 1195 | : null |
| 1196 | ) . $isTalkClass, |
| 1197 | 'icon' => 'edit', |
| 1198 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1199 | "view-$msgKey" |
| 1200 | ), |
| 1201 | 'single-id' => "ca-$msgKey", |
| 1202 | 'href' => $title->getLocalURL( $this->editUrlOptions() ), |
| 1203 | 'primary' => !$isRemoteContent, // don't collapse this in vector |
| 1204 | ]; |
| 1205 | |
| 1206 | // section link |
| 1207 | if ( $showNewSection ) { |
| 1208 | // Adds new section link |
| 1209 | // $content_navigation['actions']['addsection'] |
| 1210 | $content_navigation['views']['addsection'] = [ |
| 1211 | 'class' => ( $isEditing && $section == 'new' ) ? 'selected' : null, |
| 1212 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1213 | "action-addsection" |
| 1214 | ), |
| 1215 | 'icon' => 'speechBubbleAdd', |
| 1216 | 'href' => $title->getLocalURL( 'action=edit§ion=new' ) |
| 1217 | ]; |
| 1218 | } |
| 1219 | // Checks if the page has some kind of viewable source content |
| 1220 | } elseif ( $title->hasSourceText() ) { |
| 1221 | // Adds view source view link |
| 1222 | $content_navigation['views']['viewsource'] = [ |
| 1223 | 'class' => ( $onPage && $action == 'edit' ) ? 'selected' : null, |
| 1224 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1225 | "action-viewsource" |
| 1226 | ), |
| 1227 | 'icon' => 'editLock', |
| 1228 | 'href' => $title->getLocalURL( $this->editUrlOptions() ), |
| 1229 | 'primary' => true, // don't collapse this in vector |
| 1230 | ]; |
| 1231 | } |
| 1232 | |
| 1233 | // Checks if the page exists |
| 1234 | if ( $title->exists() ) { |
| 1235 | // Adds history view link |
| 1236 | $content_navigation['views']['history'] = [ |
| 1237 | 'class' => ( $onPage && $action == 'history' ) ? 'selected' : null, |
| 1238 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1239 | 'view-history' |
| 1240 | ), |
| 1241 | 'icon' => 'history', |
| 1242 | 'href' => $title->getLocalURL( 'action=history' ), |
| 1243 | ]; |
| 1244 | |
| 1245 | if ( $this->getAuthority()->probablyCan( 'delete', $title ) ) { |
| 1246 | $content_navigation['actions']['delete'] = [ |
| 1247 | 'icon' => 'trash', |
| 1248 | 'class' => ( $onPage && $action == 'delete' ) ? 'selected' : null, |
| 1249 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1250 | 'action-delete' |
| 1251 | ), |
| 1252 | 'href' => $title->getLocalURL( [ |
| 1253 | 'action' => 'delete', |
| 1254 | 'oldid' => $title->getLatestRevID(), |
| 1255 | ] ) |
| 1256 | ]; |
| 1257 | } |
| 1258 | |
| 1259 | if ( $this->getAuthority()->probablyCan( 'move', $title ) ) { |
| 1260 | $moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() ); |
| 1261 | $content_navigation['actions']['move'] = [ |
| 1262 | 'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : null, |
| 1263 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1264 | 'action-move' |
| 1265 | ), |
| 1266 | 'icon' => 'move', |
| 1267 | 'href' => $moveTitle->getLocalURL() |
| 1268 | ]; |
| 1269 | } |
| 1270 | } else { |
| 1271 | // article doesn't exist or is deleted |
| 1272 | if ( $this->getAuthority()->probablyCan( 'deletedhistory', $title ) ) { |
| 1273 | $n = $title->getDeletedEditsCount(); |
| 1274 | if ( $n ) { |
| 1275 | $undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ); |
| 1276 | // If the user can't undelete but can view deleted |
| 1277 | // history show them a "View .. deleted" tab instead. |
| 1278 | $msgKey = $this->getAuthority()->probablyCan( 'undelete', $title ) ? |
| 1279 | 'undelete' : 'viewdeleted'; |
| 1280 | $content_navigation['actions']['undelete'] = [ |
| 1281 | 'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : null, |
| 1282 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1283 | "action-$msgKey", $n |
| 1284 | ), |
| 1285 | 'icon' => 'trash', |
| 1286 | 'href' => $undelTitle->getLocalURL() |
| 1287 | ]; |
| 1288 | } |
| 1289 | } |
| 1290 | } |
| 1291 | |
| 1292 | $restrictionStore = $services->getRestrictionStore(); |
| 1293 | if ( $this->getAuthority()->probablyCan( 'protect', $title ) && |
| 1294 | $restrictionStore->listApplicableRestrictionTypes( $title ) && |
| 1295 | $permissionManager->getNamespaceRestrictionLevels( |
| 1296 | $title->getNamespace(), |
| 1297 | $performer->getUser() |
| 1298 | ) !== [ '' ] |
| 1299 | ) { |
| 1300 | $isProtected = $restrictionStore->isProtected( $title ); |
| 1301 | $mode = $isProtected ? 'unprotect' : 'protect'; |
| 1302 | $content_navigation['actions'][$mode] = [ |
| 1303 | 'class' => ( $onPage && $action == $mode ) ? 'selected' : null, |
| 1304 | 'text' => $this->getSkinNavOverrideableLabel( |
| 1305 | "action-$mode" |
| 1306 | ), |
| 1307 | 'icon' => $isProtected ? 'unLock' : 'lock', |
| 1308 | 'href' => $title->getLocalURL( "action=$mode" ) |
| 1309 | ]; |
| 1310 | } |
| 1311 | |
| 1312 | if ( $this->loggedin && $this->getAuthority() |
| 1313 | ->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) |
| 1314 | ) { |
| 1315 | /** |
| 1316 | * The following actions use messages which, if made particular to |
| 1317 | * the any specific skins, would break the Ajax code which makes this |
| 1318 | * action happen entirely inline. OutputPage::getJSVars |
| 1319 | * defines a set of messages in a javascript object - and these |
| 1320 | * messages are assumed to be global for all skins. Without making |
| 1321 | * a change to that procedure these messages will have to remain as |
| 1322 | * the global versions. |
| 1323 | */ |
| 1324 | $mode = MediaWikiServices::getInstance()->getWatchlistManager() |
| 1325 | ->isWatched( $performer, $title ) ? 'unwatch' : 'watch'; |
| 1326 | |
| 1327 | // Add the watch/unwatch link. |
| 1328 | $content_navigation['actions'][$mode] = $this->getWatchLinkAttrs( |
| 1329 | $mode, |
| 1330 | $performer, |
| 1331 | $title, |
| 1332 | $action, |
| 1333 | $onPage |
| 1334 | ); |
| 1335 | } |
| 1336 | } |
| 1337 | |
| 1338 | // Add language variants |
| 1339 | $languageConverterFactory = MediaWikiServices::getInstance()->getLanguageConverterFactory(); |
| 1340 | |
| 1341 | if ( $userCanRead && !$languageConverterFactory->isConversionDisabled() ) { |
| 1342 | $pageLang = $title->getPageLanguage(); |
| 1343 | $converter = $languageConverterFactory |
| 1344 | ->getLanguageConverter( $pageLang ); |
| 1345 | // Checks that language conversion is enabled and variants exist |
| 1346 | // And if it is not in the special namespace |
| 1347 | if ( $converter->hasVariants() ) { |
| 1348 | // Gets list of language variants |
| 1349 | $variants = $converter->getVariants(); |
| 1350 | // Gets preferred variant (note that user preference is |
| 1351 | // only possible for wiki content language variant) |
| 1352 | $preferred = $converter->getPreferredVariant(); |
| 1353 | if ( $action === 'view' ) { |
| 1354 | $params = $request->getQueryValues(); |
| 1355 | unset( $params['title'] ); |
| 1356 | } else { |
| 1357 | $params = []; |
| 1358 | } |
| 1359 | // Loops over each variant |
| 1360 | foreach ( $variants as $code ) { |
| 1361 | // Gets variant name from language code |
| 1362 | $varname = $pageLang->getVariantname( $code ); |
| 1363 | // Appends variant link |
| 1364 | $content_navigation['variants'][] = [ |
| 1365 | 'class' => ( $code == $preferred ) ? 'selected' : null, |
| 1366 | 'text' => $varname, |
| 1367 | 'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ), |
| 1368 | 'lang' => LanguageCode::bcp47( $code ), |
| 1369 | 'hreflang' => LanguageCode::bcp47( $code ), |
| 1370 | ]; |
| 1371 | } |
| 1372 | } |
| 1373 | } |
| 1374 | $namespaces = $associatedPages; |
| 1375 | } else { |
| 1376 | // If it's not content, and a request URL is set it's got to be a special page |
| 1377 | try { |
| 1378 | $url = $request->getRequestURL(); |
| 1379 | } catch ( MWException ) { |
| 1380 | $url = false; |
| 1381 | } |
| 1382 | $namespaces['special'] = [ |
| 1383 | 'class' => 'selected', |
| 1384 | 'text' => $this->msg( 'nstab-special' )->text(), |
| 1385 | 'href' => $url, // @see: T4457, T4510 |
| 1386 | 'context' => 'subject' |
| 1387 | ]; |
| 1388 | $associatedPages += $this->getSpecialPageAssociatedNavigationLinks( $title ); |
| 1389 | } |
| 1390 | |
| 1391 | $content_navigation['namespaces'] = $namespaces; |
| 1392 | $content_navigation['associated-pages'] = $associatedPages; |
| 1393 | $this->runOnSkinTemplateNavigationHooks( $this, $content_navigation ); |
| 1394 | |
| 1395 | // Setup xml ids and tooltip info |
| 1396 | foreach ( $content_navigation as $section => &$links ) { |
| 1397 | foreach ( $links as $key => &$link ) { |
| 1398 | // Allow links to set their own id for backwards compatibility reasons. |
| 1399 | if ( isset( $link['id'] ) || isset( $link['html' ] ) ) { |
| 1400 | continue; |
| 1401 | } |
| 1402 | $xmlID = $key; |
| 1403 | if ( isset( $link['context'] ) && $link['context'] == 'subject' ) { |
| 1404 | $xmlID = 'ca-nstab-' . $xmlID; |
| 1405 | } elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) { |
| 1406 | $xmlID = 'ca-talk'; |
| 1407 | $link['rel'] = 'discussion'; |
| 1408 | } elseif ( $section == 'variants' ) { |
| 1409 | $xmlID = 'ca-varlang-' . $xmlID; |
| 1410 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive |
| 1411 | $link['class'] .= ' ca-variants-' . $link['lang']; |
| 1412 | } else { |
| 1413 | $xmlID = 'ca-' . $xmlID; |
| 1414 | } |
| 1415 | $link['id'] = $xmlID; |
| 1416 | } |
| 1417 | } |
| 1418 | |
| 1419 | # We don't want to give the watch tab an accesskey if the |
| 1420 | # page is being edited, because that conflicts with the |
| 1421 | # accesskey on the watch checkbox. We also don't want to |
| 1422 | # give the edit tab an accesskey, because that's fairly |
| 1423 | # superfluous and conflicts with an accesskey (Ctrl-E) often |
| 1424 | # used for editing in Safari. |
| 1425 | if ( in_array( $action, [ 'edit', 'submit' ] ) ) { |
| 1426 | if ( isset( $content_navigation['views']['edit'] ) ) { |
| 1427 | $content_navigation['views']['edit']['tooltiponly'] = true; |
| 1428 | } |
| 1429 | if ( isset( $content_navigation['actions']['watch'] ) ) { |
| 1430 | $content_navigation['actions']['watch']['tooltiponly'] = true; |
| 1431 | } |
| 1432 | if ( isset( $content_navigation['actions']['unwatch'] ) ) { |
| 1433 | $content_navigation['actions']['unwatch']['tooltiponly'] = true; |
| 1434 | } |
| 1435 | } |
| 1436 | |
| 1437 | $this->contentNavigationCached = $content_navigation; |
| 1438 | return $content_navigation; |
| 1439 | } |
| 1440 | |
| 1441 | /** |
| 1442 | * Return a list of pages that have been marked as related to/associated with |
| 1443 | * the special page for display. |
| 1444 | * |
| 1445 | * @param Title $title |
| 1446 | * @return array |
| 1447 | */ |
| 1448 | private function getSpecialPageAssociatedNavigationLinks( Title $title ): array { |
| 1449 | $specialAssociatedNavigationLinks = []; |
| 1450 | $specialFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
| 1451 | $special = $specialFactory->getPage( $title->getText() ); |
| 1452 | if ( $special === null ) { |
| 1453 | // not a valid special page |
| 1454 | return []; |
| 1455 | } |
| 1456 | $special->setContext( $this ); |
| 1457 | $associatedNavigationLinks = $special->getAssociatedNavigationLinks(); |
| 1458 | // If there are no subpages, we should not render |
| 1459 | if ( count( $associatedNavigationLinks ) === 0 ) { |
| 1460 | return []; |
| 1461 | } |
| 1462 | |
| 1463 | foreach ( $associatedNavigationLinks as $i => $relatedTitleText ) { |
| 1464 | $relatedTitle = Title::newFromText( $relatedTitleText ); |
| 1465 | $special = $specialFactory->getPage( $relatedTitle->getText() ); |
| 1466 | if ( $special === null ) { |
| 1467 | $text = $relatedTitle->getText(); |
| 1468 | } else { |
| 1469 | $text = $special->getShortDescription( $relatedTitle->getSubpageText() ); |
| 1470 | } |
| 1471 | $specialAssociatedNavigationLinks['special-specialAssociatedNavigationLinks-link-' . $i ] = [ |
| 1472 | 'text' => $text, |
| 1473 | 'href' => $relatedTitle->getLocalURL(), |
| 1474 | 'class' => $relatedTitle->fixSpecialName()->equals( $title->fixSpecialName() ) ? 'selected' : null, |
| 1475 | ]; |
| 1476 | } |
| 1477 | return $specialAssociatedNavigationLinks; |
| 1478 | } |
| 1479 | |
| 1480 | /** |
| 1481 | * an array of edit links by default used for the tabs |
| 1482 | * @param array $content_navigation |
| 1483 | * @return array |
| 1484 | */ |
| 1485 | private function buildContentActionUrls( $content_navigation ) { |
| 1486 | // content_actions has been replaced with content_navigation for backwards |
| 1487 | // compatibility and also for skins that just want simple tabs content_actions |
| 1488 | // is now built by flattening the content_navigation arrays into one |
| 1489 | |
| 1490 | $content_actions = []; |
| 1491 | |
| 1492 | foreach ( $content_navigation as $links ) { |
| 1493 | foreach ( $links as $key => $value ) { |
| 1494 | if ( isset( $value['redundant'] ) && $value['redundant'] ) { |
| 1495 | // Redundant tabs are dropped from content_actions |
| 1496 | continue; |
| 1497 | } |
| 1498 | |
| 1499 | // content_actions used to have ids built using the "ca-$key" pattern |
| 1500 | // so the xmlID based id is much closer to the actual $key that we want |
| 1501 | // for that reason we'll just strip out the ca- if present and use |
| 1502 | // the latter potion of the "id" as the $key |
| 1503 | if ( isset( $value['id'] ) && str_starts_with( $value['id'], 'ca-' ) ) { |
| 1504 | $key = substr( $value['id'], 3 ); |
| 1505 | } |
| 1506 | |
| 1507 | if ( isset( $content_actions[$key] ) ) { |
| 1508 | wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " . |
| 1509 | "content_navigation into content_actions." ); |
| 1510 | continue; |
| 1511 | } |
| 1512 | |
| 1513 | $content_actions[$key] = $value; |
| 1514 | } |
| 1515 | } |
| 1516 | |
| 1517 | return $content_actions; |
| 1518 | } |
| 1519 | |
| 1520 | /** |
| 1521 | * Insert legacy menu items from content navigation into the personal toolbar. |
| 1522 | * |
| 1523 | * @internal |
| 1524 | * |
| 1525 | * @param array $contentNavigation |
| 1526 | * @return array |
| 1527 | */ |
| 1528 | final protected function injectLegacyMenusIntoPersonalTools( |
| 1529 | array $contentNavigation |
| 1530 | ): array { |
| 1531 | $userMenu = $contentNavigation['user-menu'] ?? []; |
| 1532 | // userpage is only defined for logged-in users, and wfArrayInsertAfter requires the |
| 1533 | // $after parameter to be a known key in the array. |
| 1534 | if ( isset( $contentNavigation['user-menu']['userpage'] ) && isset( $contentNavigation['notifications'] ) ) { |
| 1535 | $userMenu = wfArrayInsertAfter( |
| 1536 | $userMenu, |
| 1537 | $contentNavigation['notifications'], |
| 1538 | 'userpage' |
| 1539 | ); |
| 1540 | } |
| 1541 | if ( isset( $contentNavigation['user-interface-preferences'] ) ) { |
| 1542 | return array_merge( |
| 1543 | $contentNavigation['user-interface-preferences'], |
| 1544 | $userMenu |
| 1545 | ); |
| 1546 | } |
| 1547 | return $userMenu; |
| 1548 | } |
| 1549 | |
| 1550 | /** |
| 1551 | * Build the personal urls array. |
| 1552 | * |
| 1553 | * @internal |
| 1554 | * |
| 1555 | * @param array $contentNavigation |
| 1556 | * @return array |
| 1557 | */ |
| 1558 | private function makeSkinTemplatePersonalUrls( |
| 1559 | array $contentNavigation |
| 1560 | ): array { |
| 1561 | if ( isset( $contentNavigation['user-menu'] ) ) { |
| 1562 | return $this->injectLegacyMenusIntoPersonalTools( $contentNavigation ); |
| 1563 | } |
| 1564 | return []; |
| 1565 | } |
| 1566 | |
| 1567 | /** |
| 1568 | * @since 1.35 |
| 1569 | * @param array $attrs (optional) will be passed to tooltipAndAccesskeyAttribs |
| 1570 | * and decorate the resulting input |
| 1571 | * @return string of HTML input |
| 1572 | */ |
| 1573 | public function makeSearchInput( $attrs = [] ) { |
| 1574 | // It's possible that getTemplateData might be calling |
| 1575 | // Skin::makeSearchInput. To avoid infinite recursion create a |
| 1576 | // new instance of the search component here. |
| 1577 | $searchBox = $this->getComponent( 'search-box' ); |
| 1578 | $data = $searchBox->getTemplateData(); |
| 1579 | |
| 1580 | return Html::element( 'input', |
| 1581 | $data[ 'array-input-attributes' ] + $attrs |
| 1582 | ); |
| 1583 | } |
| 1584 | |
| 1585 | /** |
| 1586 | * @since 1.35 |
| 1587 | * @param string $mode representing the type of button wanted |
| 1588 | * either `go`, `fulltext` or `image` |
| 1589 | * @param array $attrs (optional) |
| 1590 | * @return string of HTML button |
| 1591 | */ |
| 1592 | final public function makeSearchButton( $mode, $attrs = [] ) { |
| 1593 | // It's possible that getTemplateData might be calling |
| 1594 | // Skin::makeSearchInput. To avoid infinite recursion create a |
| 1595 | // new instance of the search component here. |
| 1596 | $searchBox = $this->getComponent( 'search-box' ); |
| 1597 | $searchData = $searchBox->getTemplateData(); |
| 1598 | |
| 1599 | switch ( $mode ) { |
| 1600 | case 'go': |
| 1601 | $attrs['value'] ??= $this->msg( 'searcharticle' )->text(); |
| 1602 | return Html::element( |
| 1603 | 'input', |
| 1604 | array_merge( |
| 1605 | $searchData[ 'array-button-go-attributes' ], $attrs |
| 1606 | ) |
| 1607 | ); |
| 1608 | case 'fulltext': |
| 1609 | $attrs['value'] ??= $this->msg( 'searchbutton' )->text(); |
| 1610 | return Html::element( |
| 1611 | 'input', |
| 1612 | array_merge( |
| 1613 | $searchData[ 'array-button-fulltext-attributes' ], $attrs |
| 1614 | ) |
| 1615 | ); |
| 1616 | case 'image': |
| 1617 | $buttonAttrs = [ |
| 1618 | 'type' => 'submit', |
| 1619 | 'name' => 'button', |
| 1620 | ]; |
| 1621 | $buttonAttrs = array_merge( |
| 1622 | $buttonAttrs, |
| 1623 | Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ), |
| 1624 | $attrs |
| 1625 | ); |
| 1626 | unset( $buttonAttrs['src'] ); |
| 1627 | unset( $buttonAttrs['alt'] ); |
| 1628 | unset( $buttonAttrs['width'] ); |
| 1629 | unset( $buttonAttrs['height'] ); |
| 1630 | $imgAttrs = [ |
| 1631 | 'src' => $attrs['src'], |
| 1632 | 'alt' => $attrs['alt'] ?? $this->msg( 'searchbutton' )->text(), |
| 1633 | 'width' => $attrs['width'] ?? null, |
| 1634 | 'height' => $attrs['height'] ?? null, |
| 1635 | ]; |
| 1636 | return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) ); |
| 1637 | default: |
| 1638 | throw new InvalidArgumentException( 'Unknown mode passed to ' . __METHOD__ ); |
| 1639 | } |
| 1640 | } |
| 1641 | |
| 1642 | private function isSpecialContributeShowable(): bool { |
| 1643 | return ContributeFactory::isEnabledOnCurrentSkin( |
| 1644 | $this, |
| 1645 | $this->getConfig()->get( MainConfigNames::SpecialContributeSkinsEnabled ) |
| 1646 | ); |
| 1647 | } |
| 1648 | |
| 1649 | /** |
| 1650 | * @param array &$personal_urls |
| 1651 | * @param string $key |
| 1652 | * @param string|null $userName |
| 1653 | * @param bool $active |
| 1654 | * |
| 1655 | * @return array |
| 1656 | */ |
| 1657 | private function makeContributionsLink( |
| 1658 | array &$personal_urls, |
| 1659 | string $key, |
| 1660 | ?string $userName = null, |
| 1661 | bool $active = false |
| 1662 | ): array { |
| 1663 | $isSpecialContributeShowable = $this->isSpecialContributeShowable(); |
| 1664 | $subpage = $userName ?? false; |
| 1665 | $user = $this->getUser(); |
| 1666 | // If the "Contribute" page is showable and the user is anon. or has no edit count, |
| 1667 | // direct them to the "Contribute" page instead of the "Contributions" or "Mycontributions" pages. |
| 1668 | // Explanation: |
| 1669 | // a. For logged-in users: In wikis where the "Contribute" page is enabled, we only want |
| 1670 | // to navigate logged-in users to the "Contribute", when they have done no edits. Otherwise, we |
| 1671 | // want to navigate them to the "Mycontributions" page to easily access their edits/contributions. |
| 1672 | // Currently, the "Contribute" page is used as target for all logged-in users. |
| 1673 | // b. For anon. users: In wikis where the "Contribute" page is enabled, we still navigate the |
| 1674 | // anonymous users to the "Contribute" page. |
| 1675 | // Task: T369041 |
| 1676 | if ( $isSpecialContributeShowable && (int)$user->getEditCount() === 0 ) { |
| 1677 | $href = SkinComponentUtils::makeSpecialUrlSubpage( |
| 1678 | 'Contribute', |
| 1679 | false |
| 1680 | ); |
| 1681 | $personal_urls['contribute'] = [ |
| 1682 | 'text' => $this->msg( 'contribute' )->text(), |
| 1683 | 'href' => $href, |
| 1684 | 'active' => $href == $this->getTitle()->getLocalURL(), |
| 1685 | 'icon' => 'edit' |
| 1686 | ]; |
| 1687 | } else { |
| 1688 | $href = SkinComponentUtils::makeSpecialUrlSubpage( |
| 1689 | $subpage !== false ? 'Contributions' : 'Mycontributions', |
| 1690 | $subpage |
| 1691 | ); |
| 1692 | $personal_urls[$key] = [ |
| 1693 | 'text' => $this->msg( $key )->text(), |
| 1694 | 'href' => $href, |
| 1695 | 'active' => $active, |
| 1696 | 'icon' => 'userContributions' |
| 1697 | ]; |
| 1698 | } |
| 1699 | return $personal_urls; |
| 1700 | } |
| 1701 | |
| 1702 | } |
| 1703 | |
| 1704 | /** @deprecated class alias since 1.44 */ |
| 1705 | class_alias( SkinTemplate::class, 'SkinTemplate' ); |