Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
24.00% covered (danger)
24.00%
24 / 100
5.56% covered (danger)
5.56%
1 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebInstallerOutput
24.00% covered (danger)
24.00%
24 / 100
5.56% covered (danger)
5.56%
1 / 18
347.01
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addHTML
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addWikiTextAsInterface
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addHTMLNoFlush
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 redirect
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 output
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getCSS
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 getCssUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flush
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHeadAttribs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 headerDone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 outputHeader
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 outputFooter
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 outputTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getJQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCodex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCSPNonce
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * @license GPL-2.0-or-later
5 * @file
6 * @ingroup Installer
7 */
8
9namespace MediaWiki\Installer;
10
11use LogicException;
12use MediaWiki\Context\RequestContext;
13use MediaWiki\Html\Html;
14use MediaWiki\Language\Language;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Request\FauxRequest;
17use MediaWiki\ResourceLoader as RL;
18use MediaWiki\ResourceLoader\ResourceLoader;
19
20/**
21 * Output class modelled on OutputPage.
22 *
23 * I've opted to use a distinct class rather than derive from OutputPage here in
24 * the interests of separation of concerns: if we used a subclass, there would be
25 * quite a lot of things you could do in OutputPage that would break the installer,
26 * that wouldn't be immediately obvious.
27 *
28 * @ingroup Installer
29 * @since 1.17
30 * @internal
31 */
32class WebInstallerOutput {
33
34    /**
35     * The WebInstaller object this WebInstallerOutput is used by.
36     *
37     * @var WebInstaller
38     */
39    public $parent;
40
41    /**
42     * Buffered contents that haven't been output yet
43     * @var string
44     */
45    private $contents = '';
46
47    /**
48     * Has the header been output?
49     * @var bool
50     */
51    private $headerDone = false;
52
53    /**
54     * @var string
55     */
56    public $redirectTarget;
57
58    public function __construct( WebInstaller $parent ) {
59        $this->parent = $parent;
60    }
61
62    /**
63     * @param string $html
64     */
65    public function addHTML( $html ) {
66        $this->contents .= $html;
67        $this->flush();
68    }
69
70    /**
71     * @param string $text
72     * @since 1.32
73     */
74    public function addWikiTextAsInterface( $text ) {
75        $this->addHTML( $this->parent->parse( $text ) );
76    }
77
78    /**
79     * @param string $html
80     */
81    public function addHTMLNoFlush( $html ) {
82        $this->contents .= $html;
83    }
84
85    /**
86     * @param string $url
87     */
88    public function redirect( $url ) {
89        if ( $this->headerDone ) {
90            throw new LogicException( __METHOD__ . ' called after sending headers' );
91        }
92        $this->redirectTarget = $url;
93    }
94
95    public function output() {
96        $this->flush();
97
98        if ( !$this->redirectTarget ) {
99            $this->outputFooter();
100        }
101    }
102
103    /**
104     * Get the stylesheet of the MediaWiki skin.
105     *
106     * @return string
107     */
108    public function getCSS() {
109        $resourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
110
111        $rlContext = new RL\Context( $resourceLoader, new FauxRequest( [
112            'debug' => 'true',
113            'lang' => $this->getLanguage()->getCode(),
114            'only' => 'styles',
115        ] ) );
116
117        $module = new RL\SkinModule( [
118            'features' => [
119                'elements',
120                'interface-message-box'
121            ],
122            'styles' => [
123                'mw-config/config.css',
124            ],
125        ] );
126        $module->setConfig( $resourceLoader->getConfig() );
127
128        // Based on MediaWiki\ResourceLoader\FileModule::getStyles, without the DB query
129        $styles = ResourceLoader::makeCombinedStyles(
130            $module->readStyleFiles(
131                $module->getStyleFiles( $rlContext ),
132                $rlContext
133            ),
134            $this->parent->request
135        );
136
137        return implode( "\n", $styles );
138    }
139
140    /**
141     * "<link>" to index.php?css=1 for the "<head>"
142     *
143     * @return string
144     */
145    private function getCssUrl() {
146        return Html::linkedStyle( $this->parent->getUrl( [ 'css' => 1 ] ) );
147    }
148
149    public function flush() {
150        if ( !$this->headerDone ) {
151            $this->outputHeader();
152        }
153        if ( !$this->redirectTarget && strlen( $this->contents ) ) {
154            echo $this->contents;
155            flush();
156            $this->contents = '';
157        }
158    }
159
160    /**
161     * @since 1.33
162     * @return Language
163     */
164    private function getLanguage() {
165        return RequestContext::getMain()->getLanguage();
166    }
167
168    /**
169     * @return string[]
170     */
171    public function getHeadAttribs() {
172        return [
173            'dir' => $this->getLanguage()->getDir(),
174            'lang' => $this->getLanguage()->getHtmlCode(),
175        ];
176    }
177
178    /**
179     * Get whether the header has been output
180     *
181     * @return bool
182     */
183    public function headerDone() {
184        return $this->headerDone;
185    }
186
187    public function outputHeader() {
188        $this->headerDone = true;
189        $this->parent->request->response()->header( 'Content-Type: text/html; charset=utf-8' );
190        $this->parent->request->response()->header( 'X-Frame-Options: DENY' );
191
192        $cspPolicy = "default-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none';" .
193            " script-src 'self' 'nonce-" . $this->getCSPNonce() . "';" .
194            " img-src 'self'; frame-src 'self'; base-uri 'none'";
195
196        $this->parent->request->response()->header( 'Content-Security-Policy: ' . $cspPolicy );
197
198        if ( $this->redirectTarget ) {
199            $this->parent->request->response()->header( 'Location: ' . $this->redirectTarget );
200
201            return;
202        }
203?>
204<?php echo Html::htmlHeader( $this->getHeadAttribs() ); ?>
205
206<head>
207    <meta name="robots" content="noindex, nofollow" />
208    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
209    <title><?php $this->outputTitle(); ?></title>
210    <?php echo $this->getCodex() . "\n"; ?>
211    <?php echo $this->getCssUrl() . "\n"; ?>
212    <?php echo $this->getJQuery() . "\n"; ?>
213    <?php echo Html::linkedScript( 'config.js' ) . "\n"; ?>
214</head>
215
216<?php echo Html::openElement( 'body', [ 'class' => $this->getLanguage()->getDir() ] ) . "\n"; ?>
217<div id="mw-page-base"></div>
218<div id="mw-head-base"></div>
219<div id="content" class="mw-body" role="main">
220<div id="bodyContent" class="mw-body-content">
221
222<h1><?php $this->outputTitle(); ?></h1>
223<?php
224    }
225
226    public function outputFooter() {
227?>
228
229</div></div>
230
231<aside id="mw-panel">
232    <div class="portal" id="p-logo">
233        <a href="https://www.mediawiki.org/" title="Main Page"></a>
234    </div>
235<?php
236        // @phpcs:disable Generic.WhiteSpace.ScopeIndent.IncorrectExact
237        $message = wfMessage( 'config-sidebar' )->plain();
238        // Section 1: External links
239        // @todo FIXME: Migrate to plain link label messages (T227297).
240        foreach ( explode( '----', $message ) as $section ) {
241            echo '<div class="portal"><div class="body">';
242            echo $this->parent->parse( $section, true );
243            echo '</div></div>';
244        }
245        // Section 2: Installer pages
246        echo '<div class="portal"><div class="body"><ul>';
247        foreach ( [
248            'config-sidebar-relnotes' => 'ReleaseNotes',
249            'config-sidebar-license' => 'Copying',
250            'config-sidebar-upgrade' => 'UpgradeDoc',
251        ] as $msgKey => $pageName ) {
252            echo $this->parent->makeLinkItem(
253                $this->parent->getDocUrl( $pageName ),
254                wfMessage( $msgKey )->text()
255            );
256        }
257        echo '</ul></div></div>';
258        // @phpcs:enable
259?>
260</aside>
261
262<?php
263        echo Html::closeElement( 'body' ) . Html::closeElement( 'html' );
264    }
265
266    public function outputTitle() {
267        echo wfMessage( 'config-title', MW_VERSION )->escaped();
268    }
269
270    /**
271     * @return string
272     */
273    public function getJQuery() {
274        return Html::linkedScript( "../resources/lib/jquery/jquery.js" );
275    }
276
277    /**
278     * @return string
279     */
280    public function getCodex() {
281        return Html::linkedStyle( "../resources/lib/codex/codex.style.css" );
282    }
283
284    /**
285     * Get the nonce for use with inline scripts
286     *
287     * @since 1.45
288     * @return string
289     */
290    public function getCSPNonce() {
291        static $nonce;
292        if ( $nonce === null ) {
293            // Spec says at least 16 bytes. Do 18 so it encodes evenly in base64
294            $nonce = base64_encode( random_bytes( 18 ) );
295        }
296        return $nonce;
297    }
298}