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