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