Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 199
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
PreferencesFormOOUI
0.00% covered (danger)
0.00%
0 / 199
0.00% covered (danger)
0.00%
0 / 20
1640
0.00% covered (danger)
0.00%
0 / 1
 setModifiedUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getModifiedUser
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isPrivateInfoEditable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPrivateInfoEditable
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 areOptionsEditable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOptionsEditable
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getExtraSuccessRedirectParameters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 wrapForm
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 filterDataForSubmit
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 wrapFieldSetSection
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 isMobileLayout
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 addFields
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
 getBody
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 getLegend
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getPreferenceSections
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createMobilePreferencesForm
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
12
 getIconNames
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 createMobileDescription
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 createContentMobile
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
2
 createDesktopPreferencesForm
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
12
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
21use MediaWiki\HTMLForm\Field\HTMLCheckField;
22use MediaWiki\HTMLForm\Field\HTMLToggleSwitchField;
23use MediaWiki\HTMLForm\HTMLNestedFilterable;
24use MediaWiki\HTMLForm\OOUIHTMLForm;
25use MediaWiki\User\User;
26
27/**
28 * Form to edit user preferences.
29 *
30 * @since 1.32
31 */
32class PreferencesFormOOUI extends OOUIHTMLForm {
33    /** @var bool Override default value from HTMLForm */
34    protected $mSubSectionBeforeFields = false;
35
36    /** @var User|null */
37    private $modifiedUser;
38
39    /** @var bool */
40    private $privateInfoEditable = true;
41
42    /** @var bool */
43    private $optionsEditable = true;
44
45    /** @var bool */
46    private $useMobileLayout;
47
48    /**
49     * @param User $user
50     */
51    public function setModifiedUser( $user ) {
52        $this->modifiedUser = $user;
53    }
54
55    /**
56     * @return User
57     */
58    public function getModifiedUser() {
59        if ( $this->modifiedUser === null ) {
60            return $this->getUser();
61        } else {
62            return $this->modifiedUser;
63        }
64    }
65
66    /**
67     * @return bool
68     */
69    public function isPrivateInfoEditable() {
70        return $this->privateInfoEditable;
71    }
72
73    /**
74     * Whether the
75     * @param bool $editable
76     */
77    public function setPrivateInfoEditable( $editable ) {
78        $this->privateInfoEditable = $editable;
79        $this->suppressDefaultSubmit( !$this->privateInfoEditable && !$this->optionsEditable );
80    }
81
82    /**
83     * @return bool
84     */
85    public function areOptionsEditable() {
86        return $this->optionsEditable;
87    }
88
89    /**
90     * @param bool $optionsEditable
91     */
92    public function setOptionsEditable( $optionsEditable ) {
93        $this->optionsEditable = $optionsEditable;
94        $this->suppressDefaultSubmit( !$this->privateInfoEditable && !$this->optionsEditable );
95    }
96
97    /**
98     * Get extra parameters for the query string when redirecting after
99     * successful save.
100     *
101     * @return array
102     */
103    public function getExtraSuccessRedirectParameters() {
104        return [];
105    }
106
107    public function wrapForm( $html ) {
108        $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
109
110        return parent::wrapForm( $html );
111    }
112
113    /**
114     * Separate multi-option preferences into multiple preferences, since we
115     * have to store them separately
116     * @param array $data
117     * @return array
118     */
119    public function filterDataForSubmit( $data ) {
120        foreach ( $this->mFlatFields as $fieldname => $field ) {
121            if ( $field instanceof HTMLNestedFilterable ) {
122                $info = $field->mParams;
123                $prefix = $info['prefix'] ?? $fieldname;
124                foreach ( $field->filterDataForSubmit( $data[$fieldname] ) as $key => $value ) {
125                    $data["$prefix$key"] = $value;
126                }
127                unset( $data[$fieldname] );
128            }
129        }
130
131        return $data;
132    }
133
134    protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
135        $layout = parent::wrapFieldSetSection( $legend, $section, $attributes, $isRoot );
136
137        $layout->addClasses( [ 'mw-prefs-fieldset-wrapper' ] );
138        $layout->removeClasses( [ 'oo-ui-panelLayout-framed' ] );
139
140        return $layout;
141    }
142
143    private function isMobileLayout() {
144        if ( $this->useMobileLayout === null ) {
145            $skin = $this->getSkin();
146            $this->useMobileLayout = false;
147            $this->getHookRunner()->onPreferencesGetLayout( $this->useMobileLayout,
148                $skin->getSkinName(), [ 'isResponsive' => $skin->isResponsive() ] );
149        }
150        return $this->useMobileLayout;
151    }
152
153    /**
154     * @inheritDoc
155     */
156    public function addFields( $descriptor ) {
157        // Replace checkbox fields with toggle switchs on Special:Preferences
158        if ( $this->isMobileLayout() && $this->getTitle()->isSpecial( 'Preferences' ) ) {
159            foreach ( $descriptor as $_ => &$info ) {
160                if ( isset( $info['type'] ) && in_array( $info['type'], [ 'check', 'toggle' ] ) ) {
161                    unset( $info['type'] );
162                    $info['class'] = HTMLToggleSwitchField::class;
163                } elseif ( isset( $info['class'] ) && $info['class'] === HTMLCheckField::class ) {
164                    $info['class'] = HTMLToggleSwitchField::class;
165                }
166            }
167        }
168        return parent::addFields( $descriptor );
169    }
170
171    /**
172     * Get the whole body of the form.
173     * @return string
174     */
175    public function getBody() {
176        if ( $this->isMobileLayout() ) {
177            // Import the icons used in the mobile view
178            $this->getOutput()->addModuleStyles(
179                [
180                    'oojs-ui.styles.icons-user',
181                    'oojs-ui.styles.icons-editing-core',
182                    'oojs-ui.styles.icons-editing-advanced',
183                    'oojs-ui.styles.icons-wikimediaui',
184                    'oojs-ui.styles.icons-content',
185                    'oojs-ui.styles.icons-moderation',
186                    'oojs-ui.styles.icons-interactions',
187                    'oojs-ui.styles.icons-movement',
188                    'oojs-ui.styles.icons-wikimedia',
189                    'oojs-ui.styles.icons-media',
190                    'oojs-ui.styles.icons-accessibility',
191                    'oojs-ui.styles.icons-layout',
192                ]
193            );
194            $form = $this->createMobilePreferencesForm();
195        } else {
196            $form = $this->createDesktopPreferencesForm();
197        }
198
199        $header = $this->formatFormHeader();
200
201        return $header . $form;
202    }
203
204    /**
205     * Get the "<legend>" for a given section key. Normally this is the
206     * prefs-$key message but we'll allow extensions to override it.
207     * @param string $key
208     * @return string
209     */
210    public function getLegend( $key ) {
211        $legend = parent::getLegend( $key );
212        $this->getHookRunner()->onPreferencesGetLegend( $this, $key, $legend );
213        return $legend;
214    }
215
216    /**
217     * Get the keys of each top level preference section.
218     * @return string[] List of section keys
219     */
220    public function getPreferenceSections() {
221        return array_keys( array_filter( $this->mFieldTree, 'is_array' ) );
222    }
223
224    /**
225     * Create the preferences form for a mobile layout.
226     * @return OOUI\Tag
227     */
228    private function createMobilePreferencesForm() {
229        $sectionButtons = [];
230        $sectionContents = [];
231        $iconNames = $this->getIconNames();
232
233        foreach ( $this->mFieldTree as $key => $val ) {
234            if ( !is_array( $val ) ) {
235                wfDebug( __METHOD__ . " encountered a field not attached to a section: '$key'" );
236                continue;
237            }
238            $label = $this->getLegend( $key );
239            $content =
240                $this->getHeaderHtml( $key ) .
241                $this->displaySection(
242                    $val,
243                    "",
244                    "mw-prefsection-$key-"
245                ) .
246                $this->getFooterHtml( $key );
247
248            // Creating the header section
249            $label = ( new OOUI\Tag( 'div' ) )->appendContent(
250                ( new OOUI\Tag( 'h5' ) )->appendContent( $label )->addClasses( [ 'mw-prefs-title' ] ),
251                $this->createMobileDescription( $key )
252            );
253            $contentDiv = $this->createContentMobile( $key, $label, $content );
254
255            $sectionButton = new OOUI\ButtonWidget( [
256                'id' => 'mw-mobile-prefs-' . $key,
257                'icon' => $iconNames[ $key ] ?? 'settings',
258                'label' => new OOUI\HtmlSnippet( $label->toString() ),
259                'data' => $key,
260                'classes' => [ 'mw-mobile-prefsection' ],
261                'framed' => false,
262            ] );
263            $sectionButtons[] = $sectionButton;
264            $sectionContents[] = $contentDiv;
265        }
266
267        $buttonGroup = new OOUI\ButtonGroupWidget( [
268            'classes' => [ 'mw-mobile-prefs-sections' ],
269            'infusable' => true,
270        ] );
271        $buttonGroup->addItems( $sectionButtons );
272        $form = ( new OOUI\Tag( 'div' ) )
273            ->setAttributes( [ 'id' => 'mw-prefs-container' ] )
274            ->addClasses( [ 'mw-mobile-prefs-container' ] )
275            ->appendContent( $buttonGroup )
276            ->appendContent( $sectionContents );
277
278        return $form;
279    }
280
281    /**
282     * Get the icon names for each mobile preference section.
283     * @return array
284     */
285    private function getIconNames() {
286        $iconNames = [
287            'personal' => 'userAvatar',
288            'rendering' => 'palette',
289            'editing' => 'edit',
290            'rc' => 'recentChanges',
291            'watchlist' => 'watchlist',
292            'searchoptions' => 'search',
293            'misc' => '',
294        ];
295        $hookIcons = [];
296        // Get icons from extensions that have their own sections
297        $this->getHookRunner()->onPreferencesGetIcon( $hookIcons );
298        $iconNames += $hookIcons;
299
300        return $iconNames;
301    }
302
303    /**
304     * Creates a description tag for each section of the mobile layout.
305     * @param string $key
306     * @return OOUI\Tag
307     */
308    private function createMobileDescription( $key ) {
309        $prefDescriptionMsg = $this->msg( "prefs-description-" . $key );
310        $prefDescription = $prefDescriptionMsg->exists() ? $prefDescriptionMsg->text() : "";
311        $prefDescriptionElement = ( new OOUI\Tag( 'p' ) )
312            ->appendContent( $prefDescription )
313            ->addClasses( [ 'mw-prefs-description' ] );
314
315        return $prefDescriptionElement;
316    }
317
318    /**
319     * Creates the contents for each section of the mobile layout.
320     * @param string $key
321     * @param string $label
322     * @param string $content
323     * @return OOUI\Tag
324     */
325    private function createContentMobile( $key, $label, $content ) {
326        $contentDiv = ( new OOUI\Tag( 'div' ) );
327        $contentDiv->addClasses( [
328            'mw-prefs-content-page',
329            'mw-prefs-section-fieldset',
330        ] );
331        $contentDiv->setAttributes( [
332            'id' => 'mw-mobile-prefs-' . $key
333        ] );
334        $contentBody = ( new OOUI\Tag( 'div' ) )
335            ->addClasses( [ 'mw-htmlform-autoinfuse-lazy' ] )
336            ->setAttributes( [
337                'id' => 'mw-mobile-prefs-' . $key . '-content'
338            ] );
339        $contentHeader = ( new OOUI\Tag( 'div' ) )->setAttributes( [
340            'id' => 'mw-mobile-prefs-' . $key . '-head'
341        ] );
342        $contentHeader->addClasses( [ 'mw-prefs-content-head' ] );
343        $contentHeaderTitle = ( new OOUI\Tag( 'h5' ) )->setAttributes( [
344            'id' => 'mw-mobile-prefs-' . $key . '-title',
345        ] );
346        $contentHeaderTitle->appendContent( $label )->addClasses( [ 'mw-prefs-header-title' ] );
347        $formContent = new OOUI\Widget( [
348            'content' => new OOUI\HtmlSnippet( $content )
349        ] );
350        $hiddenForm = ( new OOUI\Tag( 'div' ) )->appendContent( $formContent );
351        $contentHeader->appendContent( $contentHeaderTitle );
352        $contentBody->appendContent( $contentHeader );
353        $contentBody->appendContent( $hiddenForm );
354        $contentDiv->appendContent( $contentBody );
355
356        return $contentDiv;
357    }
358
359    /**
360     * Create the preferences form for a desktop layout.
361     * @return OOUI\PanelLayout
362     */
363    private function createDesktopPreferencesForm() {
364        $tabPanels = [];
365        foreach ( $this->mFieldTree as $key => $val ) {
366            if ( !is_array( $val ) ) {
367                wfDebug( __METHOD__ . " encountered a field not attached to a section: '$key'" );
368                continue;
369            }
370            $label = $this->getLegend( $key );
371            $content =
372                $this->getHeaderHtml( $key ) .
373                $this->displaySection(
374                    $val,
375                    "",
376                    "mw-prefsection-$key-"
377                ) .
378                $this->getFooterHtml( $key );
379
380            $tabPanels[] = new OOUI\TabPanelLayout( 'mw-prefsection-' . $key, [
381                'classes' => [ 'mw-htmlform-autoinfuse-lazy' ],
382                'label' => $label,
383                'content' => new OOUI\FieldsetLayout( [
384                    'classes' => [ 'mw-prefs-section-fieldset' ],
385                    'id' => "mw-prefsection-$key",
386                    'label' => $label,
387                    'items' => [
388                        new OOUI\Widget( [
389                            'content' => new OOUI\HtmlSnippet( $content )
390                        ] ),
391                    ],
392                ] ),
393                'expanded' => false,
394                'framed' => true,
395            ] );
396        }
397
398        $indexLayout = new OOUI\IndexLayout( [
399            'infusable' => true,
400            'expanded' => false,
401            'autoFocus' => false,
402            'classes' => [ 'mw-prefs-tabs' ],
403        ] );
404        $indexLayout->addTabPanels( $tabPanels );
405
406        $form = new OOUI\PanelLayout( [
407            'framed' => true,
408            'expanded' => false,
409            'classes' => [ 'mw-prefs-tabs-wrapper' ],
410            'content' => $indexLayout
411        ] );
412
413        return $form;
414    }
415}