Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 358
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebInstallerOptions
0.00% covered (danger)
0.00%
0 / 358
0.00% covered (danger)
0.00%
0 / 12
3660
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 addPersonalizationOptions
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 addModeOptions
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 addEmailOptions
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
6
 addSkinOptions
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 addExtensionOptions
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
240
 addFileOptions
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
6
 addAdvancedOptions
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 makeScreenshotsLink
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 makeMoreInfoLink
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 submitSkins
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 submit
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
420
1<?php
2
3/**
4 * @license GPL-2.0-or-later
5 * @file
6 * @ingroup Installer
7 */
8
9namespace MediaWiki\Installer;
10
11use MediaWiki\Context\RequestContext;
12use MediaWiki\Html\Html;
13use MediaWiki\Specials\SpecialVersion;
14use Wikimedia\IPUtils;
15
16class WebInstallerOptions extends WebInstallerPage {
17
18    /**
19     * @return string|null
20     */
21    public function execute() {
22        if ( $this->getVar( '_SkipOptional' ) == 'skip' ) {
23            $this->submitSkins();
24            return 'skip';
25        }
26        if ( $this->parent->request->wasPosted() && $this->submit() ) {
27            return 'continue';
28        }
29
30        $this->startForm();
31        $this->addModeOptions();
32        $this->addEmailOptions();
33        $this->addSkinOptions();
34        $this->addExtensionOptions();
35        $this->addFileOptions();
36        $this->addPersonalizationOptions();
37        $this->addAdvancedOptions();
38        $this->endForm();
39
40        return null;
41    }
42
43    private function addPersonalizationOptions() {
44        $parent = $this->parent;
45        $this->addHTML(
46            $this->getFieldsetStart( 'config-personalization-settings' ) .
47            Html::rawElement( 'div', [
48                'class' => 'config-drag-drop'
49            ], wfMessage( 'config-logo-summary' )->parse() ) .
50            Html::openElement( 'div', [
51                'class' => 'config-personalization-options'
52            ] ) .
53            Html::hidden( 'config_LogoSiteName', $this->getVar( 'wgSitename' ) ) .
54            $parent->getTextBox( [
55                'var' => '_LogoIcon',
56                // Single quotes are intentional, LocalSettingsGenerator must output this unescaped.
57                'value' => '$wgResourceBasePath/resources/assets/change-your-logo.svg',
58                'label' => 'config-logo-icon',
59                'attribs' => [ 'dir' => 'ltr' ],
60                'help' => $parent->getHelpBox( 'config-logo-icon-help' )
61            ] ) .
62            $parent->getTextBox( [
63                'var' => '_LogoWordmark',
64                'label' => 'config-logo-wordmark',
65                'attribs' => [ 'dir' => 'ltr' ],
66                'help' => $parent->getHelpBox( 'config-logo-wordmark-help' )
67            ] ) .
68            $parent->getTextBox( [
69                'var' => '_LogoTagline',
70                'label' => 'config-logo-tagline',
71                'attribs' => [ 'dir' => 'ltr' ],
72                'help' => $parent->getHelpBox( 'config-logo-tagline-help' )
73            ] ) .
74            $parent->getTextBox( [
75                'var' => '_Logo1x',
76                'label' => 'config-logo-sidebar',
77                'attribs' => [ 'dir' => 'ltr' ],
78                'help' => $parent->getHelpBox( 'config-logo-sidebar-help' )
79            ] ) .
80            Html::openElement( 'div', [
81                'class' => 'logo-preview-area',
82                'data-main-page' => wfMessage( 'config-logo-preview-main' ),
83                'data-filedrop' => wfMessage( 'config-logo-filedrop' )
84            ] ) .
85            Html::closeElement( 'div' ) .
86            Html::closeElement( 'div' ) .
87            $this->getFieldsetEnd()
88        );
89    }
90
91    /**
92     * Wiki mode - user rights and copyright model.
93     */
94    private function addModeOptions(): void {
95        $this->addHTML(
96            # User Rights
97            // getRadioSet() builds a set of labeled radio buttons.
98            // For grep: The following messages are used as the item labels:
99            // config-profile-wiki, config-profile-no-anon, config-profile-fishbowl, config-profile-private
100            $this->parent->getRadioSet( [
101                'var' => '_RightsProfile',
102                'label' => 'config-profile',
103                'itemLabelPrefix' => 'config-profile-',
104                'values' => array_keys( $this->parent->rightsProfiles ),
105            ] ) .
106            $this->parent->getInfoBox( wfMessage( 'config-profile-help' )->plain() ) .
107
108            # Licensing
109            // getRadioSet() builds a set of labeled radio buttons.
110            // For grep: The following messages are used as the item labels:
111            // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
112            // config-license-cc-0, config-license-pd, config-license-gfdl,
113            // config-license-none
114            $this->parent->getRadioSet( [
115                'var' => '_LicenseCode',
116                'label' => 'config-license',
117                'itemLabelPrefix' => 'config-license-',
118                'values' => array_keys( $this->parent->licenses ),
119                'commonAttribs' => [ 'class' => 'licenseRadio' ],
120            ] ) .
121            $this->parent->getHelpBox( 'config-license-help' )
122        );
123    }
124
125    /**
126     * User email options.
127     */
128    private function addEmailOptions(): void {
129        $emailwrapperStyle = $this->getVar( 'wgEnableEmail' ) ? '' : 'display: none';
130        $this->addHTML(
131            $this->getFieldsetStart( 'config-email-settings' ) .
132            $this->parent->getCheckBox( [
133                'var' => 'wgEnableEmail',
134                'label' => 'config-enable-email',
135                'attribs' => [ 'class' => 'showHideRadio', 'rel' => 'emailwrapper' ],
136            ] ) .
137            $this->parent->getHelpBox( 'config-enable-email-help' ) .
138            "<div id=\"emailwrapper\" style=\"$emailwrapperStyle\">" .
139            $this->parent->getTextBox( [
140                'var' => 'wgPasswordSender',
141                'label' => 'config-email-sender'
142            ] ) .
143            $this->parent->getHelpBox( 'config-email-sender-help' ) .
144            $this->parent->getCheckBox( [
145                'var' => 'wgEnableUserEmail',
146                'label' => 'config-email-user',
147            ] ) .
148            $this->parent->getHelpBox( 'config-email-user-help' ) .
149            $this->parent->getCheckBox( [
150                'var' => 'wgEnotifUserTalk',
151                'label' => 'config-email-usertalk',
152            ] ) .
153            $this->parent->getHelpBox( 'config-email-usertalk-help' ) .
154            $this->parent->getCheckBox( [
155                'var' => 'wgEnotifWatchlist',
156                'label' => 'config-email-watchlist',
157            ] ) .
158            $this->parent->getHelpBox( 'config-email-watchlist-help' ) .
159            $this->parent->getCheckBox( [
160                'var' => 'wgEmailAuthentication',
161                'label' => 'config-email-auth',
162            ] ) .
163            $this->parent->getHelpBox( 'config-email-auth-help' ) .
164            "</div>" .
165            $this->getFieldsetEnd()
166        );
167    }
168
169    /**
170     * Opt-in for bundled skins.
171     */
172    private function addSkinOptions(): void {
173        $skins = $this->parent->findExtensions( 'skins' )->value;
174        '@phan-var array[] $skins';
175        $skinHtml = $this->getFieldsetStart( 'config-skins' );
176
177        $skinNames = array_map( 'strtolower', array_keys( $skins ) );
178        $chosenSkinName = $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
179
180        if ( $skins ) {
181            $radioButtons = $this->parent->getRadioElements( [
182                'var' => 'wgDefaultSkin',
183                'itemLabels' => array_fill_keys( $skinNames, 'config-skins-use-as-default' ),
184                'values' => $skinNames,
185                'value' => $chosenSkinName,
186            ] );
187
188            foreach ( $skins as $skin => $info ) {
189                if ( isset( $info['screenshots'] ) ) {
190                    $screenshotText = $this->makeScreenshotsLink( $skin, $info['screenshots'] );
191                } else {
192                    $screenshotText = htmlspecialchars( $skin );
193                }
194                $skinHtml .=
195                    '<div class="config-skins-item">' .
196                    $this->parent->getCheckBox( [
197                        'var' => "skin-$skin",
198                        'rawtext' => $screenshotText . $this->makeMoreInfoLink( $info ),
199                        'value' => $this->getVar( "skin-$skin", true ), // all found skins enabled by default
200                    ] ) .
201                    '<div class="config-skins-use-as-default">' . $radioButtons[strtolower( $skin )] . '</div>' .
202                    '</div>';
203            }
204        } else {
205            $skinHtml .=
206                Html::warningBox( wfMessage( 'config-skins-missing' )->parse(), 'config-warning-box' ) .
207                Html::hidden( 'config_wgDefaultSkin', $chosenSkinName );
208        }
209
210        $skinHtml .= $this->parent->getHelpBox( 'config-skins-help' ) .
211            $this->getFieldsetEnd();
212        $this->addHTML( $skinHtml );
213    }
214
215    /**
216     * Opt-in for bundled extensions.
217     */
218    private function addExtensionOptions(): void {
219        $lang = RequestContext::getMain()->getLanguage();
220
221        $extensions = $this->parent->findExtensions()->value;
222        '@phan-var array[] $extensions';
223        $dependencyMap = [];
224
225        if ( $extensions ) {
226            $extHtml = $this->getFieldsetStart( 'config-extensions' );
227
228            $extByType = [];
229            $types = SpecialVersion::getExtensionTypes();
230            // Sort by type first
231            foreach ( $extensions as $ext => $info ) {
232                if ( !isset( $info['type'] ) || !isset( $types[$info['type']] ) ) {
233                    // We let extensions normally define custom types, but
234                    // since we aren't loading extensions, we'll have to
235                    // categorize them under other
236                    $info['type'] = 'other';
237                }
238                $extByType[$info['type']][$ext] = $info;
239            }
240
241            foreach ( $types as $type => $message ) {
242                if ( !isset( $extByType[$type] ) ) {
243                    continue;
244                }
245                $extHtml .= Html::element( 'h2', [], $message );
246                foreach ( $extByType[$type] as $ext => $info ) {
247                    $attribs = [
248                        'data-name' => $ext,
249                        'class' => 'config-ext-input cdx-checkbox__input'
250                    ];
251                    $labelAttribs = [];
252                    if ( isset( $info['requires']['extensions'] ) ) {
253                        $dependencyMap[$ext]['extensions'] = $info['requires']['extensions'];
254                        $labelAttribs['class'] = 'mw-ext-with-dependencies';
255                    }
256                    if ( isset( $info['requires']['skins'] ) ) {
257                        $dependencyMap[$ext]['skins'] = $info['requires']['skins'];
258                        $labelAttribs['class'] = 'mw-ext-with-dependencies';
259                    }
260                    if ( isset( $dependencyMap[$ext] ) ) {
261                        $links = [];
262                        // For each dependency, link to the checkbox for each
263                        // extension/skin that is required
264                        if ( isset( $dependencyMap[$ext]['extensions'] ) ) {
265                            foreach ( $dependencyMap[$ext]['extensions'] as $name ) {
266                                $links[] = Html::element(
267                                    'a',
268                                    [ 'href' => "#config_ext-$name" ],
269                                    $name
270                                );
271                            }
272                        }
273                        if ( isset( $dependencyMap[$ext]['skins'] ) ) {
274                            // @phan-suppress-next-line PhanTypeMismatchForeach Phan internal bug
275                            foreach ( $dependencyMap[$ext]['skins'] as $name ) {
276                                $links[] = Html::element(
277                                    'a',
278                                    [ 'href' => "#config_skin-$name" ],
279                                    $name
280                                );
281                            }
282                        }
283
284                        $text = wfMessage( 'config-extensions-requires', $ext )
285                            ->rawParams( $lang->commaList( $links ) )
286                            ->escaped();
287                    } else {
288                        $text = htmlspecialchars( $ext );
289                    }
290                    $extHtml .= $this->parent->getCheckBox( [
291                        'var' => "ext-$ext",
292                        'rawtext' => $text . $this->makeMoreInfoLink( $info ),
293                        'attribs' => $attribs,
294                        'labelAttribs' => $labelAttribs,
295                    ] );
296                }
297            }
298
299            $extHtml .= $this->parent->getHelpBox( 'config-extensions-help' ) .
300                $this->getFieldsetEnd();
301            $this->addHTML( $extHtml );
302            // Push the dependency map to the client side
303            $this->addHTML( $this->inlineScript(
304                'var extDependencyMap = ' . Html::encodeJsVar( $dependencyMap )
305            ) );
306        }
307    }
308
309    /**
310     * Image and file upload options.
311     */
312    private function addFileOptions(): void {
313        // Having / in paths in Windows looks funny :)
314        $this->setVar( 'wgDeletedDirectory',
315            str_replace(
316                '/', DIRECTORY_SEPARATOR,
317                $this->getVar( 'wgDeletedDirectory' )
318            )
319        );
320
321        $uploadwrapperStyle = $this->getVar( 'wgEnableUploads' ) ? '' : 'display: none';
322        $this->addHTML(
323            # Uploading
324            $this->getFieldsetStart( 'config-upload-settings' ) .
325            $this->parent->getCheckBox( [
326                'var' => 'wgEnableUploads',
327                'label' => 'config-upload-enable',
328                'attribs' => [ 'class' => 'showHideRadio', 'rel' => 'uploadwrapper' ],
329                'help' => $this->parent->getHelpBox( 'config-upload-help' )
330            ] ) .
331            '<div id="uploadwrapper" style="' . $uploadwrapperStyle . '">' .
332            $this->parent->getTextBox( [
333                'var' => 'wgDeletedDirectory',
334                'label' => 'config-upload-deleted',
335                'attribs' => [ 'dir' => 'ltr' ],
336                'help' => $this->parent->getHelpBox( 'config-upload-deleted-help' )
337            ] ) .
338            '</div>'
339        );
340        $this->addHTML(
341            $this->parent->getCheckBox( [
342                'var' => 'wgUseInstantCommons',
343                'label' => 'config-instantcommons',
344                'help' => $this->parent->getHelpBox( 'config-instantcommons-help' )
345            ] ) .
346            $this->getFieldsetEnd()
347        );
348    }
349
350    /**
351     * System administration related options.
352     */
353    private function addAdvancedOptions(): void {
354        $caches = [ 'none' ];
355        $cachevalDefault = 'none';
356
357        if ( count( $this->getVar( '_Caches' ) ) ) {
358            // A CACHE_ACCEL implementation is available
359            $caches[] = 'accel';
360            $cachevalDefault = 'accel';
361        }
362        $caches[] = 'memcached';
363
364        // We'll hide/show this on demand when the value changes, see config.js.
365        $cacheval = $this->getVar( '_MainCacheType' );
366        if ( !$cacheval ) {
367            // We need to set a default here; but don't hardcode it
368            // or we lose it every time we reload the page for validation
369            // or going back!
370            $cacheval = $cachevalDefault;
371        }
372        $hidden = ( $cacheval == 'memcached' ) ? '' : 'display: none';
373        $this->addHTML(
374            # Advanced settings
375            $this->getFieldsetStart( 'config-advanced-settings' ) .
376            # Object cache settings
377            // getRadioSet() builds a set of labeled radio buttons.
378            // For grep: The following messages are used as the item labels:
379            // config-cache-none, config-cache-accel, config-cache-memcached
380            $this->parent->getRadioSet( [
381                'var' => '_MainCacheType',
382                'label' => 'config-cache-options',
383                'itemLabelPrefix' => 'config-cache-',
384                'values' => $caches,
385                'value' => $cacheval,
386            ] ) .
387            $this->parent->getHelpBox( 'config-cache-help' ) .
388            "<div id=\"config-memcachewrapper\" style=\"$hidden\">" .
389            $this->parent->getTextArea( [
390                'var' => '_MemCachedServers',
391                'label' => 'config-memcached-servers',
392                'help' => $this->parent->getHelpBox( 'config-memcached-help' )
393            ] ) .
394            '</div>' .
395            $this->getFieldsetEnd()
396        );
397    }
398
399    /**
400     * @param string $name
401     * @param array $screenshots
402     * @return string HTML
403     */
404    private function makeScreenshotsLink( $name, $screenshots ) {
405        $lang = RequestContext::getMain()->getLanguage();
406        if ( count( $screenshots ) > 1 ) {
407            $links = [];
408            $counter = 1;
409
410            foreach ( $screenshots as $shot ) {
411                $links[] = Html::element(
412                    'a',
413                    [ 'href' => $shot, 'target' => '_blank' ],
414                    $lang->formatNum( $counter++ )
415                );
416            }
417            return wfMessage( 'config-skins-screenshots', $name )
418                ->rawParams( $lang->commaList( $links ) )
419                ->escaped();
420        } else {
421            $link = Html::element(
422                'a',
423                [ 'href' => $screenshots[0], 'target' => '_blank' ],
424                wfMessage( 'config-screenshot' )->text()
425            );
426            return wfMessage( 'config-skins-screenshot', $name )->rawParams( $link )->escaped();
427        }
428    }
429
430    /**
431     * @param array $info
432     * @return string HTML
433     */
434    private function makeMoreInfoLink( $info ) {
435        if ( !isset( $info['url'] ) ) {
436            return '';
437        }
438        return ' ' . wfMessage( 'parentheses' )->rawParams(
439            Html::element(
440                'a',
441                [ 'href' => $info['url'] ],
442                wfMessage( 'config-ext-skins-more-info' )->text()
443            )
444        )->escaped();
445    }
446
447    /**
448     * If the user skips this installer page, we still need to set up the default skins, but ignore
449     * everything else.
450     *
451     * @return bool
452     */
453    public function submitSkins() {
454        $skins = array_keys( $this->parent->findExtensions( 'skins' )->value );
455        $this->parent->setVar( '_Skins', $skins );
456
457        if ( $skins ) {
458            $skinNames = array_map( 'strtolower', $skins );
459            $this->parent->setVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
460        }
461
462        return true;
463    }
464
465    /**
466     * @return bool
467     */
468    public function submit() {
469        $this->parent->setVarsFromRequest( [ '_RightsProfile', '_LicenseCode',
470            'wgEnableEmail', 'wgPasswordSender', 'wgEnableUploads',
471            '_Logo1x', '_LogoWordmark', '_LogoTagline', '_LogoIcon',
472            'wgEnableUserEmail', 'wgEnotifUserTalk', 'wgEnotifWatchlist',
473            'wgEmailAuthentication', '_MainCacheType', '_MemCachedServers',
474            'wgUseInstantCommons', 'wgDefaultSkin' ] );
475
476        $retVal = true;
477
478        if ( !array_key_exists( $this->getVar( '_RightsProfile' ), $this->parent->rightsProfiles ) ) {
479            $this->setVar( '_RightsProfile', array_key_first( $this->parent->rightsProfiles ) );
480        }
481
482        $code = $this->getVar( '_LicenseCode' );
483        if ( array_key_exists( $code, $this->parent->licenses ) ) {
484            // Messages:
485            // config-license-cc-by, config-license-cc-by-sa, config-license-cc-by-nc-sa,
486            // config-license-cc-0, config-license-pd, config-license-gfdl, config-license-none
487            $entry = $this->parent->licenses[$code];
488            $this->setVar( 'wgRightsText',
489                $entry['text'] ?? wfMessage( 'config-license-' . $code )->text() );
490            $this->setVar( 'wgRightsUrl', $entry['url'] );
491            $this->setVar( 'wgRightsIcon', $entry['icon'] );
492        } else {
493            $this->setVar( 'wgRightsText', '' );
494            $this->setVar( 'wgRightsUrl', '' );
495            $this->setVar( 'wgRightsIcon', '' );
496        }
497
498        $skinsAvailable = array_keys( $this->parent->findExtensions( 'skins' )->value );
499        $skinsToInstall = [];
500        foreach ( $skinsAvailable as $skin ) {
501            $this->parent->setVarsFromRequest( [ "skin-$skin" ] );
502            if ( $this->getVar( "skin-$skin" ) ) {
503                $skinsToInstall[] = $skin;
504            }
505        }
506        $this->parent->setVar( '_Skins', $skinsToInstall );
507
508        if ( !$skinsToInstall && $skinsAvailable ) {
509            $this->parent->showError( 'config-skins-must-enable-some' );
510            $retVal = false;
511        }
512        $defaultSkin = $this->getVar( 'wgDefaultSkin' );
513        $skinsToInstallLowercase = array_map( 'strtolower', $skinsToInstall );
514        if ( $skinsToInstall && !in_array( $defaultSkin, $skinsToInstallLowercase ) ) {
515            $this->parent->showError( 'config-skins-must-enable-default' );
516            $retVal = false;
517        }
518
519        $extsAvailable = array_keys( $this->parent->findExtensions()->value );
520        $extsToInstall = [];
521        foreach ( $extsAvailable as $ext ) {
522            $this->parent->setVarsFromRequest( [ "ext-$ext" ] );
523            if ( $this->getVar( "ext-$ext" ) ) {
524                $extsToInstall[] = $ext;
525            }
526        }
527        $this->parent->setVar( '_Extensions', $extsToInstall );
528
529        if ( $this->getVar( '_MainCacheType' ) == 'memcached' ) {
530            $memcServers = explode( "\n", $this->getVar( '_MemCachedServers' ) );
531            // FIXME: explode() will always result in an array of at least one string, even on null (when
532            // the string will be empty and you'll get a PHP warning), so this has never worked?
533            // @phan-suppress-next-line PhanImpossibleCondition
534            if ( !$memcServers ) {
535                $this->parent->showError( 'config-memcache-needservers' );
536                $retVal = false;
537            }
538
539            foreach ( $memcServers as $server ) {
540                $memcParts = explode( ":", $server, 2 );
541                if ( !isset( $memcParts[0] )
542                    || ( !IPUtils::isValid( $memcParts[0] )
543                        && ( gethostbyname( $memcParts[0] ) == $memcParts[0] ) )
544                ) {
545                    $this->parent->showError( 'config-memcache-badip', $memcParts[0] );
546                    $retVal = false;
547                } elseif ( !isset( $memcParts[1] ) ) {
548                    $this->parent->showError( 'config-memcache-noport', $memcParts[0] );
549                    $retVal = false;
550                } elseif ( $memcParts[1] < 1 || $memcParts[1] > 65535 ) {
551                    $this->parent->showError( 'config-memcache-badport', 1, 65535 );
552                    $retVal = false;
553                }
554            }
555        }
556
557        return $retVal;
558    }
559
560}