1<?php 2/** 3 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" 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 */ 22 23use MediaWiki\Content\IContentHandlerFactory; 24use MediaWiki\MediaWikiServices; 25use MediaWiki\Revision\RevisionRecord; 26use MediaWiki\Revision\SlotRecord; 27 28/** 29 * @ingroup API 30 */ 31class ApiParse extends ApiBase { 32 33 /** @var string|false|null */ 34 private $section = null; 35 36 /** @var Content */ 37 private $content = null; 38 39 /** @var Content */ 40 private $pstContent = null; 41 42 /** @var bool */ 43 private $contentIsDeleted = false, $contentIsSuppressed = false; 44 45 private function getPoolKey(): string { 46 $poolKey = WikiMap::getCurrentWikiDbDomain() . ':ApiParse:'; 47 if ( $this->getUser()->isAnon() ) { 48 $poolKey .= 'a:' . $this->getUser()->getName(); 49 } else { 50 $poolKey .= 'u:' . $this->getUser()->getId(); 51 } 52 return $poolKey; 53 } 54 55 private function getContentParserOutput( 56 Content $content, 57 Title $title, 58 $revId, 59 ParserOptions $popts 60 ) { 61 $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(), 62 [ 63 'doWork' => function () use ( $content, $title, $revId, $popts ) { 64 return $content->getParserOutput( $title, $revId, $popts ); 65 }, 66 'error' => function () { 67 $this->dieWithError( 'apierror-concurrency-limit' ); 68 }, 69 ] 70 ); 71 return $worker->execute(); 72 } 73 74 private function getPageParserOutput( 75 WikiPage $page, 76 $revId, 77 ParserOptions $popts, 78 bool $suppressCache 79 ) { 80 $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(), 81 [ 82 'doWork' => function () use ( $page, $revId, $popts, $suppressCache ) { 83 return $page->getParserOutput( $popts, $revId, $suppressCache ); 84 }, 85 'error' => function () { 86 $this->dieWithError( 'apierror-concurrency-limit' ); 87 }, 88 ] 89 ); 90 return $worker->execute(); 91 } 92 93 public function execute() { 94 // The data is hot but user-dependent, like page views, so we set vary cookies 95 $this->getMain()->setCacheMode( 'anon-public-user-private' ); 96 97 // Get parameters 98 $params = $this->extractRequestParams(); 99 100 // No easy way to say that text and title or revid are allowed together 101 // while the rest aren't, so just do it in three calls. 102 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' ); 103 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' ); 104 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'revid' ); 105 106 $text = $params['text']; 107 $title = $params['title']; 108 if ( $title === null ) { 109 $titleProvided = false; 110 // A title is needed for parsing, so arbitrarily choose one 111 $title = 'API'; 112 } else { 113 $titleProvided = true; 114 } 115 116 $page = $params['page']; 117 $pageid = $params['pageid']; 118 $oldid = $params['oldid']; 119 120 $model = $params['contentmodel']; 121 $format = $params['contentformat']; 122 123 $prop = array_flip( $params['prop'] ); 124 125 if ( isset( $params['section'] ) ) { 126 $this->section = $params['section']; 127 if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) { 128 $this->dieWithError( 'apierror-invalidsection' ); 129 } 130 } else { 131 $this->section = false; 132 } 133 134 // The parser needs $wgTitle to be set, apparently the 135 // $title parameter in Parser::parse isn't enough *sigh* 136 // TODO: Does this still need $wgTitle? 137 global $wgTitle; 138 139 $redirValues = null; 140 141 $needContent = isset( $prop['wikitext'] ) || 142 isset( $prop['parsetree'] ) || $params['generatexml']; 143 144 // Return result 145 $result = $this->getResult(); 146 147 $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); 148 if ( $oldid !== null || $pageid !== null || $page !== null ) { 149 if ( $this->section === 'new' ) { 150 $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' ); 151 } 152 if ( $oldid !== null ) { 153 // Don't use the parser cache 154 $rev = $revisionLookup->getRevisionById( $oldid ); 155 if ( !$rev ) { 156 $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] ); 157 } 158 159 $revLinkTarget = $rev->getPageAsLinkTarget(); 160 $this->checkTitleUserPermissions( $revLinkTarget, 'read' ); 161 162 if ( !$rev->audienceCan( 163 RevisionRecord::DELETED_TEXT, 164 RevisionRecord::FOR_THIS_USER, 165 $this->getUser() 166 ) ) { 167 $this->dieWithError( 168 [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ] 169 ); 170 } 171 172 $titleObj = Title::newFromLinkTarget( $revLinkTarget ); 173 $wgTitle = $titleObj; 174 $pageObj = WikiPage::factory( $titleObj ); 175 list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params ); 176 $p_result = $this->getParsedContent( 177 $pageObj, $popts, $suppressCache, $pageid, $rev, $needContent 178 ); 179 } else { // Not $oldid, but $pageid or $page 180 if ( $params['redirects'] ) { 181 $reqParams = [ 182 'redirects' => '', 183 ]; 184 $pageParams = []; 185 if ( $pageid !== null ) { 186 $reqParams['pageids'] = $pageid; 187 $pageParams['pageid'] = $pageid; 188 } else { // $page 189 $reqParams['titles'] = $page; 190 $pageParams['title'] = $page; 191 } 192 $req = new FauxRequest( $reqParams ); 193 $main = new ApiMain( $req ); 194 $pageSet = new ApiPageSet( $main ); 195 $pageSet->execute(); 196 $redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() ); 197 198 foreach ( $pageSet->getRedirectTitles() as $title ) { 199 $pageParams = [ 'title' => $title->getFullText() ]; 200 } 201 } elseif ( $pageid !== null ) { 202 $pageParams = [ 'pageid' => $pageid ]; 203 } else { // $page 204 $pageParams = [ 'title' => $page ]; 205 } 206 207 $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' ); 208 $titleObj = $pageObj->getTitle(); 209 if ( !$titleObj || !$titleObj->exists() ) { 210 $this->dieWithError( 'apierror-missingtitle' ); 211 } 212 213 $this->checkTitleUserPermissions( $titleObj, 'read' ); 214 $wgTitle = $titleObj; 215 216 if ( isset( $prop['revid'] ) ) { 217 $oldid = $pageObj->getLatest(); 218 } 219 220 list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params ); 221 $p_result = $this->getParsedContent( 222 $pageObj, $popts, $suppressCache, $pageid, null, $needContent 223 ); 224 } 225 } else { // Not $oldid, $pageid, $page. Hence based on $text 226 $titleObj = Title::newFromText( $title ); 227 if ( !$titleObj || $titleObj->isExternal() ) { 228 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] ); 229 } 230 $revid = $params['revid']; 231 if ( $revid !== null ) { 232 $rev = $revisionLookup->getRevisionById( $revid ); 233 if ( !$rev ) { 234 $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] ); 235 } 236 $pTitleObj = $titleObj; 237 $titleObj = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); 238 if ( $titleProvided ) { 239 if ( !$titleObj->equals( $pTitleObj ) ) { 240 $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(), 241 wfEscapeWikiText( $pTitleObj->getPrefixedText() ) ] ); 242 } 243 } else { 244 // Consider the title derived from the revid as having 245 // been provided. 246 $titleProvided = true; 247 } 248 } 249 $wgTitle = $titleObj; 250 if ( $titleObj->canExist() ) { 251 $pageObj = WikiPage::factory( $titleObj ); 252 } else { 253 // Do like MediaWiki::initializeArticle() 254 $article = Article::newFromTitle( $titleObj, $this->getContext() ); 255 $pageObj = $article->getPage(); 256 } 257 258 list( $popts, $reset ) = $this->makeParserOptions( $pageObj, $params ); 259 $textProvided = $text !== null; 260 261 if ( !$textProvided ) { 262 if ( $titleProvided && ( $prop || $params['generatexml'] ) ) { 263 if ( $revid !== null ) { 264 $this->addWarning( 'apiwarn-parse-revidwithouttext' ); 265 } else { 266 $this->addWarning( 'apiwarn-parse-titlewithouttext' ); 267 } 268 } 269 // Prevent warning from ContentHandler::makeContent() 270 $text = ''; 271 } 272 273 // If we are parsing text, do not use the content model of the default 274 // API title, but default to wikitext to keep BC. 275 if ( $textProvided && !$titleProvided && $model === null ) { 276 $model = CONTENT_MODEL_WIKITEXT; 277 $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] ); 278 } 279 280 try { 281 $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format ); 282 } catch ( MWContentSerializationException $ex ) { 283 $this->dieWithException( $ex, [ 284 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' ) 285 ] ); 286 } 287 288 if ( $this->section !== false ) { 289 if ( $this->section === 'new' ) { 290 // Insert the section title above the content. 291 if ( $params['sectiontitle'] !== null && $params['sectiontitle'] !== '' ) { 292 $this->content = $this->content->addSectionHeader( $params['sectiontitle'] ); 293 } 294 } else { 295 $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() ); 296 } 297 } 298 299 if ( $params['pst'] || $params['onlypst'] ) { 300 $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts ); 301 } 302 if ( $params['onlypst'] ) { 303 // Build a result and bail out 304 $result_array = []; 305 $result_array['text'] = $this->pstContent->serialize( $format ); 306 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; 307 if ( isset( $prop['wikitext'] ) ) { 308 $result_array['wikitext'] = $this->content->serialize( $format ); 309 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext'; 310 } 311 if ( $params['summary'] !== null || 312 ( $params['sectiontitle'] !== null && $this->section === 'new' ) 313 ) { 314 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params ); 315 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary'; 316 } 317 318 $result->addValue( null, $this->getModuleName(), $result_array ); 319 320 return; 321 } 322 323 // Not cached (save or load) 324 if ( $params['pst'] ) { 325 $p_result = $this->getContentParserOutput( $this->pstContent, $titleObj, $revid, $popts ); 326 } else { 327 $p_result = $this->getContentParserOutput( $this->content, $titleObj, $revid, $popts ); 328 } 329 } 330 331 $result_array = []; 332 333 $result_array['title'] = $titleObj->getPrefixedText(); 334 $result_array['pageid'] = $pageid ?: $pageObj->getId(); 335 if ( $this->contentIsDeleted ) { 336 $result_array['textdeleted'] = true; 337 } 338 if ( $this->contentIsSuppressed ) { 339 $result_array['textsuppressed'] = true; 340 } 341 342 if ( isset( $params['useskin'] ) ) { 343 $factory = MediaWikiServices::getInstance()->getSkinFactory(); 344 $skin = $factory->makeSkin( Skin::normalizeKey( $params['useskin'] ) ); 345 } else { 346 $skin = null; 347 } 348 349 $outputPage = null; 350 $context = null; 351 if ( $skin || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ) { 352 // Enabling the skin via 'useskin', 'headhtml', or 'categorieshtml' 353 // gets OutputPage and Skin involved, which (among others) applies 354 // these hooks: 355 // - ParserOutputHooks 356 // - Hook: LanguageLinks 357 // - Hook: OutputPageParserOutput 358 // - Hook: OutputPageMakeCategoryLinks 359 $context = new DerivativeContext( $this->getContext() ); 360 $context->setTitle( $titleObj ); 361 $context->setWikiPage( $pageObj ); 362 363 if ( $skin ) { 364 // Use the skin specified by 'useskin' 365 $context->setSkin( $skin ); 366 // Context clones the skin, refetch to stay in sync. (T166022) 367 $skin = $context->getSkin(); 368 } else { 369 // Make sure the context's skin refers to the context. Without this, 370 // $outputPage->getSkin()->getOutput() !== $outputPage which 371 // confuses some of the output. 372 $context->setSkin( $context->getSkin() ); 373 } 374 375 $outputPage = new OutputPage( $context ); 376 $outputPage->addParserOutputMetadata( $p_result ); 377 if ( $this->content ) { 378 $outputPage->addContentOverride( $titleObj, $this->content ); 379 } 380 $context->setOutput( $outputPage ); 381 382 if ( $skin ) { 383 // Based on OutputPage::headElement() 384 $skin->setupSkinUserCss( $outputPage ); 385 // Based on OutputPage::output() 386 $outputPage->loadSkinModules( $skin ); 387 } 388 389 $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage ); 390 } 391 392 if ( $oldid !== null ) { 393 $result_array['revid'] = (int)$oldid; 394 } 395 396 if ( $params['redirects'] && $redirValues !== null ) { 397 $result_array['redirects'] = $redirValues; 398 } 399 400 if ( isset( $prop['text'] ) ) { 401 $result_array['text'] = $p_result->getText( [ 402 'allowTOC' => !$params['disabletoc'], 403 'enableSectionEditLinks' => !$params['disableeditsection'], 404 'wrapperDivClass' => $params['wrapoutputclass'], 405 'deduplicateStyles' => !$params['disablestylededuplication'], 406 'skin' => $context ? $context->getSkin() : null, 407 ] ); 408 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; 409 } 410 411 if ( $params['summary'] !== null || 412 ( $params['sectiontitle'] !== null && $this->section === 'new' ) 413 ) { 414 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params ); 415 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary'; 416 } 417 418 if ( isset( $prop['langlinks'] ) ) { 419 if ( $skin ) { 420 $langlinks = $outputPage->getLanguageLinks(); 421 } else { 422 $langlinks = $p_result->getLanguageLinks(); 423 // The deprecated 'effectivelanglinks' option depredates OutputPage 424 // support via 'useskin'. If not already applied, then run just this 425 // one hook of OutputPage::addParserOutputMetadata here. 426 if ( $params['effectivelanglinks'] ) { 427 $linkFlags = []; 428 $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags ); 429 } 430 } 431 432 $result_array['langlinks'] = $this->formatLangLinks( $langlinks ); 433 } 434 if ( isset( $prop['categories'] ) ) { 435 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() ); 436 } 437 if ( isset( $prop['categorieshtml'] ) ) { 438 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories(); 439 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml'; 440 } 441 if ( isset( $prop['links'] ) ) { 442 $result_array['links'] = $this->formatLinks( $p_result->getLinks() ); 443 } 444 if ( isset( $prop['templates'] ) ) { 445 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() ); 446 } 447 if ( isset( $prop['images'] ) ) { 448 $result_array['images'] = array_keys( $p_result->getImages() ); 449 } 450 if ( isset( $prop['externallinks'] ) ) { 451 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() ); 452 } 453 if ( isset( $prop['sections'] ) ) { 454 $result_array['sections'] = $p_result->getSections(); 455 } 456 if ( isset( $prop['parsewarnings'] ) ) { 457 $result_array['parsewarnings'] = $p_result->getWarnings(); 458 } 459 460 if ( isset( $prop['displaytitle'] ) ) { 461 $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false 462 ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText(); 463 } 464 465 if ( isset( $prop['headitems'] ) ) { 466 if ( $skin ) { 467 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() ); 468 } else { 469 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() ); 470 } 471 } 472 473 if ( isset( $prop['headhtml'] ) ) { 474 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() ); 475 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml'; 476 } 477 478 if ( isset( $prop['modules'] ) ) { 479 if ( $skin ) { 480 $result_array['modules'] = $outputPage->getModules(); 481 // Deprecated since 1.32 (T188689) 482 $result_array['modulescripts'] = []; 483 $result_array['modulestyles'] = $outputPage->getModuleStyles(); 484 } else { 485 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) ); 486 // Deprecated since 1.32 (T188689) 487 $result_array['modulescripts'] = []; 488 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) ); 489 } 490 } 491 492 if ( isset( $prop['jsconfigvars'] ) ) { 493 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars(); 494 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars ); 495 } 496 497 if ( isset( $prop['encodedjsconfigvars'] ) ) { 498 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars(); 499 $result_array['encodedjsconfigvars'] = FormatJson::encode( 500 $jsconfigvars, 501 false, 502 FormatJson::ALL_OK 503 ); 504 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars'; 505 } 506 507 if ( isset( $prop['modules'] ) && 508 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) { 509 $this->addWarning( 'apiwarn-moduleswithoutvars' ); 510 } 511 512 if ( isset( $prop['indicators'] ) ) { 513 if ( $skin ) { 514 $result_array['indicators'] = (array)$outputPage->getIndicators(); 515 } else { 516 $result_array['indicators'] = (array)$p_result->getIndicators(); 517 } 518 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' ); 519 } 520 521 if ( isset( $prop['iwlinks'] ) ) { 522 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() ); 523 } 524 525 if ( isset( $prop['wikitext'] ) ) { 526 $result_array['wikitext'] = $this->content->serialize( $format ); 527 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext'; 528 if ( $this->pstContent !== null ) { 529 $result_array['psttext'] = $this->pstContent->serialize( $format ); 530 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext'; 531 } 532 } 533 if ( isset( $prop['properties'] ) ) { 534 $result_array['properties'] = (array)$p_result->getProperties(); 535 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' ); 536 } 537 538 if ( isset( $prop['limitreportdata'] ) ) { 539 $result_array['limitreportdata'] = 540 $this->formatLimitReportData( $p_result->getLimitReportData() ); 541 } 542 if ( isset( $prop['limitreporthtml'] ) ) { 543 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result ); 544 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml'; 545 } 546 547 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) { 548 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) { 549 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' ); 550 } 551 552 $parser = MediaWikiServices::getInstance()->getParser(); 553 $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS ); 554 // @phan-suppress-next-line PhanUndeclaredMethod 555 $xml = $parser->preprocessToDom( $this->content->getText() )->__toString(); 556 $result_array['parsetree'] = $xml; 557 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree'; 558 } 559 560 $result_mapping = [ 561 'redirects' => 'r', 562 'langlinks' => 'll', 563 'categories' => 'cl', 564 'links' => 'pl', 565 'templates' => 'tl', 566 'images' => 'img', 567 'externallinks' => 'el', 568 'iwlinks' => 'iw', 569 'sections' => 's', 570 'headitems' => 'hi', 571 'modules' => 'm', 572 'indicators' => 'ind', 573 'modulescripts' => 'm', 574 'modulestyles' => 'm', 575 'properties' => 'pp', 576 'limitreportdata' => 'lr', 577 'parsewarnings' => 'pw' 578 ]; 579 $this->setIndexedTagNames( $result_array, $result_mapping ); 580 $result->addValue( null, $this->getModuleName(), $result_array ); 581 } 582 583 /** 584 * Constructs a ParserOptions object 585 * 586 * @param WikiPage $pageObj 587 * @param array $params 588 * 589 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ] 590 */ 591 protected function makeParserOptions( WikiPage $pageObj, array $params ) { 592 $popts = $pageObj->makeParserOptions( $this->getContext() ); 593 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] ); 594 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] ); 595 $popts->setIsSectionPreview( $params['sectionpreview'] ); 596 597 if ( $params['wrapoutputclass'] !== '' ) { 598 $popts->setWrapOutputClass( $params['wrapoutputclass'] ); 599 } 600 601 $reset = null; 602 $suppressCache = false; 603 $this->getHookRunner()->onApiMakeParserOptions( $popts, $pageObj->getTitle(), 604 $params, $this, $reset, $suppressCache ); 605 606 // Force cache suppression when $popts aren't cacheable. 607 $suppressCache = $suppressCache || !$popts->isSafeToCache(); 608 609 return [ $popts, $reset, $suppressCache ]; 610 } 611 612 /** 613 * @param WikiPage $page 614 * @param ParserOptions $popts 615 * @param bool $suppressCache 616 * @param int $pageId 617 * @param RevisionRecord|null $rev 618 * @param bool $getContent 619 * @return ParserOutput 620 */ 621 private function getParsedContent( 622 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent 623 ) { 624 $revId = $rev ? $rev->getId() : null; 625 $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT ); 626 627 if ( $getContent || $this->section !== false || $isDeleted ) { 628 if ( $rev ) { 629 $this->content = $rev->getContent( 630 SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getUser() 631 ); 632 if ( !$this->content ) { 633 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] ); 634 } 635 } else { 636 $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getUser() ); 637 if ( !$this->content ) { 638 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] ); 639 } 640 } 641 $this->contentIsDeleted = $isDeleted; 642 $this->contentIsSuppressed = $rev && 643 $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED ); 644 } 645 646 if ( $this->section !== false ) { 647 $this->content = $this->getSectionContent( 648 $this->content, 649 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId ) 650 ); 651 return $this->getContentParserOutput( $this->content, $page->getTitle(), $revId, $popts ); 652 } 653 654 if ( $isDeleted ) { 655 // getParserOutput can't do revdeled revisions 656 657 $pout = $this->getContentParserOutput( $this->content, $page->getTitle(), $revId, $popts ); 658 } else { 659 // getParserOutput will save to Parser cache if able 660 $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache ); 661 } 662 if ( !$pout ) { 663 // @codeCoverageIgnoreStart 664 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); 665 // @codeCoverageIgnoreEnd 666 } 667 668 return $pout; 669 } 670 671 /** 672 * Extract the requested section from the given Content 673 * 674 * @param Content $content 675 * @param string|Message $what Identifies the content in error messages, e.g. page title. 676 * @return Content 677 */ 678 private function getSectionContent( Content $content, $what ) { 679 // Not cached (save or load) 680 $section = $content->getSection( $this->section ); 681 if ( $section === false ) { 682 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' ); 683 } 684 if ( $section === null ) { 685 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' ); 686 $section = false; 687 } 688 689 return $section; 690 } 691 692 /** 693 * This mimicks the behavior of EditPage in formatting a summary 694 * 695 * @param Title $title of the page being parsed 696 * @param array $params The API parameters of the request 697 * @return string HTML 698 */ 699 private function formatSummary( $title, $params ) { 700 $summary = $params['summary'] ?? ''; 701 $sectionTitle = $params['sectiontitle'] ?? ''; 702 703 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) { 704 if ( $sectionTitle !== '' ) { 705 $summary = $params['sectiontitle']; 706 } 707 if ( $summary !== '' ) { 708 $summary = wfMessage( 'newsectionsummary' ) 709 ->rawParams( MediaWikiServices::getInstance()->getParser() 710 ->stripSectionName( $summary ) ) 711 ->inContentLanguage()->text(); 712 } 713 } 714 return Linker::formatComment( $summary, $title, $this->section === 'new' ); 715 } 716 717 private function formatLangLinks( $links ) { 718 $languageNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils(); 719 $result = []; 720 foreach ( $links as $link ) { 721 $entry = []; 722 $bits = explode( ':', $link, 2 ); 723 $title = Title::newFromText( $link ); 724 725 $entry['lang'] = $bits[0]; 726 if ( $title ) { 727 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); 728 // localised language name in 'uselang' language 729 $entry['langname'] = $languageNameUtils->getLanguageName( 730 $title->getInterwiki(), 731 $this->getLanguage()->getCode() 732 ); 733 734 // native language name 735 $entry['autonym'] = $languageNameUtils->getLanguageName( $title->getInterwiki() ); 736 } 737 ApiResult::setContentValue( $entry, 'title', $bits[1] ); 738 $result[] = $entry; 739 } 740 741 return $result; 742 } 743 744 private function formatCategoryLinks( $links ) { 745 $result = []; 746 747 if ( !$links ) { 748 return $result; 749 } 750 751 // Fetch hiddencat property 752 $lb = new LinkBatch; 753 $lb->setArray( [ NS_CATEGORY => $links ] ); 754 $db = $this->getDB(); 755 $res = $db->select( [ 'page', 'page_props' ], 756 [ 'page_title', 'pp_propname' ], 757 $lb->constructSet( 'page', $db ), 758 __METHOD__, 759 [], 760 [ 'page_props' => [ 761 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] 762 ] ] 763 ); 764 $hiddencats = []; 765 foreach ( $res as $row ) { 766 $hiddencats[$row->page_title] = isset( $row->pp_propname ); 767 } 768 769 $linkCache = MediaWikiServices::getInstance()->getLinkCache(); 770 771 foreach ( $links as $link => $sortkey ) { 772 $entry = []; 773 $entry['sortkey'] = $sortkey; 774 // array keys will cast numeric category names to ints, so cast back to string 775 ApiResult::setContentValue( $entry, 'category', (string)$link ); 776 if ( !isset( $hiddencats[$link] ) ) { 777 $entry['missing'] = true; 778 779 // We already know the link doesn't exist in the database, so 780 // tell LinkCache that before calling $title->isKnown(). 781 $title = Title::makeTitle( NS_CATEGORY, $link ); 782 $linkCache->addBadLinkObj( $title ); 783 if ( $title->isKnown() ) { 784 $entry['known'] = true; 785 } 786 } elseif ( $hiddencats[$link] ) { 787 $entry['hidden'] = true; 788 } 789 $result[] = $entry; 790 } 791 792 return $result; 793 } 794 795 private function formatLinks( $links ) { 796 $result = []; 797 foreach ( $links as $ns => $nslinks ) { 798 foreach ( $nslinks as $title => $id ) { 799 $entry = []; 800 $entry['ns'] = $ns; 801 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() ); 802 $entry['exists'] = $id != 0; 803 $result[] = $entry; 804 } 805 } 806 807 return $result; 808 } 809 810 private function formatIWLinks( $iw ) { 811 $result = []; 812 foreach ( $iw as $prefix => $titles ) { 813 foreach ( array_keys( $titles ) as $title ) { 814 $entry = []; 815 $entry['prefix'] = $prefix; 816 817 $title = Title::newFromText( "{$prefix}:{$title}" ); 818 if ( $title ) { 819 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); 820 } 821 822 ApiResult::setContentValue( $entry, 'title', $title->getFullText() ); 823 $result[] = $entry; 824 } 825 } 826 827 return $result; 828 } 829 830 private function formatHeadItems( $headItems ) { 831 $result = []; 832 foreach ( $headItems as $tag => $content ) { 833 $entry = []; 834 $entry['tag'] = $tag; 835 ApiResult::setContentValue( $entry, 'content', $content ); 836 $result[] = $entry; 837 } 838 839 return $result; 840 } 841 842 private function formatLimitReportData( $limitReportData ) { 843 $result = []; 844 845 foreach ( $limitReportData as $name => $value ) { 846 $entry = []; 847 $entry['name'] = $name; 848 if ( !is_array( $value ) ) { 849 $value = [ $value ]; 850 } 851 ApiResult::setIndexedTagNameRecursive( $value, 'param' ); 852 $entry = array_merge( $entry, $value ); 853 $result[] = $entry; 854 } 855 856 return $result; 857 } 858 859 private function setIndexedTagNames( &$array, $mapping ) { 860 foreach ( $mapping as $key => $name ) { 861 if ( isset( $array[$key] ) ) { 862 ApiResult::setIndexedTagName( $array[$key], $name ); 863 } 864 } 865 } 866 867 public function getAllowedParams() { 868 return [ 869 'title' => null, 870 'text' => [ 871 ApiBase::PARAM_TYPE => 'text', 872 ], 873 'revid' => [ 874 ApiBase::PARAM_TYPE => 'integer', 875 ], 876 'summary' => null, 877 'page' => null, 878 'pageid' => [ 879 ApiBase::PARAM_TYPE => 'integer', 880 ], 881 'redirects' => false, 882 'oldid' => [ 883 ApiBase::PARAM_TYPE => 'integer', 884 ], 885 'prop' => [ 886 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' . 887 'images|externallinks|sections|revid|displaytitle|iwlinks|' . 888 'properties|parsewarnings', 889 ApiBase::PARAM_ISMULTI => true, 890 ApiBase::PARAM_TYPE => [ 891 'text', 892 'langlinks', 893 'categories', 894 'categorieshtml', 895 'links', 896 'templates', 897 'images', 898 'externallinks', 899 'sections', 900 'revid', 901 'displaytitle', 902 'headhtml', 903 'modules', 904 'jsconfigvars', 905 'encodedjsconfigvars', 906 'indicators', 907 'iwlinks', 908 'wikitext', 909 'properties', 910 'limitreportdata', 911 'limitreporthtml', 912 'parsetree', 913 'parsewarnings', 914 'headitems', 915 ], 916 ApiBase::PARAM_HELP_MSG_PER_VALUE => [ 917 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ], 918 ], 919 ApiBase::PARAM_DEPRECATED_VALUES => [ 920 'headitems' => 'apiwarn-deprecation-parse-headitems', 921 ], 922 ], 923 'wrapoutputclass' => 'mw-parser-output', 924 'pst' => false, 925 'onlypst' => false, 926 'effectivelanglinks' => [ 927 ApiBase::PARAM_DFLT => false, 928 ApiBase::PARAM_DEPRECATED => true, 929 ], 930 'section' => null, 931 'sectiontitle' => [ 932 ApiBase::PARAM_TYPE => 'string', 933 ], 934 'disablepp' => [ 935 ApiBase::PARAM_DFLT => false, 936 ApiBase::PARAM_DEPRECATED => true, 937 ], 938 'disablelimitreport' => false, 939 'disableeditsection' => false, 940 'disablestylededuplication' => false, 941 'generatexml' => [ 942 ApiBase::PARAM_DFLT => false, 943 ApiBase::PARAM_HELP_MSG => [ 944 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT 945 ], 946 ApiBase::PARAM_DEPRECATED => true, 947 ], 948 'preview' => false, 949 'sectionpreview' => false, 950 'disabletoc' => false, 951 'useskin' => [ 952 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ), 953 ], 954 'contentformat' => [ 955 ApiBase::PARAM_TYPE => $this->getContentHandlerFactory()->getAllContentFormats(), 956 ], 957 'contentmodel' => [ 958 ApiBase::PARAM_TYPE => $this->getContentHandlerFactory()->getContentModels(), 959 ], 960 ]; 961 } 962 963 protected function getExamplesMessages() { 964 return [ 965 'action=parse&page=Project:Sandbox' 966 => 'apihelp-parse-example-page', 967 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext' 968 => 'apihelp-parse-example-text', 969 'action=parse&text={{PAGENAME}}&title=Test' 970 => 'apihelp-parse-example-texttitle', 971 'action=parse&summary=Some+[[link]]&prop=' 972 => 'apihelp-parse-example-summary', 973 ]; 974 } 975 976 public function getHelpUrls() { 977 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse'; 978 } 979 980 private function getContentHandlerFactory(): IContentHandlerFactory { 981 return MediaWikiServices::getInstance()->getContentHandlerFactory(); 982 } 983} 984