1<?php 2/** 3 * Implements Special:ExpandTemplates 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup SpecialPage 22 */ 23 24use MediaWiki\Tidy\TidyDriverBase; 25use MediaWiki\User\UserOptionsLookup; 26 27/** 28 * A special page that expands submitted templates, parser functions, 29 * and variables, allowing easier debugging of these. 30 * 31 * @ingroup SpecialPage 32 */ 33class SpecialExpandTemplates extends SpecialPage { 34 35 /** @var bool Whether or not to show the XML parse tree */ 36 protected $generateXML; 37 38 /** @var bool Whether or not to show the raw HTML code */ 39 protected $generateRawHtml; 40 41 /** @var bool Whether or not to remove comments in the expanded wikitext */ 42 protected $removeComments; 43 44 /** @var bool Whether or not to remove <nowiki> tags in the expanded wikitext */ 45 protected $removeNowiki; 46 47 /** @var int Maximum size in bytes to include. 50MB allows fixing those huge pages */ 48 private const MAX_INCLUDE_SIZE = 50000000; 49 50 /** @var Parser */ 51 private $parser; 52 53 /** @var UserOptionsLookup */ 54 private $userOptionsLookup; 55 56 /** @var TidyDriverBase */ 57 private $tidy; 58 59 /** 60 * @param Parser $parser 61 * @param UserOptionsLookup $userOptionsLookup 62 * @param TidyDriverBase $tidy 63 */ 64 public function __construct( 65 Parser $parser, 66 UserOptionsLookup $userOptionsLookup, 67 TidyDriverBase $tidy 68 ) { 69 parent::__construct( 'ExpandTemplates' ); 70 $this->parser = $parser; 71 $this->userOptionsLookup = $userOptionsLookup; 72 $this->tidy = $tidy; 73 } 74 75 /** 76 * Show the special page 77 * @param string|null $subpage 78 */ 79 public function execute( $subpage ) { 80 $this->setHeaders(); 81 $this->addHelpLink( 'Help:ExpandTemplates' ); 82 83 $request = $this->getRequest(); 84 $titleStr = $request->getText( 'wpContextTitle' ); 85 $title = Title::newFromText( $titleStr ); 86 87 if ( !$title ) { 88 $title = $this->getPageTitle(); 89 } 90 $input = $request->getText( 'wpInput' ); 91 $this->generateXML = $request->getBool( 'wpGenerateXml' ); 92 $this->generateRawHtml = $request->getBool( 'wpGenerateRawHtml' ); 93 94 if ( strlen( $input ) ) { 95 $this->removeComments = $request->getBool( 'wpRemoveComments', false ); 96 $this->removeNowiki = $request->getBool( 'wpRemoveNowiki', false ); 97 $options = ParserOptions::newFromContext( $this->getContext() ); 98 $options->setRemoveComments( $this->removeComments ); 99 $options->setMaxIncludeSize( self::MAX_INCLUDE_SIZE ); 100 101 if ( $this->generateXML ) { 102 $this->parser->startExternalParse( $title, $options, Parser::OT_PREPROCESS ); 103 $dom = $this->parser->preprocessToDom( $input ); 104 105 if ( method_exists( $dom, 'saveXML' ) ) { 106 // @phan-suppress-next-line PhanUndeclaredMethod 107 $xml = $dom->saveXML(); 108 } else { 109 // @phan-suppress-next-line PhanUndeclaredMethod 110 $xml = $dom->__toString(); 111 } 112 } 113 114 $output = $this->parser->preprocess( $input, $title, $options ); 115 } else { 116 $this->removeComments = $request->getBool( 'wpRemoveComments', true ); 117 $this->removeNowiki = $request->getBool( 'wpRemoveNowiki', false ); 118 $output = false; 119 } 120 121 $out = $this->getOutput(); 122 123 $this->makeForm( $titleStr, $input ); 124 125 if ( $output !== false ) { 126 if ( $this->generateXML && strlen( $output ) > 0 ) { 127 $out->addHTML( $this->makeOutput( $xml, 'expand_templates_xml_output' ) ); 128 } 129 130 $tmp = $this->makeOutput( $output ); 131 132 if ( $this->removeNowiki ) { 133 $tmp = preg_replace( 134 [ '_<nowiki>_', '_</nowiki>_', '_<nowiki */>_' ], 135 '', 136 $tmp 137 ); 138 } 139 140 $config = $this->getConfig(); 141 142 $tmp = $this->tidy->tidy( $tmp ); 143 144 $out->addHTML( $tmp ); 145 146 $pout = $this->generateHtml( $title, $output ); 147 $rawhtml = $pout->getText(); 148 if ( $this->generateRawHtml && strlen( $rawhtml ) > 0 ) { 149 // @phan-suppress-next-line SecurityCheck-DoubleEscaped Wanted here to display the html 150 $out->addHTML( $this->makeOutput( $rawhtml, 'expand_templates_html_output' ) ); 151 } 152 153 $this->showHtmlPreview( $title, $pout, $out ); 154 } 155 } 156 157 /** 158 * Callback for the HTMLForm used in self::makeForm. 159 * Checks, if the input was given, and if not, returns a fatal Status 160 * object with an error message. 161 * 162 * @param array $values The values submitted to the HTMLForm 163 * @return Status 164 */ 165 public function onSubmitInput( array $values ) { 166 $status = Status::newGood(); 167 if ( !strlen( $values['input'] ) ) { 168 $status = Status::newFatal( 'expand_templates_input_missing' ); 169 } 170 return $status; 171 } 172 173 /** 174 * Generate a form allowing users to enter information 175 * 176 * @param string $title Value for context title field 177 * @param string $input Value for input textbox 178 */ 179 private function makeForm( $title, $input ) { 180 $fields = [ 181 'contexttitle' => [ 182 'type' => 'text', 183 'label' => $this->msg( 'expand_templates_title' )->plain(), 184 'name' => 'wpContextTitle', 185 'id' => 'contexttitle', 186 'size' => 60, 187 'default' => $title, 188 'autofocus' => true, 189 ], 190 'input' => [ 191 'type' => 'textarea', 192 'name' => 'wpInput', 193 'label' => $this->msg( 'expand_templates_input' )->text(), 194 'rows' => 10, 195 'default' => $input, 196 'id' => 'input', 197 'useeditfont' => true, 198 ], 199 'removecomments' => [ 200 'type' => 'check', 201 'label' => $this->msg( 'expand_templates_remove_comments' )->text(), 202 'name' => 'wpRemoveComments', 203 'id' => 'removecomments', 204 'default' => $this->removeComments, 205 ], 206 'removenowiki' => [ 207 'type' => 'check', 208 'label' => $this->msg( 'expand_templates_remove_nowiki' )->text(), 209 'name' => 'wpRemoveNowiki', 210 'id' => 'removenowiki', 211 'default' => $this->removeNowiki, 212 ], 213 'generate_xml' => [ 214 'type' => 'check', 215 'label' => $this->msg( 'expand_templates_generate_xml' )->text(), 216 'name' => 'wpGenerateXml', 217 'id' => 'generate_xml', 218 'default' => $this->generateXML, 219 ], 220 'generate_rawhtml' => [ 221 'type' => 'check', 222 'label' => $this->msg( 'expand_templates_generate_rawhtml' )->text(), 223 'name' => 'wpGenerateRawHtml', 224 'id' => 'generate_rawhtml', 225 'default' => $this->generateRawHtml, 226 ], 227 ]; 228 229 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); 230 $form 231 ->setSubmitTextMsg( 'expand_templates_ok' ) 232 ->setWrapperLegendMsg( 'expandtemplates' ) 233 ->setHeaderText( $this->msg( 'expand_templates_intro' )->parse() ) 234 ->setSubmitCallback( [ $this, 'onSubmitInput' ] ) 235 ->showAlways(); 236 } 237 238 /** 239 * Generate a nice little box with a heading for output 240 * 241 * @param string $output Wiki text output 242 * @param string $heading 243 * @return string 244 */ 245 private function makeOutput( $output, $heading = 'expand_templates_output' ) { 246 $out = "<h2>" . $this->msg( $heading )->escaped() . "</h2>\n"; 247 $out .= Xml::textarea( 248 'output', 249 $output, 250 10, 251 10, 252 [ 253 'id' => 'output', 254 'readonly' => 'readonly', 255 'class' => 'mw-editfont-' . $this->userOptionsLookup->getOption( $this->getUser(), 'editfont' ) 256 ] 257 ); 258 259 return $out; 260 } 261 262 /** 263 * Renders the supplied wikitext as html 264 * 265 * @param Title $title 266 * @param string $text 267 * @return ParserOutput 268 */ 269 private function generateHtml( Title $title, $text ) { 270 $popts = ParserOptions::newFromContext( $this->getContext() ); 271 $popts->setTargetLanguage( $title->getPageLanguage() ); 272 return $this->parser->parse( $text, $title, $popts ); 273 } 274 275 /** 276 * Wraps the provided html code in a div and outputs it to the page 277 * 278 * @param Title $title 279 * @param ParserOutput $pout 280 * @param OutputPage $out 281 */ 282 private function showHtmlPreview( Title $title, ParserOutput $pout, OutputPage $out ) { 283 $lang = $title->getPageViewLanguage(); 284 $out->addHTML( "<h2>" . $this->msg( 'expand_templates_preview' )->escaped() . "</h2>\n" ); 285 286 if ( $this->getConfig()->get( 'RawHtml' ) ) { 287 $request = $this->getRequest(); 288 $user = $this->getUser(); 289 290 // To prevent cross-site scripting attacks, don't show the preview if raw HTML is 291 // allowed and a valid edit token is not provided (T73111). However, MediaWiki 292 // does not currently provide logged-out users with CSRF protection; in that case, 293 // do not show the preview unless anonymous editing is allowed. 294 if ( $user->isAnon() && !$this->getAuthority()->isAllowed( 'edit' ) ) { 295 $error = [ 'expand_templates_preview_fail_html_anon' ]; 296 } elseif ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ), '', $request ) ) { 297 $error = [ 'expand_templates_preview_fail_html' ]; 298 } else { 299 $error = false; 300 } 301 302 if ( $error ) { 303 $out->wrapWikiMsg( "<div class='previewnote errorbox'>\n$1\n</div>", $error ); 304 return; 305 } 306 } 307 308 $out->addHTML( Html::openElement( 'div', [ 309 'class' => 'mw-content-' . $lang->getDir(), 310 'dir' => $lang->getDir(), 311 'lang' => $lang->getHtmlCode(), 312 ] ) ); 313 $out->addParserOutputContent( $pout ); 314 $out->addHTML( Html::closeElement( 'div' ) ); 315 $out->setCategoryLinks( $pout->getCategories() ); 316 } 317 318 protected function getGroupName() { 319 return 'wiki'; 320 } 321} 322