Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
VectorComponentUserLinks
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 9
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDropdown
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 getMenus
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
72
 stripIcons
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 makeLinksButtons
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 makeItemsCollapsible
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getOverflowMenuClass
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getTemplateData
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2namespace MediaWiki\Skins\Vector\Components;
3
4use MediaWiki\Linker\Linker;
5use MediaWiki\Skin\SkinComponentLink;
6use MediaWiki\Title\MalformedTitleException;
7use MediaWiki\Title\Title;
8use MediaWiki\User\UserIdentity;
9use Message;
10use MessageLocalizer;
11
12/**
13 * VectorComponentUserLinks component
14 */
15class VectorComponentUserLinks implements VectorComponent {
16
17    private const BUTTON_CLASSES = 'cdx-button cdx-button--fake-button '
18        . 'cdx-button--fake-button--enabled cdx-button--weight-quiet';
19    private const ICON_ONLY_BUTTON_CLASS = 'cdx-button--icon-only';
20
21    /** @var MessageLocalizer */
22    private $localizer;
23    /** @var UserIdentity */
24    private $user;
25    /** @var array */
26    private $portletData;
27    /** @var array */
28    private $linkOptions;
29    /** @var string */
30    private $userIcon;
31
32    /**
33     * @param MessageLocalizer $localizer
34     * @param UserIdentity $user
35     * @param array $portletData
36     * @param array $linkOptions
37     * @param string $userIcon that represents the current type of user
38     */
39    public function __construct(
40        MessageLocalizer $localizer,
41        UserIdentity $user,
42        array $portletData,
43        array $linkOptions,
44        string $userIcon = 'userAvatar'
45    ) {
46        $this->localizer = $localizer;
47        $this->user = $user;
48        $this->portletData = $portletData;
49        $this->linkOptions = $linkOptions;
50        $this->userIcon = $userIcon;
51    }
52
53    /**
54     * @param string $key
55     * @return Message
56     */
57    private function msg( $key ): Message {
58        return $this->localizer->msg( $key );
59    }
60
61    /**
62     * @param bool $isDefaultAnonUserLinks
63     * @param bool $isAnonEditorLinksEnabled
64     * @return VectorComponentDropdown
65     */
66    private function getDropdown( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) {
67        $user = $this->user;
68        $isAnon = !$user->isRegistered();
69
70        $class = 'vector-user-menu';
71        $class .= ' vector-button-flush-right';
72        $class .= !$isAnon ?
73            ' vector-user-menu-logged-in' :
74            ' vector-user-menu-logged-out';
75
76        // Hide entire user links dropdown on larger viewports if it only contains
77        // create account & login link, which are only shown on smaller viewports
78        if ( $isAnon && $isDefaultAnonUserLinks && !$isAnonEditorLinksEnabled ) {
79            $class .= ' user-links-collapsible-item';
80        }
81
82        $tooltip = '';
83        $icon = $this->userIcon;
84        if ( $icon === '' ) {
85            $icon = 'ellipsis';
86            // T287494 We use tooltip messages to provide title attributes on hover over certain menu icons.
87            // For modern Vector, the "tooltip-p-personal" key is set to "User menu" which is appropriate for
88            // the user icon (dropdown indicator for user links menu) for logged-in users.
89            // This overrides the tooltip for the user links menu icon which is an ellipsis for anonymous users.
90            $tooltip = Linker::tooltip( 'vector-anon-user-menu-title' ) ?? '';
91        }
92
93        return new VectorComponentDropdown(
94            'vector-user-links-dropdown', $this->msg( 'personaltools' )->text(), $class, $icon, $tooltip
95        );
96    }
97
98    /**
99     * @param bool $isDefaultAnonUserLinks
100     * @param bool $isAnonEditorLinksEnabled
101     * @return array
102     */
103    private function getMenus( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) {
104        $user = $this->user;
105        $isAnon = !$user->isRegistered();
106        $portletData = $this->portletData;
107
108        // Hide default user menu on larger viewports if it only contains
109        // create account & login link, which are only shown on smaller viewports
110        // FIXME: Replace array_merge with an add class helper function
111        $userMenuClass = $portletData[ 'data-user-menu' ][ 'class' ];
112        $userMenuClass = $isAnon && $isDefaultAnonUserLinks ?
113            $userMenuClass . ' user-links-collapsible-item' : $userMenuClass;
114        $dropdownMenus = [
115            new VectorComponentMenu( [
116                'label' => null,
117                'class' => $userMenuClass
118            ] + $portletData[ 'data-user-menu' ] )
119        ];
120
121        if ( $isAnon ) {
122            // T317789: The `anontalk` and `anoncontribs` links will not be added to
123            // the menu if `$wgGroupPermissions['*']['edit']` === false which can
124            // leave the menu empty due to our removal of other user menu items in
125            // `Hooks::updateUserLinksDropdownItems`. In this case, we do not want
126            // to render the anon "learn more" link.
127            if ( $isAnonEditorLinksEnabled ) {
128                $anonUserMenuData = $portletData[ 'data-user-menu-anon-editor' ];
129                try {
130                    $anonEditorLabelLinkData = [
131                        'text' => $this->msg( 'vector-anon-user-menu-pages-learn' )->text(),
132                        'href' => Title::newFromTextThrow( $this->msg( 'vector-intro-page' )->text() )->getLocalURL(),
133                        'aria-label' => $this->msg( 'vector-anon-user-menu-pages-label' )->text(),
134                    ];
135                    $anonEditorLabelLink = new SkinComponentLink(
136                        '', $anonEditorLabelLinkData, $this->localizer, $this->linkOptions
137                    );
138                    $anonEditorLabelLinkHtml = $anonEditorLabelLink->getTemplateData()[ 'html' ];
139                    $anonUserMenuData['html-label'] = $this->msg( 'vector-anon-user-menu-pages' )->escaped() .
140                        " " . $anonEditorLabelLinkHtml;
141                    $anonUserMenuData['label'] = null;
142                } catch ( MalformedTitleException $e ) {
143                    // ignore (T340220)
144                }
145                $dropdownMenus[] = new VectorComponentMenu( $anonUserMenuData );
146            }
147        } else {
148            // Logout isn't enabled for temp users, who are considered still considered registered
149            $isLogoutLinkEnabled = isset( $portletData[ 'data-user-menu-logout' ][ 'is-empty' ] ) &&
150                !$portletData[ 'data-user-menu-logout'][ 'is-empty' ];
151            if ( $isLogoutLinkEnabled ) {
152                $dropdownMenus[] = new VectorComponentMenu( [
153                    'label' => null
154                ] + $portletData[ 'data-user-menu-logout' ] );
155            }
156        }
157
158        return $dropdownMenus;
159    }
160
161    /**
162     * Strips icons from the menu.
163     *
164     * @param array $arrayListItems
165     * @return array
166     */
167    private static function stripIcons( array $arrayListItems ) {
168        return array_map( static function ( $item ) {
169            $item['array-links'] = array_map( static function ( $link ) {
170                $link['icon'] = null;
171                return $link;
172            }, $item['array-links'] );
173            return $item;
174        }, $arrayListItems );
175    }
176
177    /**
178     * Converts links to button icons
179     *
180     * @param array $arrayListItems
181     * @param bool $iconOnlyButton whether label should be visible.
182     * @param array $exceptions list of names of items that should not be converted.
183     * @return array
184     */
185    private static function makeLinksButtons( $arrayListItems, $iconOnlyButton = true, $exceptions = [] ) {
186        return array_map( static function ( $item ) use ( $iconOnlyButton, $exceptions ) {
187            if ( in_array( $item[ 'name'], $exceptions ) ) {
188                return $item;
189            }
190            $item['array-links'] = array_map( static function ( $link ) use ( $iconOnlyButton ) {
191                $link['array-attributes'] = array_map( static function ( $attribute ) use ( $iconOnlyButton ) {
192                    if ( $attribute['key'] === 'class' ) {
193                        $newClass = $attribute['value'] . ' ' . self::BUTTON_CLASSES;
194                        if ( $iconOnlyButton ) {
195                            $newClass .= ' ' . self::ICON_ONLY_BUTTON_CLASS;
196                        }
197                        $attribute['value'] = $newClass;
198                    }
199                    return $attribute;
200                }, $link['array-attributes'] );
201                return $link;
202            }, $item['array-links'] );
203            return $item;
204        }, $arrayListItems );
205    }
206
207    /**
208     * Makes all menu items collapsible at lower resolutions.
209     *
210     * @param array $arrayListItems
211     * @return array
212     */
213    private static function makeItemsCollapsible( $arrayListItems ) {
214        return array_map( static function ( $item ) {
215            $item['class'] .= ' user-links-collapsible-item';
216            return $item;
217        }, $arrayListItems );
218    }
219
220    /**
221     * What class should the overflow menu have?
222     *
223     * @param array $arrayListItems
224     * @return string
225     */
226    private static function getOverflowMenuClass( $arrayListItems ) {
227        $overflowMenuClass = 'mw-portlet';
228        if ( count( $arrayListItems ) === 0 ) {
229            $overflowMenuClass .= ' emptyPortlet';
230        }
231        return $overflowMenuClass;
232    }
233
234    /**
235     * @inheritDoc
236     */
237    public function getTemplateData(): array {
238        $portletData = $this->portletData;
239
240        $isDefaultAnonUserLinks = count( $portletData['data-user-menu']['array-items'] ) === 2;
241        $isAnonEditorLinksEnabled = isset( $portletData['data-user-menu-anon-editor']['is-empty'] )
242            && !$portletData['data-user-menu-anon-editor']['is-empty'];
243
244        $userInterfacePreferences = $this->makeLinksButtons(
245            $this->makeItemsCollapsible(
246                $portletData[ 'data-user-interface-preferences' ]['array-items'] ?? []
247            ),
248            false
249        );
250        $userPage = $this->makeItemsCollapsible(
251            $this->stripIcons( $portletData[ 'data-user-page' ]['array-items'] ?? [] )
252        );
253        $notifications = $this->makeLinksButtons(
254            $portletData[ 'data-notifications' ]['array-items'] ?? [],
255            true,
256            [ 'talk-alert' ]
257        );
258
259        $overflow = $this->makeItemsCollapsible(
260            array_map(
261                static function ( $item ) {
262                    // Since we're creating duplicate icons
263                    $item['id'] .= '-2';
264                    // Restore icon removed in hooks.
265                    if ( $item['name'] === 'watchlist' ) {
266                        $item['icon'] = 'watchlist';
267                    }
268                    return $item;
269                },
270                // array_filter preserves keys so use array_values to restore array.
271                array_values(
272                    array_filter(
273                        $portletData['data-user-menu']['array-items'] ?? [],
274                        static function ( $item ) {
275                            // Only certain items get promoted to the overflow menu:
276                            // * watchlist
277                            // * login
278                            // * create account
279                            $name = $item['name'];
280                            return in_array( $name, [ 'watchlist', 'createaccount', 'login', 'login-private' ] );
281                        }
282                    )
283                )
284            )
285        );
286        // Convert to buttons for logged in users.
287        // For anons these will remain as links.
288        // Note: This list is empty for temporary users currently.
289        if ( $this->user->isRegistered() ) {
290            $overflow = $this->makeLinksButtons( $overflow );
291        }
292
293        $preferencesMenu = new VectorComponentMenu( [
294            'id' => 'p-vector-user-menu-preferences',
295            'class' => self::getOverflowMenuClass( $userInterfacePreferences ),
296            'label' => null,
297            'html-items' => null,
298            'array-list-items' => $userInterfacePreferences,
299        ] );
300        $userPageMenu = new VectorComponentMenu( [
301            'id' => 'p-vector-user-menu-userpage',
302            'class' => self::getOverflowMenuClass( $userPage ),
303            'label' => null,
304            'html-items' => null,
305            'array-list-items' => $userPage,
306        ] );
307        $notificationsMenu = new VectorComponentMenu( [
308            'id' => 'p-vector-user-menu-notifications',
309            'class' => self::getOverflowMenuClass( $notifications ),
310            'label' => null,
311            'html-items' => null,
312            'array-list-items' => $notifications,
313        ] );
314        $overflowMenu = new VectorComponentMenu( [
315            'id' => 'p-vector-user-menu-overflow',
316            'class' => self::getOverflowMenuClass( $overflow ),
317            'label' => null,
318            'html-items' => null,
319            'array-list-items' => $overflow,
320        ] );
321
322        return [
323            'is-wide' => array_filter(
324                [ $overflow, $notifications, $userPage, $userInterfacePreferences ]
325            ) !== [],
326            'data-user-links-notifications' => $notificationsMenu->getTemplateData(),
327            'data-user-links-overflow' => $overflowMenu->getTemplateData(),
328            'data-user-links-preferences' => $preferencesMenu->getTemplateData(),
329            'data-user-links-user-page' => $userPageMenu->getTemplateData(),
330            'data-user-links-dropdown' => $this->getDropdown( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled )
331                ->getTemplateData(),
332            'data-user-links-menus' => array_map( static function ( $menu ) {
333                return $menu->getTemplateData();
334            }, $this->getMenus( $isDefaultAnonUserLinks, $isAnonEditorLinksEnabled ) ),
335        ];
336    }
337}