Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
55.57% |
454 / 817 |
|
19.44% |
7 / 36 |
CRAP | |
0.00% |
0 / 1 |
SkinTemplate | |
55.57% |
454 / 817 |
|
19.44% |
7 / 36 |
4154.08 | |
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.12% |
16 / 17 |
|
0.00% |
0 / 1 |
4.00 | |||
generateHTML | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
outputPage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getTemplateData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prepareQuickTemplate | |
100.00% |
108 / 108 |
|
100.00% |
1 / 1 |
7 | |||
makePersonalToolsList | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getStructuredPersonalTools | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
buildPersonalUrls | |
53.33% |
40 / 75 |
|
0.00% |
0 / 1 |
65.82 | |||
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 / 15 |
|
0.00% |
0 / 1 |
12 | |||
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 | |
73.53% |
25 / 34 |
|
0.00% |
0 / 1 |
10.50 | |||
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 | |||
buildContentNavigationUrls | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
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 / 7 |
|
0.00% |
0 / 1 |
2 | |||
makeSearchButtonInternal | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
30 | |||
isSpecialContributeShowable | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
makeContributionsLink | |
58.33% |
14 / 24 |
|
0.00% |
0 / 1 |
3.65 |
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\Html\Html; |
24 | use MediaWiki\Linker\Linker; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Permissions\Authority; |
28 | use MediaWiki\ResourceLoader as RL; |
29 | use MediaWiki\Skin\SkinComponentUtils; |
30 | use MediaWiki\SpecialPage\SpecialPage; |
31 | use MediaWiki\Specials\Contribute\ContributeFactory; |
32 | use MediaWiki\Title\Title; |
33 | |
34 | /** |
35 | * Base class for QuickTemplate-based skins. |
36 | * |
37 | * The template data is filled in SkinTemplate::prepareQuickTemplate. |
38 | * |
39 | * @stable to extend |
40 | * @ingroup Skins |
41 | */ |
42 | class SkinTemplate extends Skin { |
43 | /** |
44 | * @var string For QuickTemplate, the name of the subclass which will |
45 | * actually fill the template. |
46 | */ |
47 | public $template; |
48 | |
49 | public $thispage; |
50 | public $titletxt; |
51 | public $userpage; |
52 | // TODO: Rename this to $isRegistered (but that's a breaking change) |
53 | public $loggedin; |
54 | public $username; |
55 | public $userpageUrlDetails; |
56 | |
57 | /** @var bool */ |
58 | private $isTempUser; |
59 | |
60 | /** @var bool */ |
61 | private $isNamedUser; |
62 | |
63 | private $templateContextSet = false; |
64 | /** @var array|null */ |
65 | private $contentNavigationCached; |
66 | /** @var array|null */ |
67 | private $portletsCached; |
68 | |
69 | /** |
70 | * Create the template engine object; we feed it a bunch of data |
71 | * and eventually it spits out some HTML. Should have interface |
72 | * roughly equivalent to PHPTAL 0.7. |
73 | * |
74 | * @param string $classname |
75 | * @return QuickTemplate |
76 | */ |
77 | protected function setupTemplate( $classname ) { |
78 | return new $classname( $this->getConfig() ); |
79 | } |
80 | |
81 | /** |
82 | * @return QuickTemplate |
83 | */ |
84 | protected function setupTemplateForOutput() { |
85 | $this->setupTemplateContext(); |
86 | $template = $this->options['template'] ?? $this->template; |
87 | if ( !$template ) { |
88 | throw new RuntimeException( |
89 | 'SkinTemplate skins must define a `template` either as a public' |
90 | . ' property of by passing in a`template` option to the constructor.' |
91 | ); |
92 | } |
93 | $tpl = $this->setupTemplate( $template ); |
94 | return $tpl; |
95 | } |
96 | |
97 | /** |
98 | * Setup class properties that are necessary prior to calling |
99 | * setupTemplateForOutput. It must be called inside |
100 | * prepareQuickTemplate. |
101 | * This function may set local class properties that will be used |
102 | * by other methods, but should not make assumptions about the |
103 | * implementation of setupTemplateForOutput |
104 | * @since 1.35 |
105 | */ |
106 | final protected function setupTemplateContext() { |
107 | if ( $this->templateContextSet ) { |
108 | return; |
109 | } |
110 | |
111 | $request = $this->getRequest(); |
112 | $user = $this->getUser(); |
113 | $title = $this->getTitle(); |
114 | $this->thispage = $title->getPrefixedDBkey(); |
115 | $this->titletxt = $title->getPrefixedText(); |
116 | $userpageTitle = $user->getUserPage(); |
117 | $this->userpage = $userpageTitle->getPrefixedText(); |
118 | $this->loggedin = $user->isRegistered(); |
119 | $this->username = $user->getName(); |
120 | $this->isTempUser = $user->isTemp(); |
121 | $this->isNamedUser = $this->loggedin && !$this->isTempUser; |
122 | |
123 | if ( $this->isNamedUser ) { |
124 | $this->userpageUrlDetails = self::makeUrlDetails( $userpageTitle ); |
125 | } else { |
126 | # This won't be used in the standard skins, but we define it to preserve the interface |
127 | # To save time, we check for existence |
128 | $this->userpageUrlDetails = self::makeKnownUrlDetails( $userpageTitle ); |
129 | } |
130 | |
131 | $this->templateContextSet = true; |
132 | } |
133 | |
134 | /** |
135 | * Subclasses not wishing to use the QuickTemplate |
136 | * render method can rewrite this method, for example to use |
137 | * TemplateParser::processTemplate |
138 | * @since 1.35 |
139 | * @return string of complete skin HTML to output to the page. This varies based on |
140 | * the skin option bodyOnly (see Skin::getOptions): |
141 | * - If true, HTML includes `<!DOCTYPE>` and opening and closing html tags |
142 | * - If false, HTML is the contents of the body tag. |
143 | */ |
144 | public function generateHTML() { |
145 | $tpl = $this->prepareQuickTemplate(); |
146 | $options = $this->getOptions(); |
147 | $out = $this->getOutput(); |
148 | // execute template |
149 | ob_start(); |
150 | $tpl->execute(); |
151 | $html = ob_get_contents(); |
152 | ob_end_clean(); |
153 | |
154 | // If skin is using bodyOnly mode, for now we must output head and tail. |
155 | // In future when this is the default, |
156 | // this logic will be moved into the OutputPage::output method. |
157 | if ( $options['bodyOnly'] ) { |
158 | $html = $out->headElement( $this ) . $html . $out->tailElement( $this ); |
159 | } |
160 | |
161 | return $html; |
162 | } |
163 | |
164 | /** |
165 | * Initialize various variables and generate the template |
166 | * @stable to override |
167 | */ |
168 | public function outputPage() { |
169 | Profiler::instance()->setAllowOutput(); |
170 | $out = $this->getOutput(); |
171 | |
172 | $this->initPage( $out ); |
173 | $out->addJsConfigVars( $this->getJsConfigVars() ); |
174 | |
175 | // result may be an error |
176 | echo $this->generateHTML(); |
177 | } |
178 | |
179 | /** |
180 | * @inheritDoc |
181 | */ |
182 | public function getTemplateData() { |
183 | return parent::getTemplateData() + $this->getPortletsTemplateData(); |
184 | } |
185 | |
186 | /** |
187 | * initialize various variables and generate the template |
188 | * |
189 | * @since 1.23 |
190 | * @return QuickTemplate The template to be executed by outputPage |
191 | */ |
192 | protected function prepareQuickTemplate() { |
193 | $title = $this->getTitle(); |
194 | $request = $this->getRequest(); |
195 | $out = $this->getOutput(); |
196 | $config = $this->getConfig(); |
197 | $tpl = $this->setupTemplateForOutput(); |
198 | |
199 | $tpl->set( 'title', $out->getPageTitle() ); |
200 | $tpl->set( 'pagetitle', $out->getHTMLTitle() ); |
201 | |
202 | $tpl->set( 'thispage', $this->thispage ); |
203 | $tpl->set( 'titleprefixeddbkey', $this->thispage ); |
204 | $tpl->set( 'titletext', $title->getText() ); |
205 | $tpl->set( 'articleid', $title->getArticleID() ); |
206 | |
207 | $tpl->set( 'isarticle', $out->isArticle() ); |
208 | |
209 | $tpl->set( 'subtitle', $this->prepareSubtitle() ); |
210 | $tpl->set( 'undelete', $this->prepareUndeleteLink() ); |
211 | |
212 | $tpl->set( 'catlinks', $this->getCategories() ); |
213 | $feeds = $this->buildFeedUrls(); |
214 | $tpl->set( 'feeds', count( $feeds ) ? $feeds : false ); |
215 | |
216 | $tpl->set( 'mimetype', $config->get( MainConfigNames::MimeType ) ); |
217 | $tpl->set( 'charset', 'UTF-8' ); |
218 | $tpl->set( 'wgScript', $config->get( MainConfigNames::Script ) ); |
219 | $tpl->set( 'skinname', $this->skinname ); |
220 | $tpl->set( 'skinclass', static::class ); |
221 | $tpl->set( 'skin', $this ); |
222 | $tpl->set( 'printable', $out->isPrintable() ); |
223 | $tpl->set( 'handheld', $request->getBool( 'handheld' ) ); |
224 | $tpl->set( 'loggedin', $this->loggedin ); |
225 | $tpl->set( 'notspecialpage', !$title->isSpecialPage() ); |
226 | |
227 | $searchTitle = SpecialPage::newSearchPage( $this->getUser() ); |
228 | $searchLink = $searchTitle->getLocalURL(); |
229 | $tpl->set( 'searchaction', $searchLink ); |
230 | $tpl->deprecate( 'searchaction', '1.36' ); |
231 | |
232 | $tpl->set( 'searchtitle', $searchTitle->getPrefixedDBkey() ); |
233 | $tpl->set( 'search', trim( $request->getVal( 'search', '' ) ) ); |
234 | $tpl->set( 'stylepath', $config->get( MainConfigNames::StylePath ) ); |
235 | $tpl->set( 'articlepath', $config->get( MainConfigNames::ArticlePath ) ); |
236 | $tpl->set( 'scriptpath', $config->get( MainConfigNames::ScriptPath ) ); |
237 | $tpl->set( 'serverurl', $config->get( MainConfigNames::Server ) ); |
238 | $logos = RL\SkinModule::getAvailableLogos( $config ); |
239 | $tpl->set( 'logopath', $logos['1x'] ); |
240 | $tpl->set( 'sitename', $config->get( MainConfigNames::Sitename ) ); |
241 | |
242 | $userLang = $this->getLanguage(); |
243 | $userLangCode = $userLang->getHtmlCode(); |
244 | $userLangDir = $userLang->getDir(); |
245 | |
246 | $tpl->set( 'lang', $userLangCode ); |
247 | $tpl->set( 'dir', $userLangDir ); |
248 | $tpl->set( 'rtl', $userLang->isRTL() ); |
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 | # Personal toolbar |
307 | $tpl->set( 'personal_urls', $this->makeSkinTemplatePersonalUrls( $content_navigation ) ); |
308 | // The user-menu, notifications, and user-interface-preferences are new content navigation entries which aren't |
309 | // expected to be part of content_navigation or content_actions. Adding them in there breaks skins that do not |
310 | // expect it. (See T316196) |
311 | unset( |
312 | $content_navigation['user-menu'], |
313 | $content_navigation['notifications'], |
314 | $content_navigation['user-page'], |
315 | $content_navigation['user-interface-preferences'], |
316 | $content_navigation['category-normal'], |
317 | $content_navigation['category-hidden'], |
318 | $content_navigation['associated-pages'] |
319 | ); |
320 | $content_actions = $this->buildContentActionUrls( $content_navigation ); |
321 | $tpl->set( 'content_navigation', $content_navigation ); |
322 | $tpl->set( 'content_actions', $content_actions ); |
323 | |
324 | $tpl->set( 'sidebar', $this->buildSidebar() ); |
325 | $tpl->set( 'nav_urls', $this->buildNavUrls() ); |
326 | |
327 | // Set the head scripts near the end, in case the above actions resulted in added scripts |
328 | $tpl->set( 'headelement', $out->headElement( $this ) ); |
329 | $tpl->deprecate( 'headelement', '1.39' ); |
330 | |
331 | $tpl->set( 'debug', '' ); |
332 | $tpl->set( 'debughtml', MWDebug::getHTMLDebugLog() ); |
333 | |
334 | // Set the bodytext to another key so that skins can just output it on its own |
335 | // and output printfooter and debughtml separately |
336 | $tpl->set( 'bodycontent', $tpl->data['bodytext'] ); |
337 | |
338 | // Append printfooter and debughtml onto bodytext so that skins that |
339 | // were already using bodytext before they were split out don't suddenly |
340 | // start not outputting information. |
341 | $tpl->data['bodytext'] .= Html::rawElement( |
342 | 'div', |
343 | [ 'class' => 'printfooter' ], |
344 | "\n{$tpl->data['printfooter']}" |
345 | ) . "\n"; |
346 | $tpl->data['bodytext'] .= $tpl->data['debughtml']; |
347 | |
348 | // allow extensions adding stuff after the page content. |
349 | // See Skin::afterContentHook() for further documentation. |
350 | $tpl->set( 'dataAfterContent', $this->afterContentHook() ); |
351 | |
352 | return $tpl; |
353 | } |
354 | |
355 | /** |
356 | * Get the HTML for the personal tools list |
357 | * @since 1.31 |
358 | * |
359 | * @param array|null $personalTools |
360 | * @param array $options |
361 | * @return string |
362 | */ |
363 | public function makePersonalToolsList( $personalTools = null, $options = [] ) { |
364 | $html = ''; |
365 | |
366 | if ( $personalTools === null ) { |
367 | $personalTools = $this->getPersonalToolsForMakeListItem( |
368 | $this->buildPersonalUrls() |
369 | ); |
370 | } |
371 | |
372 | foreach ( $personalTools as $key => $item ) { |
373 | $html .= $this->makeListItem( $key, $item, $options ); |
374 | } |
375 | |
376 | return $html; |
377 | } |
378 | |
379 | /** |
380 | * Get personal tools for the user |
381 | * |
382 | * @since 1.31 |
383 | * |
384 | * @return array[] |
385 | */ |
386 | public function getStructuredPersonalTools() { |
387 | return $this->getPersonalToolsForMakeListItem( |
388 | $this->buildPersonalUrls() |
389 | ); |
390 | } |
391 | |
392 | /** |
393 | * Build array of urls for personal toolbar |
394 | * |
395 | * @param bool $includeNotifications Since 1.36, notifications are optional |
396 | * @return array |
397 | */ |
398 | protected function buildPersonalUrls( bool $includeNotifications = true ) { |
399 | $this->setupTemplateContext(); |
400 | $title = $this->getTitle(); |
401 | $authority = $this->getAuthority(); |
402 | $request = $this->getRequest(); |
403 | $pageurl = $title->getLocalURL(); |
404 | $services = MediaWikiServices::getInstance(); |
405 | $authManager = $services->getAuthManager(); |
406 | $groupPermissionsLookup = $services->getGroupPermissionsLookup(); |
407 | $returnto = SkinComponentUtils::getReturnToParam( $title, $request, $authority ); |
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 "sub page" 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 | } else { |
471 | $tempUserConfig = $services->getTempUserConfig(); |
472 | $canEdit = $authority->isAllowed( 'edit' ); |
473 | $canEditWithTemp = $tempUserConfig->isAutoCreateAction( 'edit' ); |
474 | // No need to show Talk and Contributions to anons if they can't contribute! |
475 | if ( $canEdit || $canEditWithTemp ) { |
476 | // Non interactive placeholder for anonymous users. |
477 | // It's unstyled by default (black color). Skin that |
478 | // needs it, can style it using the 'pt-anonuserpage' id. |
479 | // Skin that does not need it should unset it. |
480 | $personal_urls['anonuserpage'] = [ |
481 | 'text' => $this->msg( 'notloggedin' )->text(), |
482 | ]; |
483 | } |
484 | if ( $canEdit ) { |
485 | // Because of caching, we can't link directly to the IP talk and |
486 | // contributions pages. Instead we use the special page shortcuts |
487 | // (which work correctly regardless of caching). This means we can't |
488 | // determine whether these links are active or not, but since major |
489 | // skins (MonoBook, Vector) don't use this information, it's not a |
490 | // huge loss. |
491 | $personal_urls['anontalk'] = [ |
492 | 'text' => $this->msg( 'anontalk' )->text(), |
493 | 'href' => SkinComponentUtils::makeSpecialUrlSubpage( 'Mytalk', false ), |
494 | 'active' => false, |
495 | 'icon' => 'userTalk', |
496 | ]; |
497 | $personal_urls = $this->makeContributionsLink( $personal_urls, 'anoncontribs', null, false ); |
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', |
688 | // Note: userlogout link must always contain an & character, otherwise we might not be able |
689 | // to detect a buggy precaching proxy (T19790) |
690 | ( $title->isSpecial( 'Preferences' ) ? [] : $returnto ) ), |
691 | 'active' => false, |
692 | 'icon' => 'logOut' |
693 | ]; |
694 | } |
695 | |
696 | /** |
697 | * Build "Create Account" link data. |
698 | * @unstable |
699 | * |
700 | * @param string[] $returnto query params for the page to return to |
701 | * @return array |
702 | */ |
703 | protected function buildCreateAccountData( $returnto ) { |
704 | $title = $this->getTitle(); |
705 | |
706 | return [ |
707 | 'single-id' => 'pt-createaccount', |
708 | 'text' => $this->msg( 'pt-createaccount' )->text(), |
709 | 'href' => SkinComponentUtils::makeSpecialUrl( 'CreateAccount', $returnto ), |
710 | 'active' => $title->isSpecial( 'CreateAccount' ), |
711 | 'icon' => 'userAdd' |
712 | ]; |
713 | } |
714 | |
715 | /** |
716 | * Add the userpage link to the array |
717 | * |
718 | * @param array &$links Links array to append to |
719 | * @param string $idSuffix Something to add to the IDs to make them unique |
720 | */ |
721 | private function addPersonalPageItem( &$links, $idSuffix ) { |
722 | if ( $this->isNamedUser ) { // T340152 |
723 | $links['userpage'] = $this->buildPersonalPageItem( 'pt-userpage' . $idSuffix ); |
724 | } |
725 | } |
726 | |
727 | /** |
728 | * Build a user page link data. |
729 | * |
730 | * @param string $id of user page item to be output in HTML attribute (optional) |
731 | * @return array |
732 | */ |
733 | protected function buildPersonalPageItem( $id = 'pt-userpage' ): array { |
734 | $linkClasses = $this->userpageUrlDetails['exists'] ? [] : [ 'new' ]; |
735 | // T335440 Temp accounts dont show a user page link |
736 | // But we still need to update the user icon, as its used by other UI elements |
737 | $icon = $this->isTempUser ? 'userTemporary' : 'userAvatar'; |
738 | $href = &$this->userpageUrlDetails['href']; |
739 | return [ |
740 | 'id' => $id, |
741 | 'single-id' => 'pt-userpage', |
742 | 'text' => $this->username, |
743 | 'href' => $href, |
744 | 'link-class' => $linkClasses, |
745 | 'exists' => $this->userpageUrlDetails['exists'], |
746 | 'active' => ( $this->userpageUrlDetails['href'] == $this->getTitle()->getLocalURL() ), |
747 | 'icon' => $icon, |
748 | ]; |
749 | } |
750 | |
751 | /** |
752 | * Build a watchlist link data. |
753 | * |
754 | * @return array Array of data required to create a watchlist link. |
755 | */ |
756 | private function buildWatchlistData() { |
757 | $href = SkinComponentUtils::makeSpecialUrl( 'Watchlist' ); |
758 | $pageurl = $this->getTitle()->getLocalURL(); |
759 | |
760 | return [ |
761 | 'single-id' => 'pt-watchlist', |
762 | 'text' => $this->msg( 'mywatchlist' )->text(), |
763 | 'href' => $href, |
764 | 'active' => ( $href == $pageurl ), |
765 | 'icon' => 'watchlist' |
766 | ]; |
767 | } |
768 | |
769 | /** |
770 | * Builds an array with tab definition |
771 | * |
772 | * @param Title $title Page Where the tab links to |
773 | * @param string|string[]|MessageSpecifier $message Message or an array of message keys |
774 | * (will fall back) |
775 | * @param bool $selected Display the tab as selected |
776 | * @param string $query Query string attached to tab URL |
777 | * @param bool $checkEdit Check if $title exists and mark with .new if one doesn't |
778 | * |
779 | * @return array |
780 | * @param-taint $message tainted |
781 | */ |
782 | public function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) { |
783 | $classes = []; |
784 | if ( $selected ) { |
785 | $classes[] = 'selected'; |
786 | } |
787 | $exists = true; |
788 | if ( $checkEdit && !$title->isKnown() ) { |
789 | // Selected tabs should not show as red link. It doesn't make sense |
790 | // to show a red link on a page the user has already navigated to. |
791 | // https://phabricator.wikimedia.org/T294129#7451549 |
792 | if ( !$selected ) { |
793 | $classes[] = 'new'; |
794 | } |
795 | $exists = false; |
796 | if ( $query !== '' ) { |
797 | $query = 'action=edit&redlink=1&' . $query; |
798 | } else { |
799 | $query = 'action=edit&redlink=1'; |
800 | } |
801 | } |
802 | |
803 | $services = MediaWikiServices::getInstance(); |
804 | $linkClass = $services->getLinkRenderer()->getLinkClasses( $title ); |
805 | |
806 | if ( $message instanceof MessageSpecifier ) { |
807 | $msg = new Message( $message ); |
808 | } else { |
809 | // wfMessageFallback will nicely accept $message as an array of fallbacks |
810 | // or just a single key |
811 | $msg = wfMessageFallback( $message ); |
812 | } |
813 | $msg->setContext( $this->getContext() ); |
814 | if ( !$msg->isDisabled() ) { |
815 | $text = $msg->text(); |
816 | } else { |
817 | $text = $services->getLanguageConverterFactory() |
818 | ->getLanguageConverter( $services->getContentLanguage() ) |
819 | ->convertNamespace( |
820 | $services->getNamespaceInfo() |
821 | ->getSubject( $title->getNamespace() ) |
822 | ); |
823 | } |
824 | |
825 | $result = [ |
826 | 'class' => implode( ' ', $classes ), |
827 | 'text' => $text, |
828 | 'href' => $title->getLocalURL( $query ), |
829 | 'exists' => $exists, |
830 | 'primary' => true ]; |
831 | if ( $linkClass !== '' ) { |
832 | $result['link-class'] = $linkClass; |
833 | } |
834 | |
835 | return $result; |
836 | } |
837 | |
838 | /** |
839 | * Get a message label that skins can override. |
840 | * |
841 | * @param string $labelMessageKey |
842 | * @param mixed $param for the message |
843 | * @return string |
844 | */ |
845 | private function getSkinNavOverrideableLabel( $labelMessageKey, $param = null ) { |
846 | $skname = $this->skinname; |
847 | // The following messages can be used here: |
848 | // * skin-action-addsection |
849 | // * skin-action-delete |
850 | // * skin-action-move |
851 | // * skin-action-protect |
852 | // * skin-action-undelete |
853 | // * skin-action-unprotect |
854 | // * skin-action-viewdeleted |
855 | // * skin-action-viewsource |
856 | // * skin-view-create |
857 | // * skin-view-create-local |
858 | // * skin-view-edit |
859 | // * skin-view-edit-local |
860 | // * skin-view-foreign |
861 | // * skin-view-history |
862 | // * skin-view-view |
863 | $msg = wfMessageFallback( |
864 | "$skname-$labelMessageKey", |
865 | "skin-$labelMessageKey" |
866 | )->setContext( $this->getContext() ); |
867 | |
868 | if ( $param ) { |
869 | if ( is_numeric( $param ) ) { |
870 | $msg->numParams( $param ); |
871 | } else { |
872 | $msg->params( $param ); |
873 | } |
874 | } |
875 | return $msg->text(); |
876 | } |
877 | |
878 | /** |
879 | * @param string $name |
880 | * @param string|array $urlaction |
881 | * @return array |
882 | */ |
883 | private function makeTalkUrlDetails( $name, $urlaction = '' ) { |
884 | $title = Title::newFromTextThrow( $name )->getTalkPage(); |
885 | return [ |
886 | 'href' => $title->getLocalURL( $urlaction ), |
887 | 'exists' => $title->isKnown(), |
888 | ]; |
889 | } |
890 | |
891 | /** |
892 | * Get the attributes for the watch link. |
893 | * @param string $mode Either 'watch' or 'unwatch' |
894 | * @param Authority $performer |
895 | * @param Title $title |
896 | * @param string|null $action |
897 | * @param bool $onPage |
898 | * @return array |
899 | */ |
900 | private function getWatchLinkAttrs( |
901 | string $mode, Authority $performer, Title $title, ?string $action, bool $onPage |
902 | ): array { |
903 | $isWatchMode = $action == 'watch'; |
904 | $class = 'mw-watchlink ' . ( |
905 | $onPage && ( $isWatchMode || $action == 'unwatch' ) ? 'selected' : '' |
906 | ); |
907 | |
908 | $services = MediaWikiServices::getInstance(); |
909 | $watchlistManager = $services->getWatchlistManager(); |
910 | $watchIcon = $watchlistManager->isWatched( $performer, $title ) ? 'unStar' : 'star'; |
911 | $watchExpiry = null; |
912 | // Modify tooltip and add class identifying the page is temporarily watched, if applicable. |
913 | if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && |
914 | $watchlistManager->isTempWatched( $performer, $title ) |
915 | ) { |
916 | $class .= ' mw-watchlink-temp'; |
917 | $watchIcon = 'halfStar'; |
918 | |
919 | $watchStore = $services->getWatchedItemStore(); |
920 | $watchedItem = $watchStore->getWatchedItem( $performer->getUser(), $title ); |
921 | $diffInDays = $watchedItem->getExpiryInDays(); |
922 | $watchExpiry = $watchedItem->getExpiry( TS_ISO_8601 ); |
923 | if ( $diffInDays ) { |
924 | $msgParams = [ $diffInDays ]; |
925 | // Resolves to tooltip-ca-unwatch-expiring message |
926 | $tooltip = 'ca-unwatch-expiring'; |
927 | } else { |
928 | // Resolves to tooltip-ca-unwatch-expiring-hours message |
929 | $tooltip = 'ca-unwatch-expiring-hours'; |
930 | } |
931 | } |
932 | |
933 | return [ |
934 | 'class' => $class, |
935 | 'icon' => $watchIcon, |
936 | // uses 'watch' or 'unwatch' message |
937 | 'text' => $this->msg( $mode )->text(), |
938 | 'single-id' => $tooltip ?? null, |
939 | 'tooltip-params' => $msgParams ?? null, |
940 | 'href' => $title->getLocalURL( [ 'action' => $mode ] ), |
941 | // Set a data-mw=interface attribute, which the mediawiki.page.ajax |
942 | // module will look for to make sure it's a trusted link |
943 | 'data' => [ |
944 | 'mw' => 'interface', |
945 | 'mw-expiry' => $watchExpiry, |
946 | ], |
947 | ]; |
948 | } |
949 | |
950 | /** |
951 | * Run hooks relating to navigation menu data. |
952 | * Skins should extend this if they want to run opinionated transformations to the data after all |
953 | * hooks have been run. Note hooks are run in an arbitrary order. |
954 | * |
955 | * @param SkinTemplate $skin |
956 | * @param array &$content_navigation representing all menus. |
957 | * @since 1.37 |
958 | */ |
959 | protected function runOnSkinTemplateNavigationHooks( SkinTemplate $skin, &$content_navigation ) { |
960 | $beforeHookAssociatedPages = array_keys( $content_navigation['associated-pages'] ); |
961 | $beforeHookNamespaces = array_keys( $content_navigation['namespaces'] ); |
962 | |
963 | // Equiv to SkinTemplateContentActions, run |
964 | $this->getHookRunner()->onSkinTemplateNavigation__Universal( |
965 | $skin, $content_navigation ); |
966 | // The new `associatedPages` menu (added in 1.39) |
967 | // should be backwards compatibile with `namespaces`. |
968 | // To do this we look for hook modifications to both keys. If modifications are not |
969 | // made the new key, but are made to the old key, associatedPages reverts back to the |
970 | // links in the namespaces menu. |
971 | // It's expected in future that `namespaces` menu will become an alias for `associatedPages` |
972 | // at which point this code can be removed. |
973 | $afterHookNamespaces = array_keys( $content_navigation[ 'namespaces' ] ); |
974 | $afterHookAssociatedPages = array_keys( $content_navigation[ 'associated-pages' ] ); |
975 | $associatedPagesChanged = count( array_diff( $afterHookAssociatedPages, $beforeHookAssociatedPages ) ) > 0; |
976 | $namespacesChanged = count( array_diff( $afterHookNamespaces, $beforeHookNamespaces ) ) > 0; |
977 | // If some change occurred to namespaces via the hook, revert back to namespaces. |
978 | if ( !$associatedPagesChanged && $namespacesChanged ) { |
979 | $content_navigation['associated-pages'] = $content_navigation['namespaces']; |
980 | } |
981 | } |
982 | |
983 | /** |
984 | * a structured array of links usually used for the tabs in a skin |
985 | * |
986 | * There are 4 standard sections |
987 | * namespaces: Used for namespace tabs like special, page, and talk namespaces |
988 | * views: Used for primary page views like read, edit, history |
989 | * actions: Used for most extra page actions like deletion, protection, etc... |
990 | * variants: Used to list the language variants for the page |
991 | * |
992 | * Each section's value is a key/value array of links for that section. |
993 | * The links themselves have these common keys: |
994 | * - class: The css classes to apply to the tab |
995 | * - text: The text to display on the tab |
996 | * - href: The href for the tab to point to |
997 | * - rel: An optional rel= for the tab's link |
998 | * - redundant: If true the tab will be dropped in skins using content_actions |
999 | * this is useful for tabs like "Read" which only have meaning in skins that |
1000 | * take special meaning from the grouped structure of content_navigation |
1001 | * |
1002 | * Views also have an extra key which can be used: |
1003 | * - primary: If this is not true skins like vector may try to hide the tab |
1004 | * when the user has limited space in their browser window |
1005 | * |
1006 | * content_navigation using code also expects these ids to be present on the |
1007 | * links, however these are usually automatically generated by SkinTemplate |
1008 | * itself and are not necessary when using a hook. The only things these may |
1009 | * matter to are people modifying content_navigation after it's initial creation: |
1010 | * - id: A "preferred" id, most skins are best off outputting this preferred |
1011 | * id for best compatibility. |
1012 | * - tooltiponly: This is set to true for some tabs in cases where the system |
1013 | * believes that the accesskey should not be added to the tab. |
1014 | * |
1015 | * @return array |
1016 | */ |
1017 | private function buildContentNavigationUrlsInternal() { |
1018 | if ( $this->contentNavigationCached ) { |
1019 | return $this->contentNavigationCached; |
1020 | } |
1021 | // Display tabs for the relevant title rather than always the title itself |
1022 | $title = $this->getRelevantTitle(); |
1023 | $onPage = $title->equals( $this->getTitle() ); |
1024 | |
1025 | $out = $this->getOutput(); |
1026 | $request = $this->getRequest(); |
1027 | $performer = $this->getAuthority(); |
1028 | $action = $this->getContext()->getActionName(); |
1029 | $services = MediaWikiServices::getInstance(); |
1030 | $permissionManager = $services->getPermissionManager(); |
1031 | $categoriesData = $this->getCategoryPortletsData( $this->getOutput()->getCategoryLinks() ); |
1032 | $userPageLink = []; |
1033 | $this->addPersonalPageItem( $userPageLink, '-2' ); |
1034 | |
1035 | $content_navigation = $categoriesData + [ |
1036 | // Modern keys: Please ensure these get unset inside Skin::prepareQuickTemplate |
1037 | 'user-interface-preferences' => [], |
1038 | 'user-page' => $userPageLink, |
1039 | 'user-menu' => $this->buildPersonalUrls( false ), |
1040 | 'notifications' => [], |
1041 | 'associated-pages' => [], |
1042 | // Legacy keys |
1043 | 'namespaces' => [], |
1044 | 'views' => [], |
1045 | 'actions' => [], |
1046 | 'variants' => [] |
1047 | ]; |
1048 | |
1049 | $associatedPages = []; |
1050 | $namespaces = []; |
1051 | $userCanRead = $this->getAuthority()->probablyCan( 'read', $title ); |
1052 | |
1053 | // Checks if page is some kind of content |
1054 | if ( $title->canExist() ) { |
1055 | // Gets page objects for the associatedPages namespaces |
1056 | $subjectPage = $title->getSubjectPage(); |
1057 | $talkPage = $title->getTalkPage(); |
1058 | |
1059 | // Determines if this is a talk page |
1060 | $isTalk = $title->isTalkPage(); |
1061 | |
1062 | // Generates XML IDs from namespace names |
1063 | $subjectId = $title->getNamespaceKey( '' ); |
1064 | |
1065 | if ( $subjectId == 'main' ) { |
1066 | $talkId = 'talk'; |
1067 | } else { |
1068 | $talkId = "{$subjectId}_talk"; |
1069 | } |
1070 | |
1071 | // Adds namespace links |
1072 | if ( $subjectId === 'user' ) { |
1073 | $subjectMsg = $this->msg( 'nstab-user', $subjectPage->getRootText() ); |
1074 | } else { |
1075 | // The following messages are used here: |
1076 | // * nstab-main |
1077 | // * nstab-media |
1078 | // * nstab-special |
1079 | // * nstab-project |
1080 | // * nstab-image |
1081 | // * nstab-mediawiki |
1082 | // * nstab-template |
1083 | // * nstab-help |
1084 | // * nstab-category |
1085 | // * nstab-<subject namespace key> |
1086 | $subjectMsg = [ "nstab-$subjectId" ]; |
1087 | |
1088 | if ( $subjectPage->isMainPage() ) { |
1089 | array_unshift( $subjectMsg, 'nstab-mainpage' ); |
1090 | } |
1091 | } |
1092 | |
1093 | $associatedPages[$subjectId] = $this->tabAction( |
1094 | $subjectPage, $subjectMsg, !$isTalk, '', $userCanRead |
1095 | ); |
1096 | $associatedPages[$subjectId]['context'] = 'subject'; |
1097 | // Use the following messages if defined or talk if not: |
1098 | // * nstab-talk, nstab-user_talk, nstab-media_talk, nstab-project_talk |
1099 | // * nstab-image_talk, nstab-mediawiki_talk, nstab-template_talk |
1100 | // * nstab-help_talk, nstab-category_talk, |
1101 | // * nstab-<subject namespace key>_talk |
1102 | $associatedPages[$talkId] = $this->tabAction( |
1103 | $talkPage, [ "nstab-$talkId", "talk" ], $isTalk, '', $userCanRead |
1104 | ); |
1105 | $associatedPages[$talkId]['context'] = 'talk'; |
1106 | |
1107 | if ( $userCanRead ) { |
1108 | // Adds "view" view link |
1109 | if ( $title->isKnown() ) { |
1110 | $content_navigation['views']['view'] = $this->tabAction( |
1111 | $isTalk ? $talkPage : $subjectPage, |
1112 | 'view-view', |
1113 | ( $onPage && ( $action == 'view' || $action == 'purge' ) ), '', true |
1114 | ); |
1115 | $content_navigation['views']['view']['text'] = $this->getSkinNavOverrideableLabel( |
1116 | 'view-view' |
1117 | ); |
1118 | // signal to hide this from simple content_actions |
1119 | $content_navigation['views']['view']['redundant'] = true; |
1120 | } |
1121 | |
1122 | $page = $this->canUseWikiPage() ? $this->getWikiPage() : false; |
1123 | $isRemoteContent = $page && !$page->isLocal(); |
1124 | |
1125 | // If it is a non-local file, show a link to the file in its own repository |
1126 | // @todo abstract this for remote content that isn't a file |
1127 | if ( $isRemoteContent ) { |
1128 | $content_navigation['views']['view-foreign'] = [ |
1129 | 'class' => '', |
1130 | 'text' => $this->getSkinNavOverrideableLabel( |
1131 | 'view-foreign', $page->getWikiDisplayName() |
1132 | ), |
1133 | 'href' => $page->getSourceURL(), |
1134 | 'primary' => false, |
1135 | ]; |
1136 | } |
1137 | |
1138 | // Checks if user can edit the current page if it exists or create it otherwise |
1139 | if ( $this->getAuthority()->probablyCan( 'edit', $title ) ) { |
1140 | // Builds CSS class for talk page links |
1141 | $isTalkClass = $isTalk ? ' istalk' : ''; |
1142 | // Whether the user is editing the page |
1143 | $isEditing = $onPage && ( $action == 'edit' || $action == 'submit' ); |
1144 | $isRedirect = $page && $page->isRedirect(); |
1145 | // Whether to show the "Add a new section" tab |
1146 | // Checks if this is a current rev of talk page and is not forced to be hidden |
1147 | $showNewSection = !$out->forceHideNewSectionLink() |
1148 | && ( ( $isTalk && !$isRedirect && $out->isRevisionCurrent() ) || $out->showNewSectionLink() ); |
1149 | $section = $request->getVal( 'section' ); |
1150 | |
1151 | if ( $title->exists() |
1152 | || ( $title->inNamespace( NS_MEDIAWIKI ) |
1153 | && $title->getDefaultMessageText() !== false |
1154 | ) |
1155 | ) { |
1156 | $msgKey = $isRemoteContent ? 'edit-local' : 'edit'; |
1157 | } else { |
1158 | $msgKey = $isRemoteContent ? 'create-local' : 'create'; |
1159 | } |
1160 | $content_navigation['views']['edit'] = [ |
1161 | 'class' => ( $isEditing && ( $section !== 'new' || !$showNewSection ) |
1162 | ? 'selected' |
1163 | : '' |
1164 | ) . $isTalkClass, |
1165 | 'text' => $this->getSkinNavOverrideableLabel( |
1166 | "view-$msgKey" |
1167 | ), |
1168 | 'single-id' => "ca-$msgKey", |
1169 | 'href' => $title->getLocalURL( $this->editUrlOptions() ), |
1170 | 'primary' => !$isRemoteContent, // don't collapse this in vector |
1171 | ]; |
1172 | |
1173 | // section link |
1174 | if ( $showNewSection ) { |
1175 | // Adds new section link |
1176 | // $content_navigation['actions']['addsection'] |
1177 | $content_navigation['views']['addsection'] = [ |
1178 | 'class' => ( $isEditing && $section == 'new' ) ? 'selected' : false, |
1179 | 'text' => $this->getSkinNavOverrideableLabel( |
1180 | "action-addsection" |
1181 | ), |
1182 | 'href' => $title->getLocalURL( 'action=edit§ion=new' ) |
1183 | ]; |
1184 | } |
1185 | // Checks if the page has some kind of viewable source content |
1186 | } elseif ( $title->hasSourceText() ) { |
1187 | // Adds view source view link |
1188 | $content_navigation['views']['viewsource'] = [ |
1189 | 'class' => ( $onPage && $action == 'edit' ) ? 'selected' : false, |
1190 | 'text' => $this->getSkinNavOverrideableLabel( |
1191 | "action-viewsource" |
1192 | ), |
1193 | 'href' => $title->getLocalURL( $this->editUrlOptions() ), |
1194 | 'primary' => true, // don't collapse this in vector |
1195 | ]; |
1196 | } |
1197 | |
1198 | // Checks if the page exists |
1199 | if ( $title->exists() ) { |
1200 | // Adds history view link |
1201 | $content_navigation['views']['history'] = [ |
1202 | 'class' => ( $onPage && $action == 'history' ) ? 'selected' : false, |
1203 | 'text' => $this->getSkinNavOverrideableLabel( |
1204 | 'view-history' |
1205 | ), |
1206 | 'href' => $title->getLocalURL( 'action=history' ), |
1207 | ]; |
1208 | |
1209 | if ( $this->getAuthority()->probablyCan( 'delete', $title ) ) { |
1210 | $content_navigation['actions']['delete'] = [ |
1211 | 'icon' => 'trash', |
1212 | 'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false, |
1213 | 'text' => $this->getSkinNavOverrideableLabel( |
1214 | 'action-delete' |
1215 | ), |
1216 | 'href' => $title->getLocalURL( [ |
1217 | 'action' => 'delete', |
1218 | 'oldid' => $out->getRevisionId(), |
1219 | ] ) |
1220 | ]; |
1221 | } |
1222 | |
1223 | if ( $this->getAuthority()->probablyCan( 'move', $title ) ) { |
1224 | $moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() ); |
1225 | $content_navigation['actions']['move'] = [ |
1226 | 'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false, |
1227 | 'text' => $this->getSkinNavOverrideableLabel( |
1228 | 'action-move' |
1229 | ), |
1230 | 'icon' => 'move', |
1231 | 'href' => $moveTitle->getLocalURL() |
1232 | ]; |
1233 | } |
1234 | } else { |
1235 | // article doesn't exist or is deleted |
1236 | if ( $this->getAuthority()->probablyCan( 'deletedhistory', $title ) ) { |
1237 | $n = $title->getDeletedEditsCount(); |
1238 | if ( $n ) { |
1239 | $undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() ); |
1240 | // If the user can't undelete but can view deleted |
1241 | // history show them a "View .. deleted" tab instead. |
1242 | $msgKey = $this->getAuthority()->probablyCan( 'undelete', $title ) ? |
1243 | 'undelete' : 'viewdeleted'; |
1244 | $content_navigation['actions']['undelete'] = [ |
1245 | 'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false, |
1246 | 'text' => $this->getSkinNavOverrideableLabel( |
1247 | "action-$msgKey", $n |
1248 | ), |
1249 | 'icon' => 'trash', |
1250 | 'href' => $undelTitle->getLocalURL() |
1251 | ]; |
1252 | } |
1253 | } |
1254 | } |
1255 | |
1256 | $restrictionStore = $services->getRestrictionStore(); |
1257 | if ( $this->getAuthority()->probablyCan( 'protect', $title ) && |
1258 | $restrictionStore->listApplicableRestrictionTypes( $title ) && |
1259 | $permissionManager->getNamespaceRestrictionLevels( |
1260 | $title->getNamespace(), |
1261 | $performer->getUser() |
1262 | ) !== [ '' ] |
1263 | ) { |
1264 | $isProtected = $restrictionStore->isProtected( $title ); |
1265 | $mode = $isProtected ? 'unprotect' : 'protect'; |
1266 | $content_navigation['actions'][$mode] = [ |
1267 | 'class' => ( $onPage && $action == $mode ) ? 'selected' : false, |
1268 | 'text' => $this->getSkinNavOverrideableLabel( |
1269 | "action-$mode" |
1270 | ), |
1271 | 'icon' => $isProtected ? 'unLock' : 'lock', |
1272 | 'href' => $title->getLocalURL( "action=$mode" ) |
1273 | ]; |
1274 | } |
1275 | |
1276 | if ( $this->loggedin && $this->getAuthority() |
1277 | ->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' ) |
1278 | ) { |
1279 | /** |
1280 | * The following actions use messages which, if made particular to |
1281 | * the any specific skins, would break the Ajax code which makes this |
1282 | * action happen entirely inline. OutputPage::getJSVars |
1283 | * defines a set of messages in a javascript object - and these |
1284 | * messages are assumed to be global for all skins. Without making |
1285 | * a change to that procedure these messages will have to remain as |
1286 | * the global versions. |
1287 | */ |
1288 | $mode = MediaWikiServices::getInstance()->getWatchlistManager() |
1289 | ->isWatched( $performer, $title ) ? 'unwatch' : 'watch'; |
1290 | |
1291 | // Add the watch/unwatch link. |
1292 | $content_navigation['actions'][$mode] = $this->getWatchLinkAttrs( |
1293 | $mode, |
1294 | $performer, |
1295 | $title, |
1296 | $action, |
1297 | $onPage |
1298 | ); |
1299 | } |
1300 | } |
1301 | |
1302 | // Add language variants |
1303 | $languageConverterFactory = MediaWikiServices::getInstance()->getLanguageConverterFactory(); |
1304 | |
1305 | if ( $userCanRead && !$languageConverterFactory->isConversionDisabled() ) { |
1306 | $pageLang = $title->getPageLanguage(); |
1307 | $converter = $languageConverterFactory |
1308 | ->getLanguageConverter( $pageLang ); |
1309 | // Checks that language conversion is enabled and variants exist |
1310 | // And if it is not in the special namespace |
1311 | if ( $converter->hasVariants() ) { |
1312 | // Gets list of language variants |
1313 | $variants = $converter->getVariants(); |
1314 | // Gets preferred variant (note that user preference is |
1315 | // only possible for wiki content language variant) |
1316 | $preferred = $converter->getPreferredVariant(); |
1317 | if ( $action === 'view' ) { |
1318 | $params = $request->getQueryValues(); |
1319 | unset( $params['title'] ); |
1320 | } else { |
1321 | $params = []; |
1322 | } |
1323 | // Loops over each variant |
1324 | foreach ( $variants as $code ) { |
1325 | // Gets variant name from language code |
1326 | $varname = $pageLang->getVariantname( $code ); |
1327 | // Appends variant link |
1328 | $content_navigation['variants'][] = [ |
1329 | 'class' => ( $code == $preferred ) ? 'selected' : false, |
1330 | 'text' => $varname, |
1331 | 'href' => $title->getLocalURL( [ 'variant' => $code ] + $params ), |
1332 | 'lang' => LanguageCode::bcp47( $code ), |
1333 | 'hreflang' => LanguageCode::bcp47( $code ), |
1334 | ]; |
1335 | } |
1336 | } |
1337 | } |
1338 | $namespaces = $associatedPages; |
1339 | } else { |
1340 | // If it's not content, and a request URL is set it's got to be a special page |
1341 | try { |
1342 | $url = $request->getRequestURL(); |
1343 | } catch ( MWException $e ) { |
1344 | $url = false; |
1345 | } |
1346 | $namespaces['special'] = [ |
1347 | 'class' => 'selected', |
1348 | 'text' => $this->msg( 'nstab-special' )->text(), |
1349 | 'href' => $url, // @see: T4457, T4510 |
1350 | 'context' => 'subject' |
1351 | ]; |
1352 | $associatedPages += $this->getSpecialPageAssociatedNavigationLinks( $title ); |
1353 | } |
1354 | |
1355 | $content_navigation['namespaces'] = $namespaces; |
1356 | $content_navigation['associated-pages'] = $associatedPages; |
1357 | $this->runOnSkinTemplateNavigationHooks( $this, $content_navigation ); |
1358 | |
1359 | // Setup xml ids and tooltip info |
1360 | foreach ( $content_navigation as $section => &$links ) { |
1361 | foreach ( $links as $key => &$link ) { |
1362 | // Allow links to set their own id for backwards compatibility reasons. |
1363 | if ( isset( $link['id'] ) || isset( $link['html' ] ) ) { |
1364 | continue; |
1365 | } |
1366 | $xmlID = $key; |
1367 | if ( isset( $link['context'] ) && $link['context'] == 'subject' ) { |
1368 | $xmlID = 'ca-nstab-' . $xmlID; |
1369 | } elseif ( isset( $link['context'] ) && $link['context'] == 'talk' ) { |
1370 | $xmlID = 'ca-talk'; |
1371 | $link['rel'] = 'discussion'; |
1372 | } elseif ( $section == 'variants' ) { |
1373 | $xmlID = 'ca-varlang-' . $xmlID; |
1374 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive |
1375 | $link['class'] .= ' ca-variants-' . $link['lang']; |
1376 | } else { |
1377 | $xmlID = 'ca-' . $xmlID; |
1378 | } |
1379 | $link['id'] = $xmlID; |
1380 | } |
1381 | } |
1382 | |
1383 | # We don't want to give the watch tab an accesskey if the |
1384 | # page is being edited, because that conflicts with the |
1385 | # accesskey on the watch checkbox. We also don't want to |
1386 | # give the edit tab an accesskey, because that's fairly |
1387 | # superfluous and conflicts with an accesskey (Ctrl-E) often |
1388 | # used for editing in Safari. |
1389 | if ( in_array( $action, [ 'edit', 'submit' ] ) ) { |
1390 | if ( isset( $content_navigation['views']['edit'] ) ) { |
1391 | $content_navigation['views']['edit']['tooltiponly'] = true; |
1392 | } |
1393 | if ( isset( $content_navigation['actions']['watch'] ) ) { |
1394 | $content_navigation['actions']['watch']['tooltiponly'] = true; |
1395 | } |
1396 | if ( isset( $content_navigation['actions']['unwatch'] ) ) { |
1397 | $content_navigation['actions']['unwatch']['tooltiponly'] = true; |
1398 | } |
1399 | } |
1400 | |
1401 | $this->contentNavigationCached = $content_navigation; |
1402 | return $content_navigation; |
1403 | } |
1404 | |
1405 | /** |
1406 | * Return a list of pages that have been marked as related to/associated with |
1407 | * the special page for display. |
1408 | * |
1409 | * @param Title $title |
1410 | * @return array |
1411 | */ |
1412 | private function getSpecialPageAssociatedNavigationLinks( Title $title ): array { |
1413 | $specialAssociatedNavigationLinks = []; |
1414 | $specialFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
1415 | $special = $specialFactory->getPage( $title->getText() ); |
1416 | if ( $special === null ) { |
1417 | // not a valid special page |
1418 | return []; |
1419 | } |
1420 | $special->setContext( $this ); |
1421 | $associatedNavigationLinks = $special->getAssociatedNavigationLinks(); |
1422 | // If no sub pages we should not render. |
1423 | if ( count( $associatedNavigationLinks ) === 0 ) { |
1424 | return []; |
1425 | } |
1426 | |
1427 | foreach ( $associatedNavigationLinks as $i => $relatedTitleText ) { |
1428 | $relatedTitle = Title::newFromText( $relatedTitleText ); |
1429 | $special = $specialFactory->getPage( $relatedTitle->getText() ); |
1430 | if ( $special === null ) { |
1431 | $text = $relatedTitle->getText(); |
1432 | } else { |
1433 | $text = $special->getShortDescription( $relatedTitle->getSubpageText() ); |
1434 | } |
1435 | $specialAssociatedNavigationLinks['special-specialAssociatedNavigationLinks-link-' . $i ] = [ |
1436 | 'text' => $text, |
1437 | 'href' => $relatedTitle->getLocalURL(), |
1438 | 'class' => $relatedTitle->fixSpecialName()->equals( $title->fixSpecialName() ) ? 'selected' : '', |
1439 | ]; |
1440 | } |
1441 | return $specialAssociatedNavigationLinks; |
1442 | } |
1443 | |
1444 | /** |
1445 | * Wrapper for private buildContentNavigationUrlsInternal |
1446 | * @deprecated since 1.38 skins can use runOnSkinTemplateNavigationHooks instead. |
1447 | * @return array |
1448 | */ |
1449 | protected function buildContentNavigationUrls() { |
1450 | wfDeprecated( __METHOD__, '1.38' ); |
1451 | return $this->buildContentNavigationUrlsInternal(); |
1452 | } |
1453 | |
1454 | /** |
1455 | * an array of edit links by default used for the tabs |
1456 | * @param array $content_navigation |
1457 | * @return array |
1458 | */ |
1459 | private function buildContentActionUrls( $content_navigation ) { |
1460 | // content_actions has been replaced with content_navigation for backwards |
1461 | // compatibility and also for skins that just want simple tabs content_actions |
1462 | // is now built by flattening the content_navigation arrays into one |
1463 | |
1464 | $content_actions = []; |
1465 | |
1466 | foreach ( $content_navigation as $links ) { |
1467 | foreach ( $links as $key => $value ) { |
1468 | if ( isset( $value['redundant'] ) && $value['redundant'] ) { |
1469 | // Redundant tabs are dropped from content_actions |
1470 | continue; |
1471 | } |
1472 | |
1473 | // content_actions used to have ids built using the "ca-$key" pattern |
1474 | // so the xmlID based id is much closer to the actual $key that we want |
1475 | // for that reason we'll just strip out the ca- if present and use |
1476 | // the latter potion of the "id" as the $key |
1477 | if ( isset( $value['id'] ) && substr( $value['id'], 0, 3 ) == 'ca-' ) { |
1478 | $key = substr( $value['id'], 3 ); |
1479 | } |
1480 | |
1481 | if ( isset( $content_actions[$key] ) ) { |
1482 | wfDebug( __METHOD__ . ": Found a duplicate key for $key while flattening " . |
1483 | "content_navigation into content_actions." ); |
1484 | continue; |
1485 | } |
1486 | |
1487 | $content_actions[$key] = $value; |
1488 | } |
1489 | } |
1490 | |
1491 | return $content_actions; |
1492 | } |
1493 | |
1494 | /** |
1495 | * Insert legacy menu items from content navigation into the personal toolbar. |
1496 | * |
1497 | * @internal |
1498 | * |
1499 | * @param array $contentNavigation |
1500 | * @return array |
1501 | */ |
1502 | final protected function injectLegacyMenusIntoPersonalTools( |
1503 | array $contentNavigation |
1504 | ): array { |
1505 | $userMenu = $contentNavigation['user-menu'] ?? []; |
1506 | // userpage is only defined for logged-in users, and wfArrayInsertAfter requires the |
1507 | // $after parameter to be a known key in the array. |
1508 | if ( isset( $contentNavigation['user-menu']['userpage'] ) && isset( $contentNavigation['notifications'] ) ) { |
1509 | $userMenu = wfArrayInsertAfter( |
1510 | $userMenu, |
1511 | $contentNavigation['notifications'], |
1512 | 'userpage' |
1513 | ); |
1514 | } |
1515 | if ( isset( $contentNavigation['user-interface-preferences'] ) ) { |
1516 | return array_merge( |
1517 | $contentNavigation['user-interface-preferences'], |
1518 | $userMenu |
1519 | ); |
1520 | } |
1521 | return $userMenu; |
1522 | } |
1523 | |
1524 | /** |
1525 | * Build the personal urls array. |
1526 | * |
1527 | * @internal |
1528 | * |
1529 | * @param array $contentNavigation |
1530 | * @return array |
1531 | */ |
1532 | private function makeSkinTemplatePersonalUrls( |
1533 | array $contentNavigation |
1534 | ): array { |
1535 | if ( isset( $contentNavigation['user-menu'] ) ) { |
1536 | return $this->injectLegacyMenusIntoPersonalTools( $contentNavigation ); |
1537 | } |
1538 | return []; |
1539 | } |
1540 | |
1541 | /** |
1542 | * @since 1.35 |
1543 | * @param array $attrs (optional) will be passed to tooltipAndAccesskeyAttribs |
1544 | * and decorate the resulting input |
1545 | * @return string of HTML input |
1546 | */ |
1547 | public function makeSearchInput( $attrs = [] ) { |
1548 | // It's possible that getTemplateData might be calling |
1549 | // Skin::makeSearchInput. To avoid infinite recursion create a |
1550 | // new instance of the search component here. |
1551 | $searchBox = $this->getComponent( 'search-box' ); |
1552 | $data = $searchBox->getTemplateData(); |
1553 | |
1554 | return Html::element( 'input', |
1555 | $data[ 'array-input-attributes' ] + $attrs |
1556 | ); |
1557 | } |
1558 | |
1559 | /** |
1560 | * @since 1.35 |
1561 | * @param string $mode representing the type of button wanted |
1562 | * either `go`, `fulltext` or `image` |
1563 | * @param array $attrs (optional) |
1564 | * @return string of HTML button |
1565 | */ |
1566 | final public function makeSearchButton( $mode, $attrs = [] ) { |
1567 | // It's possible that getTemplateData might be calling |
1568 | // Skin::makeSearchInput. To avoid infinite recursion create a |
1569 | // new instance of the search component here. |
1570 | $searchBox = $this->getComponent( 'search-box' ); |
1571 | $data = $searchBox->getTemplateData(); |
1572 | |
1573 | return self::makeSearchButtonInternal( |
1574 | $mode, |
1575 | $data, |
1576 | $attrs |
1577 | ); |
1578 | } |
1579 | |
1580 | /** |
1581 | * @deprecated 1.38 see @internal note. |
1582 | * @param string $mode representing the type of button wanted |
1583 | * either `go`, `fulltext` or `image` |
1584 | * @param array $searchData Skin data returned by Skin::getTemplateData()['data-search-box'] |
1585 | * @param array $attrs (optional) |
1586 | * @internal Please use SkinTemplate::makeSearchButton. |
1587 | * For usage only inside Skin class to support deprecated Skin::makeSearchButton method. |
1588 | * This should be merged with SkinTemplate::makeSearchButton when |
1589 | * Skin::makeSearchButton method is removed. |
1590 | * @return string of HTML button |
1591 | */ |
1592 | public static function makeSearchButtonInternal( $mode, $searchData, $attrs = [] ) { |
1593 | switch ( $mode ) { |
1594 | case 'go': |
1595 | $attrs['value'] ??= wfMessage( 'searcharticle' )->text(); |
1596 | return Html::element( |
1597 | 'input', |
1598 | array_merge( |
1599 | $searchData[ 'array-button-go-attributes' ], $attrs |
1600 | ) |
1601 | ); |
1602 | case 'fulltext': |
1603 | $attrs['value'] ??= wfMessage( 'searchbutton' )->text(); |
1604 | return Html::element( |
1605 | 'input', |
1606 | array_merge( |
1607 | $searchData[ 'array-button-fulltext-attributes' ], $attrs |
1608 | ) |
1609 | ); |
1610 | case 'image': |
1611 | $buttonAttrs = [ |
1612 | 'type' => 'submit', |
1613 | 'name' => 'button', |
1614 | ]; |
1615 | $buttonAttrs = array_merge( |
1616 | $buttonAttrs, |
1617 | Linker::tooltipAndAccesskeyAttribs( 'search-fulltext' ), |
1618 | $attrs |
1619 | ); |
1620 | unset( $buttonAttrs['src'] ); |
1621 | unset( $buttonAttrs['alt'] ); |
1622 | unset( $buttonAttrs['width'] ); |
1623 | unset( $buttonAttrs['height'] ); |
1624 | $imgAttrs = [ |
1625 | 'src' => $attrs['src'], |
1626 | 'alt' => $attrs['alt'] ?? wfMessage( 'searchbutton' )->text(), |
1627 | 'width' => $attrs['width'] ?? null, |
1628 | 'height' => $attrs['height'] ?? null, |
1629 | ]; |
1630 | return Html::rawElement( 'button', $buttonAttrs, Html::element( 'img', $imgAttrs ) ); |
1631 | default: |
1632 | throw new InvalidArgumentException( 'Unknown mode passed to ' . __METHOD__ ); |
1633 | } |
1634 | } |
1635 | |
1636 | /** |
1637 | * @return bool |
1638 | */ |
1639 | private function isSpecialContributeShowable(): bool { |
1640 | return ContributeFactory::isEnabledOnCurrentSkin( |
1641 | $this, |
1642 | $this->getConfig()->get( 'SpecialContributeSkinsEnabled' ) |
1643 | ); |
1644 | } |
1645 | |
1646 | /** |
1647 | * @param array &$personal_urls |
1648 | * @param string $key |
1649 | * @param string|null $userName |
1650 | * @param bool $active |
1651 | * |
1652 | * @return array |
1653 | */ |
1654 | private function makeContributionsLink( |
1655 | array &$personal_urls, |
1656 | string $key, |
1657 | ?string $userName = null, |
1658 | bool $active = false |
1659 | ): array { |
1660 | $isSpecialContributeShowable = $this->isSpecialContributeShowable(); |
1661 | $subpage = $userName ?? false; |
1662 | if ( $isSpecialContributeShowable ) { |
1663 | $href = SkinComponentUtils::makeSpecialUrlSubpage( |
1664 | 'Contribute', |
1665 | false |
1666 | ); |
1667 | $personal_urls['contribute'] = [ |
1668 | 'text' => $this->msg( 'contribute' )->text(), |
1669 | 'href' => $href, |
1670 | 'active' => $href == $this->getTitle()->getLocalURL(), |
1671 | 'icon' => 'edit' |
1672 | ]; |
1673 | } else { |
1674 | $href = SkinComponentUtils::makeSpecialUrlSubpage( |
1675 | $subpage !== false ? 'Contributions' : 'Mycontributions', |
1676 | $subpage |
1677 | ); |
1678 | $personal_urls[$key] = [ |
1679 | 'text' => $this->msg( $key )->text(), |
1680 | 'href' => $href, |
1681 | 'active' => $active, |
1682 | 'icon' => 'userContributions' |
1683 | ]; |
1684 | } |
1685 | return $personal_urls; |
1686 | } |
1687 | |
1688 | } |