Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
14.46% |
73 / 505 |
|
10.26% |
4 / 39 |
CRAP | |
0.00% |
0 / 1 |
SkinMinerva | |
14.46% |
73 / 505 |
|
10.26% |
4 / 39 |
14807.13 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
hasPageActions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
hasSecondaryActions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isFallbackEditor | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getPageActions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getNotificationFallbackButton | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getCombinedNotificationButton | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
getNotificationCircleButton | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getNotificationButton | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
runOnSkinTemplateNavigationHooks | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
getTemplateData | |
0.00% |
0 / 80 |
|
0.00% |
0 / 1 |
72 | |||
getNotificationButtons | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
7 | |||
isHistoryPage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasPageTabs | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
getTabsData | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getMainMenu | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getPersonalToolsMenu | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
getSubjectPage | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
doEditSectionLinksHTML | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getPageClasses | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
resolveNightModeQueryValue | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
56 | |||
getHtmlElementAttributes | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
6.04 | |||
hasCategoryLinks | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getRelativeHistoryLink | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getGenericHistoryLink | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
shouldUseSpecialHistory | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getHistoryUrl | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getHistoryLink | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
72 | |||
getRevisionEditorData | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getTaglineHtml | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getUserPageHeadingHtml | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
prepareBanners | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getLanguageButton | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getTalkButton | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 | |||
getSecondaryActions | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
156 | |||
getJsConfigVars | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultModules | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
getPageSpecificStyles | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
210 | |||
getFeatureSpecificStyles | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Minerva\Skins; |
22 | |
23 | use ExtensionRegistry; |
24 | use Language; |
25 | use MediaWiki\Cache\GenderCache; |
26 | use MediaWiki\Extension\Notifications\Controller\NotificationController; |
27 | use MediaWiki\Html\Html; |
28 | use MediaWiki\Linker\LinkRenderer; |
29 | use MediaWiki\Linker\LinkTarget; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Minerva\LanguagesHelper; |
32 | use MediaWiki\Minerva\Menu\Definitions; |
33 | use MediaWiki\Minerva\Menu\Main\AdvancedMainMenuBuilder; |
34 | use MediaWiki\Minerva\Menu\Main\DefaultMainMenuBuilder; |
35 | use MediaWiki\Minerva\Menu\Main\MainMenuDirector; |
36 | use MediaWiki\Minerva\Menu\PageActions\PageActionsDirector; |
37 | use MediaWiki\Minerva\Menu\User\AdvancedUserMenuBuilder; |
38 | use MediaWiki\Minerva\Menu\User\DefaultUserMenuBuilder; |
39 | use MediaWiki\Minerva\Menu\User\UserMenuDirector; |
40 | use MediaWiki\Minerva\Permissions\IMinervaPagePermissions; |
41 | use MediaWiki\Minerva\Permissions\MinervaPagePermissions; |
42 | use MediaWiki\Minerva\SkinOptions; |
43 | use MediaWiki\Revision\RevisionLookup; |
44 | use MediaWiki\SpecialPage\SpecialPage; |
45 | use MediaWiki\Title\NamespaceInfo; |
46 | use MediaWiki\Title\Title; |
47 | use MediaWiki\User\Options\UserOptionsManager; |
48 | use MediaWiki\User\UserIdentityUtils; |
49 | use MediaWiki\Utils\MWTimestamp; |
50 | use RuntimeException; |
51 | use SkinMustache; |
52 | use SkinTemplate; |
53 | use SpecialMobileHistory; |
54 | |
55 | /** |
56 | * Minerva: Born from the godhead of Jupiter with weapons! |
57 | * A skin that works on both desktop and mobile |
58 | * @ingroup Skins |
59 | */ |
60 | class SkinMinerva extends SkinMustache { |
61 | /** @const LEAD_SECTION_NUMBER integer which corresponds to the lead section |
62 | * in editing mode |
63 | */ |
64 | public const LEAD_SECTION_NUMBER = 0; |
65 | |
66 | /** @var string Name of this skin */ |
67 | public $skinname = 'minerva'; |
68 | /** @var string Name of this used template */ |
69 | public $template = 'MinervaTemplate'; |
70 | |
71 | /** @var array|null */ |
72 | private $contentNavigationUrls; |
73 | private GenderCache $genderCache; |
74 | private LinkRenderer $linkRenderer; |
75 | private LanguagesHelper $languagesHelper; |
76 | private Definitions $definitions; |
77 | private IMinervaPagePermissions $permissions; |
78 | private SkinOptions $skinOptions; |
79 | private SkinUserPageHelper $skinUserPageHelper; |
80 | private NamespaceInfo $namespaceInfo; |
81 | private RevisionLookup $revisionLookup; |
82 | private UserIdentityUtils $userIdentityUtils; |
83 | private UserOptionsManager $userOptionsManager; |
84 | |
85 | /** |
86 | * @param GenderCache $genderCache |
87 | * @param LinkRenderer $linkRenderer |
88 | * @param LanguagesHelper $languagesHelper |
89 | * @param Definitions $definitions |
90 | * @param MinervaPagePermissions $permissions |
91 | * @param SkinOptions $skinOptions |
92 | * @param SkinUserPageHelper $skinUserPageHelper |
93 | * @param NamespaceInfo $namespaceInfo |
94 | * @param RevisionLookup $revisionLookup |
95 | * @param UserIdentityUtils $userIdentityUtils |
96 | * @param UserOptionsManager $userOptionsManager |
97 | * @param array $options |
98 | */ |
99 | public function __construct( |
100 | GenderCache $genderCache, |
101 | LinkRenderer $linkRenderer, |
102 | LanguagesHelper $languagesHelper, |
103 | Definitions $definitions, |
104 | MinervaPagePermissions $permissions, |
105 | SkinOptions $skinOptions, |
106 | SkinUserPageHelper $skinUserPageHelper, |
107 | NamespaceInfo $namespaceInfo, |
108 | RevisionLookup $revisionLookup, |
109 | UserIdentityUtils $userIdentityUtils, |
110 | UserOptionsManager $userOptionsManager, |
111 | $options = [] |
112 | ) { |
113 | parent::__construct( $options ); |
114 | $this->genderCache = $genderCache; |
115 | $this->linkRenderer = $linkRenderer; |
116 | $this->languagesHelper = $languagesHelper; |
117 | $this->definitions = $definitions |
118 | ->setContext( $this->getContext() ); |
119 | $this->permissions = $permissions |
120 | ->setContext( $this->getContext() ); |
121 | $this->skinOptions = $skinOptions; |
122 | $this->skinUserPageHelper = $skinUserPageHelper |
123 | ->setContext( $this->getContext() ) |
124 | ->setTitle( $this->getTitle() ); |
125 | $this->namespaceInfo = $namespaceInfo; |
126 | $this->revisionLookup = $revisionLookup; |
127 | $this->userIdentityUtils = $userIdentityUtils; |
128 | $this->userOptionsManager = $userOptionsManager; |
129 | } |
130 | |
131 | /** |
132 | * @return bool |
133 | */ |
134 | private function hasPageActions(): bool { |
135 | $title = $this->getTitle(); |
136 | return !$title->isSpecialPage() && !$title->isMainPage() && |
137 | $this->getContext()->getActionName() === 'view'; |
138 | } |
139 | |
140 | /** |
141 | * @return bool |
142 | */ |
143 | private function hasSecondaryActions(): bool { |
144 | return !$this->skinUserPageHelper->isUserPage(); |
145 | } |
146 | |
147 | /** |
148 | * @return bool |
149 | */ |
150 | private function isFallbackEditor(): bool { |
151 | $action = $this->getContext()->getActionName(); |
152 | return $action === 'edit'; |
153 | } |
154 | |
155 | /** |
156 | * Returns available page actions if the page has any. |
157 | * |
158 | * @param array $nav result of SkinTemplate::buildContentNavigationUrls |
159 | * @return array|null |
160 | */ |
161 | private function getPageActions( array $nav ): ?array { |
162 | if ( $this->isFallbackEditor() || !$this->hasPageActions() ) { |
163 | return null; |
164 | } |
165 | $services = MediaWikiServices::getInstance(); |
166 | /** @var PageActionsDirector $pageActionsDirector */ |
167 | $pageActionsDirector = $services->getService( 'Minerva.Menu.PageActionsDirector' ); |
168 | $sidebar = $this->buildSidebar(); |
169 | $actions = $nav['actions'] ?? []; |
170 | $views = $nav['views'] ?? []; |
171 | return $pageActionsDirector->buildMenu( $sidebar['TOOLBOX'], $actions, $views ); |
172 | } |
173 | |
174 | /** |
175 | * A notification icon that links to Special:Mytalk when Echo is not installed. |
176 | * Consider upstreaming this to core or removing at a future date. |
177 | * |
178 | * @return array |
179 | */ |
180 | private function getNotificationFallbackButton(): array { |
181 | return [ |
182 | 'icon' => 'bellOutline-base20', |
183 | 'href' => SpecialPage::getTitleFor( 'Mytalk' )->getLocalURL( |
184 | [ 'returnto' => $this->getTitle()->getPrefixedText() ] |
185 | ), |
186 | ]; |
187 | } |
188 | |
189 | /** |
190 | * @param array $alert |
191 | * @param array $notice |
192 | * @return array |
193 | */ |
194 | private function getCombinedNotificationButton( array $alert, array $notice ): array { |
195 | // Sum the notifications from the two original buttons |
196 | $notifCount = ( $alert['data']['counter-num'] ?? 0 ) + ( $notice['data']['counter-num'] ?? 0 ); |
197 | $alert['data']['counter-num'] = $notifCount; |
198 | // @phan-suppress-next-line PhanUndeclaredClassReference |
199 | if ( class_exists( NotificationController::class ) ) { |
200 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
201 | $alert['data']['counter-text'] = NotificationController::formatNotificationCount( $notifCount ); |
202 | } else { |
203 | $alert['data']['counter-text'] = $notifCount; |
204 | } |
205 | |
206 | $linkClassAlert = $alert['link-class'] ?? []; |
207 | $hasUnseenAlerts = is_array( $linkClassAlert ) && in_array( 'mw-echo-unseen-notifications', $linkClassAlert ); |
208 | // The circle should only appear if there are unseen notifications. |
209 | // Once the notifications are seen (by opening the notification drawer) |
210 | // then the icon reverts to a gray circle, but on page refresh |
211 | // it should revert back to a bell icon. |
212 | // If you try and change this behaviour, at time of writing |
213 | // (December 2022) JavaScript will correct it. |
214 | if ( $notifCount > 0 && $hasUnseenAlerts ) { |
215 | $linkClass = $notice['link-class'] ?? []; |
216 | $hasUnseenNotices = is_array( $linkClass ) && in_array( 'mw-echo-unseen-notifications', $linkClass ); |
217 | return $this->getNotificationCircleButton( $alert, $hasUnseenNotices ); |
218 | } else { |
219 | return $this->getNotificationButton( $alert ); |
220 | } |
221 | } |
222 | |
223 | /** |
224 | * Minerva differs from other skins in that for users with unread notifications |
225 | * instead of a bell with a small square indicating the number of notifications |
226 | * it shows a red circle with a number inside. Ideally Vector and Minerva would |
227 | * be treated the same but we'd need to talk to a designer about consolidating these |
228 | * before making such a decision. |
229 | * |
230 | * @param array $alert |
231 | * @param bool $hasUnseenNotices does the user have unseen notices? |
232 | * @return array |
233 | */ |
234 | private function getNotificationCircleButton( array $alert, bool $hasUnseenNotices ): array { |
235 | $alertCount = $alert['data']['counter-num'] ?? 0; |
236 | $linkClass = $alert['link-class'] ?? []; |
237 | $hasSeenAlerts = is_array( $linkClass ) && in_array( 'mw-echo-unseen-notifications', $linkClass ); |
238 | $alertText = $alert['data']['counter-text'] ?? $alertCount; |
239 | $alert['icon'] = 'circle'; |
240 | $alert['class'] = 'notification-count'; |
241 | if ( $hasSeenAlerts || $hasUnseenNotices ) { |
242 | $alert['class'] .= ' notification-unseen mw-echo-unseen-notifications'; |
243 | } |
244 | return $alert; |
245 | } |
246 | |
247 | /** |
248 | * Removes the OOUI icon class and adds Minerva notification classes. |
249 | * |
250 | * @param array $alert |
251 | * @return array |
252 | */ |
253 | private function getNotificationButton( array $alert ): array { |
254 | $linkClass = $alert['link-class']; |
255 | $alert['link-class'] = array_filter( |
256 | $linkClass, |
257 | static function ( $class ) { |
258 | return $class !== 'oo-ui-icon-bellOutline'; |
259 | } |
260 | ); |
261 | $alert['icon'] = 'bellOutline-base20'; |
262 | return $alert; |
263 | } |
264 | |
265 | /** |
266 | * Caches content navigation urls locally for use inside getTemplateData |
267 | * |
268 | * @inheritDoc |
269 | */ |
270 | protected function runOnSkinTemplateNavigationHooks( SkinTemplate $skin, &$contentNavigationUrls ) { |
271 | parent::runOnSkinTemplateNavigationHooks( $skin, $contentNavigationUrls ); |
272 | // There are some SkinTemplate modifications that occur after the execution of this hook |
273 | // to add rel attributes and ID attributes. |
274 | // The only one Minerva needs is this one so we manually add it. |
275 | foreach ( array_keys( $contentNavigationUrls['associated-pages'] ) as $id ) { |
276 | if ( in_array( $id, [ 'user_talk', 'talk' ] ) ) { |
277 | $contentNavigationUrls['associated-pages'][ $id ]['rel'] = 'discussion'; |
278 | } |
279 | } |
280 | $this->contentNavigationUrls = $contentNavigationUrls; |
281 | |
282 | // |
283 | // Echo Technical debt!! |
284 | // * Convert the Echo button into a single button |
285 | // * Switch out the icon. |
286 | // |
287 | if ( $this->getUser()->isRegistered() ) { |
288 | if ( count( $contentNavigationUrls['notifications'] ) === 0 ) { |
289 | // Shown to logged in users when Echo is not installed: |
290 | $contentNavigationUrls['notifications']['mytalks'] = $this->getNotificationFallbackButton(); |
291 | } elseif ( $this->skinOptions->get( SkinOptions::SINGLE_ECHO_BUTTON ) ) { |
292 | // Combine notification icons. Minerva only shows one entry point to notifications. |
293 | // This can be reconsidered with a solution to https://phabricator.wikimedia.org/T142981 |
294 | $alert = $contentNavigationUrls['notifications']['notifications-alert'] ?? null; |
295 | $notice = $contentNavigationUrls['notifications']['notifications-notice'] ?? null; |
296 | if ( $alert && $notice ) { |
297 | unset( $contentNavigationUrls['notifications']['notifications-notice'] ); |
298 | $contentNavigationUrls['notifications']['notifications-alert'] = |
299 | $this->getCombinedNotificationButton( $alert, $notice ); |
300 | } |
301 | } else { |
302 | // Show desktop alert icon. |
303 | $alert = $contentNavigationUrls['notifications']['notifications-alert'] ?? null; |
304 | if ( $alert ) { |
305 | // Correct the icon to be the bell filled rather than the outline to match |
306 | // Echo's badge. |
307 | $linkClass = $alert['link-class'] ?? []; |
308 | $alert['link-class'] = array_filter( $linkClass, static function ( $class ) { |
309 | return $class !== 'oo-ui-icon-bellOutline'; |
310 | } ); |
311 | $contentNavigationUrls['notifications']['notifications-alert'] = $alert; |
312 | } |
313 | } |
314 | } |
315 | } |
316 | |
317 | /** |
318 | * @inheritDoc |
319 | */ |
320 | public function getTemplateData(): array { |
321 | $data = parent::getTemplateData(); |
322 | // FIXME: Can we use $data instead of calling buildContentNavigationUrls ? |
323 | $nav = $this->contentNavigationUrls; |
324 | if ( $nav === null ) { |
325 | throw new RuntimeException( 'contentNavigationUrls was not set as expected.' ); |
326 | } |
327 | if ( !$this->hasCategoryLinks() ) { |
328 | unset( $data['html-categories'] ); |
329 | } |
330 | |
331 | // Special handling for certain pages. |
332 | // This is technical debt that should be upstreamed to core. |
333 | $isUserPage = $this->skinUserPageHelper->isUserPage(); |
334 | $isUserPageAccessible = $this->skinUserPageHelper->isUserPageAccessibleToCurrentUser(); |
335 | if ( $isUserPage && $isUserPageAccessible ) { |
336 | $data['html-title-heading'] = $this->getUserPageHeadingHtml( $data['html-title-heading' ] ); |
337 | } |
338 | |
339 | $usermessage = $data['html-user-message'] ?? ''; |
340 | if ( $usermessage ) { |
341 | $data['html-user-message'] = Html::warningBox( |
342 | '<span class="minerva-icon minerva-icon--userTalk-warning"></span> ' |
343 | . $usermessage, |
344 | 'minerva-anon-talk-message' |
345 | ); |
346 | } |
347 | $allLanguages = $data['data-portlets']['data-languages']['array-items'] ?? []; |
348 | $allVariants = $data['data-portlets']['data-variants']['array-items'] ?? []; |
349 | $notifications = $data['data-portlets']['data-notifications']['array-items'] ?? []; |
350 | $associatedPages = $data['data-portlets']['data-associated-pages'] ?? []; |
351 | |
352 | return $data + [ |
353 | 'has-minerva-languages' => $allLanguages || $allVariants, |
354 | 'array-minerva-banners' => $this->prepareBanners( $data['html-site-notice'] ), |
355 | 'data-minerva-search-box' => $data['data-search-box'] + [ |
356 | 'data-btn' => [ |
357 | 'data-icon' => [ |
358 | 'icon' => 'search-base20', |
359 | ], |
360 | 'label' => $this->msg( 'searchbutton' )->escaped(), |
361 | 'classes' => 'skin-minerva-search-trigger', |
362 | 'array-attributes' => [ |
363 | [ |
364 | 'key' => 'id', |
365 | 'value' => 'searchIcon', |
366 | ] |
367 | ] |
368 | ], |
369 | ], |
370 | 'data-minerva-main-menu-btn' => [ |
371 | 'data-icon' => [ |
372 | 'icon' => 'menu-base20', |
373 | ], |
374 | 'tag-name' => 'label', |
375 | 'classes' => 'toggle-list__toggle', |
376 | 'array-attributes' => [ |
377 | [ |
378 | 'key' => 'for', |
379 | 'value' => 'main-menu-input', |
380 | ], |
381 | [ |
382 | 'key' => 'id', |
383 | 'value' => 'mw-mf-main-menu-button', |
384 | ], |
385 | [ |
386 | 'key' => 'aria-hidden', |
387 | 'value' => 'true', |
388 | ], |
389 | [ |
390 | 'key' => 'data-event-name', |
391 | 'value' => 'ui.mainmenu', |
392 | ], |
393 | ], |
394 | 'text' => $this->msg( 'mobile-frontend-main-menu-button-tooltip' )->escaped(), |
395 | ], |
396 | 'data-minerva-main-menu' => $this->getMainMenu()->getMenuData( |
397 | $nav, |
398 | $this->buildSidebar() |
399 | )['items'], |
400 | 'html-minerva-tagline' => $this->getTaglineHtml(), |
401 | 'html-minerva-user-menu' => $this->getPersonalToolsMenu( $nav['user-menu'] ), |
402 | 'is-minerva-beta' => $this->skinOptions->get( SkinOptions::BETA_MODE ), |
403 | 'data-minerva-notifications' => $notifications ? [ |
404 | 'array-buttons' => $this->getNotificationButtons( $notifications ), |
405 | ] : null, |
406 | 'data-minerva-tabs' => $this->getTabsData( $nav, $associatedPages ), |
407 | 'data-minerva-page-actions' => $this->getPageActions( $nav ), |
408 | 'data-minerva-secondary-actions' => $this->getSecondaryActions( $nav ), |
409 | 'html-minerva-subject-link' => $this->getSubjectPage(), |
410 | 'data-minerva-history-link' => $this->getHistoryLink( $this->getTitle() ), |
411 | ]; |
412 | } |
413 | |
414 | /** |
415 | * Prepares the notification badges for the Button template. |
416 | * |
417 | * @internal |
418 | * @param array $notifications |
419 | * @return array |
420 | */ |
421 | public static function getNotificationButtons( array $notifications ): array { |
422 | $btns = []; |
423 | |
424 | foreach ( $notifications as $notification ) { |
425 | $linkData = $notification['array-links'][ 0 ] ?? []; |
426 | $icon = $linkData['icon'] ?? null; |
427 | if ( !$icon ) { |
428 | continue; |
429 | } |
430 | $id = $notification['id'] ?? null; |
431 | $classes = ''; |
432 | $attributes = []; |
433 | |
434 | // We don't want to output multiple attributes. |
435 | // Iterate through the attributes and pull out ID and class which |
436 | // will be defined separately. |
437 | foreach ( $linkData[ 'array-attributes' ] as $keyValuePair ) { |
438 | if ( $keyValuePair['key'] === 'class' ) { |
439 | $classes = $keyValuePair['value']; |
440 | } elseif ( $keyValuePair['key'] === 'id' ) { |
441 | // ignore. We want to use the LI `id` instead. |
442 | } else { |
443 | $attributes[] = $keyValuePair; |
444 | } |
445 | } |
446 | // add LI ID to end for use on the button. |
447 | if ( $id ) { |
448 | $attributes[] = [ |
449 | 'key' => 'id', |
450 | 'value' => $id, |
451 | ]; |
452 | } |
453 | $btns[] = [ |
454 | 'tag-name' => 'a', |
455 | // FIXME: Move preg_replace when Echo no longer provides this class. |
456 | 'classes' => preg_replace( '/oo-ui-icon-(bellOutline|tray)/', '', $classes ), |
457 | 'array-attributes' => $attributes, |
458 | 'data-icon' => [ |
459 | 'icon' => $icon, |
460 | ], |
461 | 'label' => $linkData['text'] ?? '', |
462 | ]; |
463 | } |
464 | return $btns; |
465 | } |
466 | |
467 | /** |
468 | * @return bool |
469 | */ |
470 | private function isHistoryPage(): bool { |
471 | return $this->getRequest()->getRawVal( 'action' ) === 'history'; |
472 | } |
473 | |
474 | /** |
475 | * Tabs are available if a page has page actions but is not the talk page of |
476 | * the main page. |
477 | * |
478 | * Special pages have tabs if SkinOptions::TABS_ON_SPECIALS is enabled. |
479 | * This is used by Extension:GrowthExperiments |
480 | * |
481 | * @return bool |
482 | */ |
483 | private function hasPageTabs(): bool { |
484 | $title = $this->getTitle(); |
485 | $isSpecialPageOrHistory = $title->isSpecialPage() || |
486 | $this->isHistoryPage(); |
487 | $subjectPage = $this->namespaceInfo->getSubjectPage( $title ); |
488 | $isMainPageTalk = Title::newFromLinkTarget( $subjectPage )->isMainPage(); |
489 | return ( |
490 | $this->hasPageActions() && !$isMainPageTalk && |
491 | $this->skinOptions->get( SkinOptions::TALK_AT_TOP ) |
492 | ) || ( |
493 | $isSpecialPageOrHistory && |
494 | $this->skinOptions->get( SkinOptions::TABS_ON_SPECIALS ) |
495 | ); |
496 | } |
497 | |
498 | /** |
499 | * @param array $contentNavigationUrls |
500 | * @param array $associatedPages - data-associated-pages from template data, currently only used for ID |
501 | * @return array |
502 | */ |
503 | private function getTabsData( array $contentNavigationUrls, array $associatedPages ): array { |
504 | $hasPageTabs = $this->hasPageTabs(); |
505 | if ( !$hasPageTabs ) { |
506 | return []; |
507 | } |
508 | return $contentNavigationUrls ? [ |
509 | 'items' => array_values( $contentNavigationUrls['associated-pages'] ), |
510 | 'id' => $associatedPages['id'] ?? null, |
511 | ] : []; |
512 | } |
513 | |
514 | /** |
515 | * Build the Main Menu Director by passing the skin options |
516 | * |
517 | * @return MainMenuDirector |
518 | */ |
519 | protected function getMainMenu(): MainMenuDirector { |
520 | $showMobileOptions = $this->skinOptions->get( SkinOptions::MOBILE_OPTIONS ); |
521 | // Add a donate link (see https://phabricator.wikimedia.org/T219793) |
522 | $showDonateLink = $this->skinOptions->get( SkinOptions::SHOW_DONATE ); |
523 | $builder = $this->skinOptions->get( SkinOptions::MAIN_MENU_EXPANDED ) ? |
524 | new AdvancedMainMenuBuilder( |
525 | $showMobileOptions, |
526 | $showDonateLink, |
527 | $this->definitions |
528 | ) : |
529 | new DefaultMainMenuBuilder( |
530 | $showMobileOptions, |
531 | $showDonateLink, |
532 | $this->getUser(), |
533 | $this->definitions, |
534 | $this->userIdentityUtils |
535 | ); |
536 | return new MainMenuDirector( $builder ); |
537 | } |
538 | |
539 | /** |
540 | * Prepare all Minerva menus |
541 | * |
542 | * @param array $personalUrls result of SkinTemplate::buildPersonalUrls |
543 | * @return string|null |
544 | */ |
545 | private function getPersonalToolsMenu( array $personalUrls ): ?string { |
546 | $builder = $this->skinOptions->get( SkinOptions::PERSONAL_MENU ) ? |
547 | new AdvancedUserMenuBuilder( |
548 | $this->getContext(), |
549 | $this->getUser(), |
550 | $this->definitions |
551 | ) : |
552 | new DefaultUserMenuBuilder(); |
553 | |
554 | $userMenuDirector = new UserMenuDirector( |
555 | $builder, |
556 | $this->getContext()->getSkin() |
557 | ); |
558 | return $userMenuDirector->renderMenuData( $personalUrls ); |
559 | } |
560 | |
561 | /** |
562 | * @return string |
563 | */ |
564 | protected function getSubjectPage(): string { |
565 | $title = $this->getTitle(); |
566 | |
567 | // If it's a talk page, add a link to the main namespace page |
568 | // In AMC we do not need to do this as there is an easy way back to the article page |
569 | // via the talk/article tabs. |
570 | if ( $title->isTalkPage() && !$this->skinOptions->get( SkinOptions::TALK_AT_TOP ) ) { |
571 | // if it's a talk page for which we have a special message, use it |
572 | switch ( $title->getNamespace() ) { |
573 | case NS_USER_TALK: |
574 | $msg = 'mobile-frontend-talk-back-to-userpage'; |
575 | break; |
576 | case NS_PROJECT_TALK: |
577 | $msg = 'mobile-frontend-talk-back-to-projectpage'; |
578 | break; |
579 | case NS_FILE_TALK: |
580 | $msg = 'mobile-frontend-talk-back-to-filepage'; |
581 | break; |
582 | default: |
583 | // generic (all other NS) |
584 | $msg = 'mobile-frontend-talk-back-to-page'; |
585 | } |
586 | $subjectPage = $this->namespaceInfo->getSubjectPage( $title ); |
587 | |
588 | return $this->linkRenderer->makeLink( |
589 | $subjectPage, |
590 | $this->msg( $msg, $title->getText() )->text(), |
591 | [ |
592 | 'data-event-name' => 'talk.returnto', |
593 | 'class' => 'return-link' |
594 | ] |
595 | ); |
596 | } else { |
597 | return ''; |
598 | } |
599 | } |
600 | |
601 | /** |
602 | * Modifies the template data before parsing in SkinMustache. |
603 | * |
604 | * @inheritDoc |
605 | */ |
606 | final protected function doEditSectionLinksHTML( array $links, Language $lang ): string { |
607 | $transformedLinks = []; |
608 | foreach ( $links as $key => $link ) { |
609 | $transformedLinks[] = $link + [ |
610 | 'data-icon' => [ |
611 | 'icon' => $link['icon'], |
612 | ], |
613 | ]; |
614 | } |
615 | return parent::doEditSectionLinksHTML( $transformedLinks, $lang ); |
616 | } |
617 | |
618 | /** |
619 | * Takes a title and returns classes to apply to the body tag |
620 | * @param Title $title |
621 | * @return string |
622 | */ |
623 | public function getPageClasses( $title ): string { |
624 | $className = parent::getPageClasses( $title ); |
625 | $className .= ' ' . ( $this->skinOptions->get( SkinOptions::BETA_MODE ) |
626 | ? 'beta' : 'stable' ); |
627 | |
628 | if ( $this->getUser()->isRegistered() ) { |
629 | $className .= ' is-authenticated'; |
630 | } |
631 | |
632 | // The new treatment should only apply to the main namespace |
633 | if ( |
634 | $title->getNamespace() === NS_MAIN && |
635 | $this->skinOptions->get( SkinOptions::PAGE_ISSUES ) |
636 | ) { |
637 | $className .= ' issues-group-B'; |
638 | } |
639 | |
640 | return $className; |
641 | } |
642 | |
643 | /** |
644 | * Converts "1", "2", and "0" to equivalent values. |
645 | * |
646 | * @return string |
647 | */ |
648 | private static function resolveNightModeQueryValue( string $value ): string { |
649 | switch ( $value ) { |
650 | case 'day': |
651 | case 'night': |
652 | case 'os': |
653 | return $value; |
654 | case '1': |
655 | return 'night'; |
656 | case '2': |
657 | return 'os'; |
658 | default: |
659 | return 'day'; |
660 | } |
661 | } |
662 | |
663 | /** |
664 | * Provides skin-specific modifications to the HTML element attributes |
665 | * |
666 | * Currently only used for adding the night mode class |
667 | * |
668 | * @return array |
669 | */ |
670 | public function getHtmlElementAttributes(): array { |
671 | $attributes = parent::getHtmlElementAttributes(); |
672 | |
673 | // check to see if night mode is enabled via query params or by config |
674 | $webRequest = $this->getContext()->getRequest(); |
675 | $forceNightMode = $webRequest->getRawVal( 'minervanightmode' ); |
676 | |
677 | // get skin config of night mode to check what is execluded |
678 | $nightModeConfig = $this->getConfig()->get( 'MinervaNightModeOptions' ); |
679 | $featuresHelper = new FeaturesHelper(); |
680 | $shouldDisableNightMode = $featuresHelper->shouldDisableNightMode( $nightModeConfig, |
681 | $webRequest, |
682 | $this->getContext()->getTitle() |
683 | ); |
684 | |
685 | if ( |
686 | $this->skinOptions->get( SkinOptions::NIGHT_MODE ) || $forceNightMode !== null |
687 | ) { |
688 | $user = $this->getUser(); |
689 | $value = $this->userOptionsManager->getOption( $user, 'minerva-theme' ); |
690 | |
691 | // if forcing a (valid) setting via query params, take priority over the user option |
692 | if ( $forceNightMode !== null && in_array( $forceNightMode, [ '1', '0', '2', 'day', 'night', 'os' ] ) ) { |
693 | $value = self::resolveNightModeQueryValue( $forceNightMode ); |
694 | } |
695 | |
696 | // For T356653 add a class to the page to allow the client to detect we've |
697 | // intentionally disabled night mode. |
698 | if ( $shouldDisableNightMode ) { |
699 | $attributes[ 'class' ] .= ' skin-night-mode-page-disabled'; |
700 | return $attributes; |
701 | } |
702 | |
703 | $attributes[ 'class' ] .= " skin-theme-clientpref-$value"; |
704 | } |
705 | |
706 | return $attributes; |
707 | } |
708 | |
709 | /** |
710 | * Whether the output page contains category links and the category feature is enabled. |
711 | * @return bool |
712 | */ |
713 | private function hasCategoryLinks(): bool { |
714 | if ( !$this->skinOptions->get( SkinOptions::CATEGORIES ) ) { |
715 | return false; |
716 | } |
717 | $categoryLinks = $this->getOutput()->getCategoryLinks(); |
718 | |
719 | if ( !count( $categoryLinks ) ) { |
720 | return false; |
721 | } |
722 | return !empty( $categoryLinks['normal'] ) || !empty( $categoryLinks['hidden'] ); |
723 | } |
724 | |
725 | /** |
726 | * Get a history link which describes author and relative time of last edit |
727 | * @param Title $title The Title object of the page being viewed |
728 | * @param string $timestamp |
729 | * @return array |
730 | */ |
731 | protected function getRelativeHistoryLink( Title $title, string $timestamp ): array { |
732 | $user = $this->getUser(); |
733 | $userDate = $this->getLanguage()->userDate( $timestamp, $user ); |
734 | $text = $this->msg( |
735 | 'minerva-last-modified-date', $userDate, |
736 | $this->getLanguage()->userTime( $timestamp, $user ) |
737 | )->parse(); |
738 | return [ |
739 | // Use $edit['timestamp'] (Unix format) instead of $timestamp (MW format) |
740 | 'data-timestamp' => wfTimestamp( TS_UNIX, $timestamp ), |
741 | 'href' => $this->getHistoryUrl( $title ), |
742 | 'text' => $text, |
743 | ] + $this->getRevisionEditorData( $title ); |
744 | } |
745 | |
746 | /** |
747 | * Get a history link which makes no reference to user or last edited time |
748 | * @param Title $title The Title object of the page being viewed |
749 | * @return array |
750 | */ |
751 | protected function getGenericHistoryLink( Title $title ): array { |
752 | $text = $this->msg( 'mobile-frontend-history' )->plain(); |
753 | return [ |
754 | 'href' => $this->getHistoryUrl( $title ), |
755 | 'text' => $text, |
756 | ]; |
757 | } |
758 | |
759 | /** |
760 | * Checks if the Special:History page is being used. |
761 | * @param Title $title The Title object of the page being viewed |
762 | * @return bool |
763 | */ |
764 | private function shouldUseSpecialHistory( Title $title ): bool { |
765 | return ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) && |
766 | SpecialMobileHistory::shouldUseSpecialHistory( $title, $this->getUser() ); |
767 | } |
768 | |
769 | /** |
770 | * Get the URL for the history page for the given title using Special:History |
771 | * when available. |
772 | * @param Title $title The Title object of the page being viewed |
773 | * @return string |
774 | */ |
775 | protected function getHistoryUrl( Title $title ): string { |
776 | return $this->shouldUseSpecialHistory( $title ) ? |
777 | SpecialPage::getTitleFor( 'History', $title )->getLocalURL() : |
778 | $title->getLocalURL( [ 'action' => 'history' ] ); |
779 | } |
780 | |
781 | /** |
782 | * Prepare the content for the 'last edited' message, e.g. 'Last edited on 30 August |
783 | * 2013, at 23:31'. This message is different for the main page since main page |
784 | * content is typically transcluded rather than edited directly. |
785 | * |
786 | * The relative time is only rendered on the latest revision. |
787 | * For older revisions the last modified information will not render with a relative time |
788 | * nor will it show the name of the editor. |
789 | * @param Title $title The Title object of the page being viewed |
790 | * @return array|null |
791 | */ |
792 | protected function getHistoryLink( Title $title ): ?array { |
793 | if ( !$title->exists() || |
794 | $this->getContext()->getActionName() !== 'view' |
795 | ) { |
796 | return null; |
797 | } |
798 | // Do not show the last modified bar on diff pages [T350515] |
799 | $request = $this->getRequest(); |
800 | if ( $request->getCheck( 'diff' ) ) { |
801 | return null; |
802 | } |
803 | |
804 | $out = $this->getOutput(); |
805 | |
806 | if ( !$out->getRevisionId() || !$out->isRevisionCurrent() || $title->isMainPage() ) { |
807 | $historyLink = $this->getGenericHistoryLink( $title ); |
808 | } else { |
809 | // Get rev_timestamp of current revision (preloaded by MediaWiki core) |
810 | $timestamp = $out->getRevisionTimestamp(); |
811 | if ( !$timestamp ) { |
812 | # No cached timestamp, load it from the database |
813 | $timestamp = $this->revisionLookup->getTimestampFromId( $out->getRevisionId() ); |
814 | } |
815 | $historyLink = $this->getRelativeHistoryLink( $title, $timestamp ); |
816 | } |
817 | |
818 | return $historyLink + [ |
819 | 'historyIcon' => [ |
820 | 'icon' => 'modified-history', |
821 | 'size' => 'medium' |
822 | ], |
823 | 'arrowIcon' => [ |
824 | 'icon' => 'expand', |
825 | 'size' => 'small' |
826 | ] |
827 | ]; |
828 | } |
829 | |
830 | /** |
831 | * Returns data attributes representing the editor for the current revision. |
832 | * @param LinkTarget $title The Title object of the page being viewed |
833 | * @return array representing user with name and gender fields. Empty if the editor no longer |
834 | * exists in the database or is hidden from public view. |
835 | */ |
836 | private function getRevisionEditorData( LinkTarget $title ): array { |
837 | $rev = $this->revisionLookup->getRevisionByTitle( $title ); |
838 | $result = []; |
839 | if ( $rev ) { |
840 | $revUser = $rev->getUser(); |
841 | // Note the user will only be returned if that information is public |
842 | if ( $revUser ) { |
843 | $editorName = $revUser->getName(); |
844 | $editorGender = $this->genderCache->getGenderOf( $revUser, __METHOD__ ); |
845 | $result += [ |
846 | 'data-user-name' => $editorName, |
847 | 'data-user-gender' => $editorGender, |
848 | ]; |
849 | } |
850 | } |
851 | return $result; |
852 | } |
853 | |
854 | /** |
855 | * Returns the HTML representing the tagline |
856 | * @return string HTML for tagline |
857 | */ |
858 | protected function getTaglineHtml(): string { |
859 | $tagline = ''; |
860 | |
861 | $pageUser = $this->skinUserPageHelper->getPageUser(); |
862 | if ( $pageUser ) { |
863 | $fromDate = $pageUser->getRegistration(); |
864 | |
865 | if ( $this->skinUserPageHelper->isUserPageAccessibleToCurrentUser() && is_string( $fromDate ) ) { |
866 | $fromDateTs = wfTimestamp( TS_UNIX, $fromDate ); |
867 | |
868 | // This is shown when js is disabled. js enhancement made due to caching |
869 | $tagline = $this->msg( 'mobile-frontend-user-page-member-since', |
870 | $this->getLanguage()->userDate( new MWTimestamp( $fromDateTs ), $this->getUser() ), |
871 | $pageUser )->text(); |
872 | |
873 | // Define html attributes for usage with js enhancement (unix timestamp, gender) |
874 | $attrs = [ 'id' => 'tagline-userpage', |
875 | 'data-userpage-registration-date' => $fromDateTs, |
876 | 'data-userpage-gender' => $this->genderCache->getGenderOf( $pageUser, __METHOD__ ) ]; |
877 | } |
878 | } else { |
879 | if ( $this->getTitle() ) { |
880 | $out = $this->getOutput(); |
881 | $tagline = $out->getProperty( 'wgMFDescription' ); |
882 | } |
883 | } |
884 | |
885 | $attrs[ 'class' ] = 'tagline'; |
886 | return Html::element( 'div', $attrs, $tagline ); |
887 | } |
888 | |
889 | /** |
890 | * Returns the HTML representing the heading. |
891 | * |
892 | * @param string $heading The heading suggested by core. |
893 | * @return string HTML for header |
894 | */ |
895 | private function getUserPageHeadingHtml( string $heading ): string { |
896 | // The heading is just the username without namespace |
897 | return Html::element( 'h1', |
898 | // These IDs and classes should match Skin::getTemplateData |
899 | [ |
900 | 'id' => 'firstHeading', |
901 | 'class' => 'firstHeading mw-first-heading mw-minerva-user-heading', |
902 | ], |
903 | $this->skinUserPageHelper->getPageUser()->getName() |
904 | ); |
905 | } |
906 | |
907 | /** |
908 | * Load internal banner content to show in pre content in template |
909 | * Beware of HTML caching when using this function. |
910 | * Content set as "internalbanner" |
911 | * @param string $siteNotice HTML fragment |
912 | * @return array |
913 | */ |
914 | protected function prepareBanners( string $siteNotice ): array { |
915 | $banners = []; |
916 | if ( $siteNotice && $this->getConfig()->get( 'MinervaEnableSiteNotice' ) ) { |
917 | $banners[] = $siteNotice; |
918 | } else { |
919 | $banners[] = '<div id="siteNotice"></div>'; |
920 | } |
921 | return $banners; |
922 | } |
923 | |
924 | /** |
925 | * Returns an array with details for a language button. |
926 | * @return array |
927 | */ |
928 | protected function getLanguageButton(): array { |
929 | return [ |
930 | 'array-attributes' => [ |
931 | [ |
932 | 'key' => 'href', |
933 | 'value' => '#p-lang' |
934 | ] |
935 | ], |
936 | 'tag-name' => 'a', |
937 | 'classes' => 'language-selector button', |
938 | 'label' => $this->msg( 'mobile-frontend-language-article-heading' )->text() |
939 | ]; |
940 | } |
941 | |
942 | /** |
943 | * Returns an array with details for a talk button. |
944 | * @param Title $talkTitle Title object of the talk page |
945 | * @param string $label Button label |
946 | * @return array |
947 | */ |
948 | protected function getTalkButton( Title $talkTitle, string $label ): array { |
949 | return [ |
950 | 'array-attributes' => [ |
951 | [ |
952 | 'key' => 'href', |
953 | 'value' => $talkTitle->getLinkURL(), |
954 | ], |
955 | [ |
956 | 'key' => 'data-title', |
957 | 'value' => $talkTitle->getFullText(), |
958 | ] |
959 | ], |
960 | 'tag-name' => 'a', |
961 | 'classes' => 'talk button', |
962 | 'label' => $label, |
963 | ]; |
964 | } |
965 | |
966 | /** |
967 | * Returns an array of links for page secondary actions |
968 | * @param array $contentNavigationUrls |
969 | * @return array|null |
970 | */ |
971 | protected function getSecondaryActions( array $contentNavigationUrls ): ?array { |
972 | if ( $this->isFallbackEditor() || !$this->hasSecondaryActions() ) { |
973 | return null; |
974 | } |
975 | |
976 | $buttons = []; |
977 | // always add a button to link to the talk page |
978 | // it will link to the wikitext talk page |
979 | $title = $this->getTitle(); |
980 | $subjectPage = Title::newFromLinkTarget( $this->namespaceInfo->getSubjectPage( $title ) ); |
981 | $talkAtBottom = !$this->skinOptions->get( SkinOptions::TALK_AT_TOP ) || |
982 | $subjectPage->isMainPage(); |
983 | if ( !$this->skinUserPageHelper->isUserPage() && |
984 | $this->permissions->isTalkAllowed() && $talkAtBottom && |
985 | // When showing talk at the bottom we restrict this so it is not shown to anons |
986 | // https://phabricator.wikimedia.org/T54165 |
987 | // This whole code block can be removed when SkinOptions::TALK_AT_TOP is always true |
988 | $this->getUser()->isRegistered() |
989 | ) { |
990 | $namespaces = $contentNavigationUrls['associated-pages']; |
991 | // FIXME [core]: This seems unnecessary.. |
992 | $subjectId = $title->getNamespaceKey( '' ); |
993 | $talkId = $subjectId === 'main' ? 'talk' : "{$subjectId}_talk"; |
994 | |
995 | if ( isset( $namespaces[$talkId] ) ) { |
996 | $talkButton = $namespaces[$talkId]; |
997 | $talkTitle = Title::newFromLinkTarget( $this->namespaceInfo->getTalkPage( $title ) ); |
998 | |
999 | $buttons['talk'] = $this->getTalkButton( $talkTitle, $talkButton['text'] ); |
1000 | } |
1001 | } |
1002 | |
1003 | if ( |
1004 | $this->languagesHelper->doesTitleHasLanguagesOrVariants( $this->getOutput(), $title ) && |
1005 | $title->isMainPage() |
1006 | ) { |
1007 | $buttons['language'] = $this->getLanguageButton(); |
1008 | } |
1009 | |
1010 | return $buttons; |
1011 | } |
1012 | |
1013 | /** |
1014 | * @inheritDoc |
1015 | * @return array |
1016 | */ |
1017 | protected function getJsConfigVars(): array { |
1018 | return array_merge( parent::getJsConfigVars(), [ |
1019 | 'wgMinervaPermissions' => [ |
1020 | 'watchable' => $this->permissions->isAllowed( IMinervaPagePermissions::WATCHABLE ), |
1021 | 'watch' => $this->permissions->isAllowed( IMinervaPagePermissions::WATCH ), |
1022 | ], |
1023 | 'wgMinervaFeatures' => $this->skinOptions->getAll(), |
1024 | 'wgMinervaDownloadNamespaces' => $this->getConfig()->get( 'MinervaDownloadNamespaces' ), |
1025 | ] ); |
1026 | } |
1027 | |
1028 | /** |
1029 | * Returns the javascript entry modules to load. Only modules that need to |
1030 | * be overriden or added conditionally should be placed here. |
1031 | * @return array |
1032 | */ |
1033 | public function getDefaultModules(): array { |
1034 | $modules = parent::getDefaultModules(); |
1035 | |
1036 | // FIXME: T223204: Dequeue default content modules except for the history |
1037 | // action. Allow default content modules on history action in order to |
1038 | // enable toggling of the filters. |
1039 | // Long term this won't be necessary when T111565 is resolved and a |
1040 | // more general solution can be used. |
1041 | if ( $this->getContext()->getActionName() !== 'history' ) { |
1042 | // dequeue default content modules (toc, collapsible, etc.) |
1043 | $modules['content'] = array_diff( $modules['content'], [ |
1044 | // T111565 |
1045 | 'jquery.makeCollapsible', |
1046 | // Minerva provides its own implementation. Loading this will break display. |
1047 | 'mediawiki.toc' |
1048 | ] ); |
1049 | // dequeue styles associated with `content` key. |
1050 | $modules['styles']['content'] = array_diff( $modules['styles']['content'], [ |
1051 | // T111565 |
1052 | 'jquery.makeCollapsible.styles', |
1053 | ] ); |
1054 | } |
1055 | |
1056 | $modules['styles']['skin.page'] = $this->getPageSpecificStyles(); |
1057 | $modules['styles']['skin.features'] = $this->getFeatureSpecificStyles(); |
1058 | |
1059 | return $modules; |
1060 | } |
1061 | |
1062 | /** |
1063 | * Provide styles required to present the server rendered page in this skin. Additional styles |
1064 | * may be loaded dynamically by the client. |
1065 | * |
1066 | * Any styles returned by this method are loaded on the critical rendering path as linked |
1067 | * stylesheets. I.e., they are required to load on the client before first paint. |
1068 | * |
1069 | * @return array |
1070 | */ |
1071 | protected function getPageSpecificStyles(): array { |
1072 | $styles = []; |
1073 | $title = $this->getTitle(); |
1074 | $request = $this->getRequest(); |
1075 | $requestAction = $this->getContext()->getActionName(); |
1076 | $viewAction = $requestAction === 'view'; |
1077 | |
1078 | // Warning box styles are needed when reviewing old revisions |
1079 | // and inside the fallback editor styles to action=edit page. |
1080 | if ( |
1081 | $title->getNamespace() !== NS_MAIN || |
1082 | $request->getCheck( 'oldid' ) || |
1083 | !$viewAction |
1084 | ) { |
1085 | $styles[] = 'skins.minerva.messageBox.styles'; |
1086 | } |
1087 | |
1088 | if ( $title->isMainPage() ) { |
1089 | $styles[] = 'skins.minerva.mainPage.styles'; |
1090 | } elseif ( $this->skinUserPageHelper->isUserPage() ) { |
1091 | $styles[] = 'skins.minerva.userpage.styles'; |
1092 | } |
1093 | |
1094 | if ( $this->getUser()->isRegistered() ) { |
1095 | $styles[] = 'skins.minerva.loggedin.styles'; |
1096 | } |
1097 | |
1098 | // When any of these features are enabled in production |
1099 | // remove the if condition |
1100 | // and move the associated LESS file inside `skins.minerva.amc.styles` |
1101 | // into a more appropriate module. |
1102 | if ( |
1103 | // T356117 - enable on all special pages - some special pages e.g. Special:Contribute have tabs. |
1104 | $title->isSpecialPage() || |
1105 | ( $this->isHistoryPage() && !$this->shouldUseSpecialHistory( $title ) ) || |
1106 | $this->skinOptions->get( SkinOptions::PERSONAL_MENU ) || |
1107 | $this->skinOptions->get( SkinOptions::TALK_AT_TOP ) || |
1108 | $this->skinOptions->get( SkinOptions::HISTORY_IN_PAGE_ACTIONS ) || |
1109 | $this->skinOptions->get( SkinOptions::TOOLBAR_SUBMENU ) |
1110 | ) { |
1111 | // SkinOptions::PERSONAL_MENU + SkinOptions::TOOLBAR_SUBMENU uses ToggleList |
1112 | // SkinOptions::TALK_AT_TOP uses tabs.less |
1113 | // SkinOptions::HISTORY_IN_PAGE_ACTIONS + SkinOptions::TOOLBAR_SUBMENU uses pageactions.less |
1114 | $styles[] = 'skins.minerva.amc.styles'; |
1115 | } |
1116 | |
1117 | return $styles; |
1118 | } |
1119 | |
1120 | /** |
1121 | * Provide styles required to present the server rendered page with related features in this skin. |
1122 | * Additional styles may be loaded dynamically by the client. |
1123 | * |
1124 | * Any styles returned by this method are loaded on the critical rendering path as linked |
1125 | * stylesheets. I.e., they are required to load on the client before first paint. |
1126 | * |
1127 | * @return array |
1128 | */ |
1129 | protected function getFeatureSpecificStyles(): array { |
1130 | $styles = []; |
1131 | |
1132 | if ( $this->hasCategoryLinks() ) { |
1133 | $styles[] = 'skins.minerva.categories.styles'; |
1134 | } |
1135 | |
1136 | if ( $this->skinOptions->get( SkinOptions::PERSONAL_MENU ) ) { |
1137 | // If ever enabled as the default, please remove the duplicate icons |
1138 | // inside skins.minerva.mainMenu.icons. See comment for MAIN_MENU_EXPANDED |
1139 | $styles[] = 'skins.minerva.personalMenu.icons'; |
1140 | } |
1141 | |
1142 | if ( |
1143 | $this->skinOptions->get( SkinOptions::MAIN_MENU_EXPANDED ) |
1144 | ) { |
1145 | // If ever enabled as the default, please review skins.minerva.mainMenu.icons |
1146 | // and remove any unneeded icons |
1147 | $styles[] = 'skins.minerva.mainMenu.advanced.icons'; |
1148 | } |
1149 | |
1150 | if ( |
1151 | $this->skinOptions->get( SkinOptions::PERSONAL_MENU ) || |
1152 | $this->skinOptions->get( SkinOptions::TOOLBAR_SUBMENU ) |
1153 | ) { |
1154 | // SkinOptions::PERSONAL_MENU requires the `userTalk` icon. |
1155 | // SkinOptions::TOOLBAR_SUBMENU requires the rest of the icons including `overflow`. |
1156 | // Note `skins.minerva.overflow.icons` is pulled down by skins.minerva.scripts but the menu can |
1157 | // work without JS. |
1158 | $styles[] = 'skins.minerva.overflow.icons'; |
1159 | } |
1160 | |
1161 | return $styles; |
1162 | } |
1163 | } |