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