build: Upgrade mediawiki-codesniffer from 26.0.0 to 28.0.0
[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\Revision\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 // @codeCoverageIgnoreStart
603 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
604 // @codeCoverageIgnoreEnd
605 }
606
607 return $pout;
608 }
609
610 /**
611 * Extract the requested section from the given Content
612 *
613 * @param Content $content
614 * @param string|Message $what Identifies the content in error messages, e.g. page title.
615 * @return Content
616 */
617 private function getSectionContent( Content $content, $what ) {
618 // Not cached (save or load)
619 $section = $content->getSection( $this->section );
620 if ( $section === false ) {
621 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
622 }
623 if ( $section === null ) {
624 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
625 $section = false;
626 }
627
628 return $section;
629 }
630
631 /**
632 * This mimicks the behavior of EditPage in formatting a summary
633 *
634 * @param Title $title of the page being parsed
635 * @param array $params The API parameters of the request
636 * @return Content|bool
637 */
638 private function formatSummary( $title, $params ) {
639 $summary = $params['summary'] ?? '';
640 $sectionTitle = $params['sectiontitle'] ?? '';
641
642 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
643 if ( $sectionTitle !== '' ) {
644 $summary = $params['sectiontitle'];
645 }
646 if ( $summary !== '' ) {
647 $summary = wfMessage( 'newsectionsummary' )
648 ->rawParams( MediaWikiServices::getInstance()->getParser()
649 ->stripSectionName( $summary ) )
650 ->inContentLanguage()->text();
651 }
652 }
653 return Linker::formatComment( $summary, $title, $this->section === 'new' );
654 }
655
656 private function formatLangLinks( $links ) {
657 $result = [];
658 foreach ( $links as $link ) {
659 $entry = [];
660 $bits = explode( ':', $link, 2 );
661 $title = Title::newFromText( $link );
662
663 $entry['lang'] = $bits[0];
664 if ( $title ) {
665 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
666 // localised language name in 'uselang' language
667 $entry['langname'] = Language::fetchLanguageName(
668 $title->getInterwiki(),
669 $this->getLanguage()->getCode()
670 );
671
672 // native language name
673 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
674 }
675 ApiResult::setContentValue( $entry, 'title', $bits[1] );
676 $result[] = $entry;
677 }
678
679 return $result;
680 }
681
682 private function formatCategoryLinks( $links ) {
683 $result = [];
684
685 if ( !$links ) {
686 return $result;
687 }
688
689 // Fetch hiddencat property
690 $lb = new LinkBatch;
691 $lb->setArray( [ NS_CATEGORY => $links ] );
692 $db = $this->getDB();
693 $res = $db->select( [ 'page', 'page_props' ],
694 [ 'page_title', 'pp_propname' ],
695 $lb->constructSet( 'page', $db ),
696 __METHOD__,
697 [],
698 [ 'page_props' => [
699 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
700 ] ]
701 );
702 $hiddencats = [];
703 foreach ( $res as $row ) {
704 $hiddencats[$row->page_title] = isset( $row->pp_propname );
705 }
706
707 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
708
709 foreach ( $links as $link => $sortkey ) {
710 $entry = [];
711 $entry['sortkey'] = $sortkey;
712 // array keys will cast numeric category names to ints, so cast back to string
713 ApiResult::setContentValue( $entry, 'category', (string)$link );
714 if ( !isset( $hiddencats[$link] ) ) {
715 $entry['missing'] = true;
716
717 // We already know the link doesn't exist in the database, so
718 // tell LinkCache that before calling $title->isKnown().
719 $title = Title::makeTitle( NS_CATEGORY, $link );
720 $linkCache->addBadLinkObj( $title );
721 if ( $title->isKnown() ) {
722 $entry['known'] = true;
723 }
724 } elseif ( $hiddencats[$link] ) {
725 $entry['hidden'] = true;
726 }
727 $result[] = $entry;
728 }
729
730 return $result;
731 }
732
733 private function formatLinks( $links ) {
734 $result = [];
735 foreach ( $links as $ns => $nslinks ) {
736 foreach ( $nslinks as $title => $id ) {
737 $entry = [];
738 $entry['ns'] = $ns;
739 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
740 $entry['exists'] = $id != 0;
741 $result[] = $entry;
742 }
743 }
744
745 return $result;
746 }
747
748 private function formatIWLinks( $iw ) {
749 $result = [];
750 foreach ( $iw as $prefix => $titles ) {
751 foreach ( array_keys( $titles ) as $title ) {
752 $entry = [];
753 $entry['prefix'] = $prefix;
754
755 $title = Title::newFromText( "{$prefix}:{$title}" );
756 if ( $title ) {
757 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
758 }
759
760 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
761 $result[] = $entry;
762 }
763 }
764
765 return $result;
766 }
767
768 private function formatHeadItems( $headItems ) {
769 $result = [];
770 foreach ( $headItems as $tag => $content ) {
771 $entry = [];
772 $entry['tag'] = $tag;
773 ApiResult::setContentValue( $entry, 'content', $content );
774 $result[] = $entry;
775 }
776
777 return $result;
778 }
779
780 private function formatLimitReportData( $limitReportData ) {
781 $result = [];
782
783 foreach ( $limitReportData as $name => $value ) {
784 $entry = [];
785 $entry['name'] = $name;
786 if ( !is_array( $value ) ) {
787 $value = [ $value ];
788 }
789 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
790 $entry = array_merge( $entry, $value );
791 $result[] = $entry;
792 }
793
794 return $result;
795 }
796
797 private function setIndexedTagNames( &$array, $mapping ) {
798 foreach ( $mapping as $key => $name ) {
799 if ( isset( $array[$key] ) ) {
800 ApiResult::setIndexedTagName( $array[$key], $name );
801 }
802 }
803 }
804
805 public function getAllowedParams() {
806 return [
807 'title' => null,
808 'text' => [
809 ApiBase::PARAM_TYPE => 'text',
810 ],
811 'revid' => [
812 ApiBase::PARAM_TYPE => 'integer',
813 ],
814 'summary' => null,
815 'page' => null,
816 'pageid' => [
817 ApiBase::PARAM_TYPE => 'integer',
818 ],
819 'redirects' => false,
820 'oldid' => [
821 ApiBase::PARAM_TYPE => 'integer',
822 ],
823 'prop' => [
824 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
825 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
826 'properties|parsewarnings',
827 ApiBase::PARAM_ISMULTI => true,
828 ApiBase::PARAM_TYPE => [
829 'text',
830 'langlinks',
831 'categories',
832 'categorieshtml',
833 'links',
834 'templates',
835 'images',
836 'externallinks',
837 'sections',
838 'revid',
839 'displaytitle',
840 'headhtml',
841 'modules',
842 'jsconfigvars',
843 'encodedjsconfigvars',
844 'indicators',
845 'iwlinks',
846 'wikitext',
847 'properties',
848 'limitreportdata',
849 'limitreporthtml',
850 'parsetree',
851 'parsewarnings',
852 'headitems',
853 ],
854 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
855 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
856 ],
857 ApiBase::PARAM_DEPRECATED_VALUES => [
858 'headitems' => 'apiwarn-deprecation-parse-headitems',
859 ],
860 ],
861 'wrapoutputclass' => 'mw-parser-output',
862 'pst' => false,
863 'onlypst' => false,
864 'effectivelanglinks' => [
865 ApiBase::PARAM_DFLT => false,
866 ApiBase::PARAM_DEPRECATED => true,
867 ],
868 'section' => null,
869 'sectiontitle' => [
870 ApiBase::PARAM_TYPE => 'string',
871 ],
872 'disablepp' => [
873 ApiBase::PARAM_DFLT => false,
874 ApiBase::PARAM_DEPRECATED => true,
875 ],
876 'disablelimitreport' => false,
877 'disableeditsection' => false,
878 'disabletidy' => [
879 ApiBase::PARAM_DFLT => false,
880 ApiBase::PARAM_DEPRECATED => true, // Since 1.32
881 ],
882 'disablestylededuplication' => false,
883 'generatexml' => [
884 ApiBase::PARAM_DFLT => false,
885 ApiBase::PARAM_HELP_MSG => [
886 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
887 ],
888 ApiBase::PARAM_DEPRECATED => true,
889 ],
890 'preview' => false,
891 'sectionpreview' => false,
892 'disabletoc' => false,
893 'useskin' => [
894 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ),
895 ],
896 'contentformat' => [
897 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
898 ],
899 'contentmodel' => [
900 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
901 ]
902 ];
903 }
904
905 protected function getExamplesMessages() {
906 return [
907 'action=parse&page=Project:Sandbox'
908 => 'apihelp-parse-example-page',
909 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
910 => 'apihelp-parse-example-text',
911 'action=parse&text={{PAGENAME}}&title=Test'
912 => 'apihelp-parse-example-texttitle',
913 'action=parse&summary=Some+[[link]]&prop='
914 => 'apihelp-parse-example-summary',
915 ];
916 }
917
918 public function getHelpUrls() {
919 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
920 }
921 }