Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialExpandTemplates
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 7
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
90
 onSubmitInput
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 makeForm
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
2
 makeOutput
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 showHtmlPreview
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Html\Html;
10use MediaWiki\HTMLForm\HTMLForm;
11use MediaWiki\MainConfigNames;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Output\OutputPage;
14use MediaWiki\Parser\Parser;
15use MediaWiki\Parser\ParserFactory;
16use MediaWiki\Parser\ParserOptions;
17use MediaWiki\Parser\ParserOutput;
18use MediaWiki\SpecialPage\SpecialPage;
19use MediaWiki\Status\Status;
20use MediaWiki\Tidy\TidyDriverBase;
21use MediaWiki\Title\Title;
22use MediaWiki\User\Options\UserOptionsLookup;
23
24/**
25 * A special page to enter wikitext and expands its templates, parser functions,
26 * and variables, allowing easier debugging of these.
27 *
28 * @ingroup SpecialPage
29 * @ingroup Parser
30 */
31class SpecialExpandTemplates extends SpecialPage {
32
33    /** @var int Maximum size in bytes to include. 50 MB allows fixing those huge pages */
34    private const MAX_INCLUDE_SIZE = 50_000_000;
35
36    private ParserFactory $parserFactory;
37    private UserOptionsLookup $userOptionsLookup;
38    private TidyDriverBase $tidy;
39
40    public function __construct(
41        ParserFactory $parserFactory,
42        UserOptionsLookup $userOptionsLookup,
43        TidyDriverBase $tidy
44    ) {
45        parent::__construct( 'ExpandTemplates' );
46        $this->parserFactory = $parserFactory;
47        $this->userOptionsLookup = $userOptionsLookup;
48        $this->tidy = $tidy;
49    }
50
51    /**
52     * Show the special page
53     * @param string|null $subpage
54     */
55    public function execute( $subpage ) {
56        $this->setHeaders();
57        $this->addHelpLink( 'Help:ExpandTemplates' );
58
59        $request = $this->getRequest();
60        $input = $request->getText( 'wpInput' );
61
62        if ( $input !== '' ) {
63            $removeComments = $request->getBool( 'wpRemoveComments', false );
64            $removeNowiki = $request->getBool( 'wpRemoveNowiki', false );
65            $generateXML = $request->getBool( 'wpGenerateXml' );
66            $generateRawHtml = $request->getBool( 'wpGenerateRawHtml' );
67
68            $options = ParserOptions::newFromContext( $this->getContext() );
69            $options->setRemoveComments( $removeComments );
70            $options->setMaxIncludeSize( self::MAX_INCLUDE_SIZE );
71            $options->setSuppressSectionEditLinks();
72
73            $titleStr = $request->getText( 'wpContextTitle' );
74            $title = Title::newFromText( $titleStr );
75            if ( !$title ) {
76                $title = $this->getPageTitle();
77                $options->setTargetLanguage( $this->getContentLanguage() );
78            }
79
80            $parser = $this->parserFactory->getInstance();
81            if ( $generateXML ) {
82                $parser->startExternalParse( $title, $options, Parser::OT_PREPROCESS );
83                $dom = $parser->preprocessToDom( $input );
84
85                if ( method_exists( $dom, 'saveXML' ) ) {
86                    // @phan-suppress-next-line PhanUndeclaredMethod
87                    $xml = $dom->saveXML();
88                } else {
89                    // @phan-suppress-next-line PhanUndeclaredMethod
90                    $xml = $dom->__toString();
91                }
92            }
93
94            $output = $parser->preprocess( $input, $title, $options );
95            $this->makeForm();
96
97            $out = $this->getOutput();
98            if ( $generateXML ) {
99                // @phan-suppress-next-line PhanPossiblyUndeclaredVariable xml is set when used
100                $out->addHTML( $this->makeOutput( $xml, 'expand_templates_xml_output' ) );
101            }
102
103            $tmp = $this->makeOutput( $output );
104
105            if ( $removeNowiki ) {
106                $tmp = preg_replace(
107                    [ '_&lt;nowiki&gt;_', '_&lt;/nowiki&gt;_', '_&lt;nowiki */&gt;_' ],
108                    '',
109                    $tmp
110                );
111            }
112
113            $tmp = $this->tidy->tidy( $tmp );
114
115            $out->addHTML( $tmp );
116
117            $pout = $parser->parse( $output, $title, $options );
118            // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
119            $rawhtml = MediaWikiServices::getInstance()->getDefaultOutputPipeline()
120                ->run( $pout, $options, [] )->getContentHolderText();
121            if ( $generateRawHtml && $rawhtml !== '' ) {
122                $out->addHTML( $this->makeOutput( $rawhtml, 'expand_templates_html_output' ) );
123            }
124
125            $this->showHtmlPreview( $title, $pout, $options, $out );
126        } else {
127            $this->makeForm();
128        }
129    }
130
131    /**
132     * Callback for the HTMLForm used in self::makeForm.
133     * Checks, if the input was given, and if not, returns a fatal Status
134     * object with an error message.
135     *
136     * @param array $values The values submitted to the HTMLForm
137     * @return Status
138     */
139    private function onSubmitInput( array $values ) {
140        $status = Status::newGood();
141        if ( $values['Input'] === '' ) {
142            $status = Status::newFatal( 'expand_templates_input_missing' );
143        }
144        return $status;
145    }
146
147    /**
148     * Generate a form allowing users to enter information
149     */
150    private function makeForm() {
151        $fields = [
152            'Input' => [
153                'type' => 'textarea',
154                'label' => $this->msg( 'expand_templates_input' )->text(),
155                'rows' => 10,
156                'id' => 'input',
157                'useeditfont' => true,
158                'required' => true,
159                'autofocus' => true,
160            ],
161            'ContextTitle' => [
162                'type' => 'text',
163                'label' => $this->msg( 'expand_templates_title' )->plain(),
164                'id' => 'contexttitle',
165                'size' => 60,
166            ],
167            'RemoveComments' => [
168                'type' => 'check',
169                'label' => $this->msg( 'expand_templates_remove_comments' )->text(),
170                'id' => 'removecomments',
171                'default' => true,
172            ],
173            'RemoveNowiki' => [
174                'type' => 'check',
175                'label' => $this->msg( 'expand_templates_remove_nowiki' )->text(),
176                'id' => 'removenowiki',
177            ],
178            'GenerateXml' => [
179                'type' => 'check',
180                'label' => $this->msg( 'expand_templates_generate_xml' )->text(),
181                'id' => 'generate_xml',
182            ],
183            'GenerateRawHtml' => [
184                'type' => 'check',
185                'label' => $this->msg( 'expand_templates_generate_rawhtml' )->text(),
186                'id' => 'generate_rawhtml',
187            ],
188        ];
189
190        $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
191        $form
192            ->setSubmitTextMsg( 'expand_templates_ok' )
193            ->setWrapperLegendMsg( 'expandtemplates' )
194            ->setHeaderHtml( $this->msg( 'expand_templates_intro' )->parse() )
195            ->setSubmitCallback( $this->onSubmitInput( ... ) )
196            ->showAlways();
197    }
198
199    /**
200     * Generate a nice little box with a heading for output
201     *
202     * @param string $output Wiki text output
203     * @param string $heading
204     * @return string
205     */
206    private function makeOutput( $output, $heading = 'expand_templates_output' ) {
207        $out = "<h2>" . $this->msg( $heading )->escaped() . "</h2>\n";
208        $out .= Html::textarea(
209            'output',
210            $output,
211            [
212                // #output is used by CodeMirror (T384148)
213                'id' => $heading === 'expand_templates_output' ? 'output' : $heading,
214                'cols' => 10,
215                'rows' => 10,
216                'readonly' => 'readonly',
217                'class' => 'mw-editfont-' . $this->userOptionsLookup->getOption( $this->getUser(), 'editfont' )
218            ]
219        );
220
221        return $out;
222    }
223
224    /**
225     * Wraps the provided html code in a div and outputs it to the page
226     *
227     * @param Title $title
228     * @param ParserOutput $pout
229     * @param ParserOptions $popts
230     * @param OutputPage $out
231     */
232    private function showHtmlPreview( Title $title, ParserOutput $pout, ParserOptions $popts, OutputPage $out ) {
233        $out->addHTML( "<h2>" . $this->msg( 'expand_templates_preview' )->escaped() . "</h2>\n" );
234
235        if ( $this->getConfig()->get( MainConfigNames::RawHtml ) ) {
236            $request = $this->getRequest();
237            $user = $this->getUser();
238
239            // To prevent cross-site scripting attacks, don't show the preview if raw HTML is
240            // allowed and a valid edit token is not provided (T73111). However, MediaWiki
241            // does not currently provide logged-out users with CSRF protection; in that case,
242            // do not show the preview unless anonymous editing is allowed.
243            if ( $user->isAnon() && !$this->getAuthority()->isAllowed( 'edit' ) ) {
244                $error = [ 'expand_templates_preview_fail_html_anon' ];
245            } elseif ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ), '', $request ) ) {
246                $error = [ 'expand_templates_preview_fail_html' ];
247            } else {
248                $error = false;
249            }
250
251            if ( $error ) {
252                $out->addHTML(
253                    Html::errorBox(
254                        $out->msg( $error )->parse(),
255                        '',
256                        'previewnote'
257                    )
258                );
259                return;
260            }
261        }
262
263        $out->addParserOutputContent( $pout, $popts );
264        $out->addCategoryLinks( $pout->getCategoryMap() );
265    }
266
267    /** @inheritDoc */
268    protected function getGroupName() {
269        return 'wiki';
270    }
271}
272
273/** @deprecated class alias since 1.41 */
274class_alias( SpecialExpandTemplates::class, 'SpecialExpandTemplates' );