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