MediaWiki  master
ApiParse.php
Go to the documentation of this file.
1 <?php
27 
31 class ApiParse extends ApiBase {
32 
34  private $section = null;
35 
37  private $content = null;
38 
40  private $pstContent = null;
41 
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(
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,
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 
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 
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 
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 
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,
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  ],
917  'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
918  ],
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,
929  ],
930  'section' => null,
931  'sectiontitle' => [
932  ApiBase::PARAM_TYPE => 'string',
933  ],
934  'disablepp' => [
935  ApiBase::PARAM_DFLT => false,
937  ],
938  'disablelimitreport' => false,
939  'disableeditsection' => false,
940  'disablestylededuplication' => false,
941  'generatexml' => [
942  ApiBase::PARAM_DFLT => false,
944  'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
945  ],
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 
981  return MediaWikiServices::getInstance()->getContentHandlerFactory();
982  }
983 }
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
ApiMain
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:44
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:33
ApiParse\formatIWLinks
formatIWLinks( $iw)
Definition: ApiParse.php:810
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:329
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
ApiParse\getParsedContent
getParsedContent(WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent)
Definition: ApiParse.php:621
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
ApiBase\addWarning
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition: ApiBase.php:1334
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
IContextSource\getSkin
getSkin()
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:35
ApiParse\formatLangLinks
formatLangLinks( $links)
Definition: ApiParse.php:717
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
ApiBase\dieWithError
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:1415
ApiBase\PARAM_HELP_MSG
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:104
ApiResult\META_BC_SUBELEMENTS
const META_BC_SUBELEMENTS
Key for the 'BC subelements' metadata item.
Definition: ApiResult.php:143
ApiBase\getTitleOrPageId
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition: ApiBase.php:964
ApiBase\PARAM_TYPE
const PARAM_TYPE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:68
ApiParse\formatLinks
formatLinks( $links)
Definition: ApiParse.php:795
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:546
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:51
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2013
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1222
ApiBase\getDB
getDB()
Gets a default replica DB connection object.
Definition: ApiBase.php:574
PoolCounterWorkViaCallback
Convenience class for dealing with PoolCounters using callbacks.
Definition: PoolCounterWorkViaCallback.php:28
CONTENT_MODEL_WIKITEXT
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:224
ApiParse\$contentIsDeleted
bool $contentIsDeleted
Definition: ApiParse.php:43
$res
$res
Definition: testCompression.php:57
ContextSource\getUser
getUser()
Definition: ContextSource.php:120
ApiBase\PARAM_DEPRECATED_VALUES
const PARAM_DEPRECATED_VALUES
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:80
LinkBatch\setArray
setArray( $array)
Set the link list to a given 2-d array First key is the namespace, second is the DB key,...
Definition: LinkBatch.php:147
$wgTitle
$wgTitle
Definition: Setup.php:800
FormatJson\ALL_OK
const ALL_OK
Skip escaping as many characters as reasonably possible.
Definition: FormatJson.php:55
ApiPageSet
This class contains a list of pages that the client has requested.
Definition: ApiPageSet.php:42
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:50
ApiParse\getPageParserOutput
getPageParserOutput(WikiPage $page, $revId, ParserOptions $popts, bool $suppressCache)
Definition: ApiParse.php:74
ApiParse\getContentHandlerFactory
getContentHandlerFactory()
Definition: ApiParse.php:980
ApiResult\setContentValue
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
Definition: ApiResult.php:466
ApiBase\PARAM_DEPRECATED
const PARAM_DEPRECATED
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:73
DerivativeContext
An IContextSource implementation which will inherit context from another source but allow individual ...
Definition: DerivativeContext.php:30
ApiParse
Definition: ApiParse.php:31
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
ApiParse\$pstContent
Content $pstContent
Definition: ApiParse.php:40
ApiResult\setArrayType
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Definition: ApiResult.php:716
ApiParse\getHelpUrls
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiParse.php:976
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:154
Content\getSection
getSection( $sectionId)
Returns the section with the given ID.
ApiResult\addMetadataToResultVars
static addMetadataToResultVars( $vars, $forceHash=true)
Add the correct metadata to an array of vars we want to export through the API.
Definition: ApiResult.php:1138
Skin\normalizeKey
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:102
ApiParse\getAllowedParams
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiParse.php:867
PoolCounterWork\execute
execute( $skipcache=false)
Get the result of the work (whatever it is), or the result of the error() function.
Definition: PoolCounterWork.php:127
ApiResult\META_SUBELEMENTS
const META_SUBELEMENTS
Key for the 'subelements' metadata item.
Definition: ApiResult.php:78
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1249
WikiPage\getId
getId()
Definition: WikiPage.php:578
ApiParse\formatLimitReportData
formatLimitReportData( $limitReportData)
Definition: ApiParse.php:842
EditPage\getPreviewLimitReport
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3779
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:318
MWContentSerializationException
Exception representing a failure to serialize or unserialize a content object.
Definition: MWContentSerializationException.php:7
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:211
ApiParse\getContentParserOutput
getContentParserOutput(Content $content, Title $title, $revId, ParserOptions $popts)
Definition: ApiParse.php:55
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:695
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:592
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:696
ApiMessage\create
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:40
ApiParse\formatSummary
formatSummary( $title, $params)
This mimicks the behavior of EditPage in formatting a summary.
Definition: ApiParse.php:699
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:47
ContentHandler\makeContent
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
Definition: ContentHandler.php:140
ApiParse\formatCategoryLinks
formatCategoryLinks( $links)
Definition: ApiParse.php:744
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:168
ApiResult\setIndexedTagName
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
ApiBase\requireMaxOneParameter
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:867
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
ApiParse\setIndexedTagNames
setIndexedTagNames(&$array, $mapping)
Definition: ApiParse.php:859
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1490
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:281
Parser\OT_PREPROCESS
const OT_PREPROCESS
Definition: Parser.php:122
Linker\formatComment
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition: Linker.php:1199
Content
Base interface for content objects.
Definition: Content.php:34
ApiParse\$section
string false null $section
Definition: ApiParse.php:34
Title
Represents a title within MediaWiki.
Definition: Title.php:42
ApiParse\getPoolKey
getPoolKey()
Definition: ApiParse.php:45
ApiBase\PARAM_DFLT
const PARAM_DFLT
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:66
ApiParse\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiParse.php:963
ApiBase\dieWithException
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1427
ApiBase\getModuleName
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:426
ApiBase\PARAM_ISMULTI
const PARAM_ISMULTI
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:67
ApiParse\getSectionContent
getSectionContent(Content $content, $what)
Extract the requested section from the given Content.
Definition: ApiParse.php:678
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:442
ApiBase\checkTitleUserPermissions
checkTitleUserPermissions(LinkTarget $linkTarget, $actions, array $options=[])
Helper function for permission-denied errors.
Definition: ApiBase.php:1542
Content\getParserOutput
getParserOutput(Title $title, $revId=null, ParserOptions $options=null, $generateHtml=true)
Parse the Content object and generate a ParserOutput from the result.
Skin\getAllowedSkins
static getAllowedSkins()
Fetch the list of user-selectable skins in regards to $wgSkipSkins.
Definition: Skin.php:77
ApiParse\formatHeadItems
formatHeadItems( $headItems)
Definition: ApiParse.php:830
ApiResult\setIndexedTagNameRecursive
static setIndexedTagNameRecursive(array &$arr, $tag)
Set indexed tag name on $arr and all subarrays.
Definition: ApiResult.php:629
ApiBase\PARAM_HELP_MSG_PER_VALUE
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition: ApiBase.php:137
ApiBase\getHookRunner
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:641
ApiParse\makeParserOptions
makeParserOptions(WikiPage $pageObj, array $params)
Constructs a ParserOptions object.
Definition: ApiParse.php:591
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:802
ApiParse\$content
Content $content
Definition: ApiParse.php:37
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:490
ApiParse\$contentIsSuppressed
bool $contentIsSuppressed
Definition: ApiParse.php:43
ApiParse\execute
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiParse.php:93
Article\newFromTitle
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:194