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::output()
384  $outputPage->loadSkinModules( $skin );
385  }
386 
387  $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
388  }
389 
390  if ( $oldid !== null ) {
391  $result_array['revid'] = (int)$oldid;
392  }
393 
394  if ( $params['redirects'] && $redirValues !== null ) {
395  $result_array['redirects'] = $redirValues;
396  }
397 
398  if ( isset( $prop['text'] ) ) {
399  $result_array['text'] = $p_result->getText( [
400  'allowTOC' => !$params['disabletoc'],
401  'enableSectionEditLinks' => !$params['disableeditsection'],
402  'wrapperDivClass' => $params['wrapoutputclass'],
403  'deduplicateStyles' => !$params['disablestylededuplication'],
404  'skin' => $context ? $context->getSkin() : null,
405  ] );
406  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
407  }
408 
409  if ( $params['summary'] !== null ||
410  ( $params['sectiontitle'] !== null && $this->section === 'new' )
411  ) {
412  $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
413  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
414  }
415 
416  if ( isset( $prop['langlinks'] ) ) {
417  if ( $skin ) {
418  $langlinks = $outputPage->getLanguageLinks();
419  } else {
420  $langlinks = $p_result->getLanguageLinks();
421  // The deprecated 'effectivelanglinks' option depredates OutputPage
422  // support via 'useskin'. If not already applied, then run just this
423  // one hook of OutputPage::addParserOutputMetadata here.
424  if ( $params['effectivelanglinks'] ) {
425  $linkFlags = [];
426  $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
427  }
428  }
429 
430  $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
431  }
432  if ( isset( $prop['categories'] ) ) {
433  $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
434  }
435  if ( isset( $prop['categorieshtml'] ) ) {
436  $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
437  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
438  }
439  if ( isset( $prop['links'] ) ) {
440  $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
441  }
442  if ( isset( $prop['templates'] ) ) {
443  $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
444  }
445  if ( isset( $prop['images'] ) ) {
446  $result_array['images'] = array_keys( $p_result->getImages() );
447  }
448  if ( isset( $prop['externallinks'] ) ) {
449  $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
450  }
451  if ( isset( $prop['sections'] ) ) {
452  $result_array['sections'] = $p_result->getSections();
453  }
454  if ( isset( $prop['parsewarnings'] ) ) {
455  $result_array['parsewarnings'] = $p_result->getWarnings();
456  }
457 
458  if ( isset( $prop['displaytitle'] ) ) {
459  $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
460  ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText();
461  }
462 
463  if ( isset( $prop['headitems'] ) ) {
464  if ( $skin ) {
465  $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
466  } else {
467  $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
468  }
469  }
470 
471  if ( isset( $prop['headhtml'] ) ) {
472  $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
473  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
474  }
475 
476  if ( isset( $prop['modules'] ) ) {
477  if ( $skin ) {
478  $result_array['modules'] = $outputPage->getModules();
479  // Deprecated since 1.32 (T188689)
480  $result_array['modulescripts'] = [];
481  $result_array['modulestyles'] = $outputPage->getModuleStyles();
482  } else {
483  $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
484  // Deprecated since 1.32 (T188689)
485  $result_array['modulescripts'] = [];
486  $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
487  }
488  }
489 
490  if ( isset( $prop['jsconfigvars'] ) ) {
491  $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
492  $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
493  }
494 
495  if ( isset( $prop['encodedjsconfigvars'] ) ) {
496  $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
497  $result_array['encodedjsconfigvars'] = FormatJson::encode(
498  $jsconfigvars,
499  false,
501  );
502  $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
503  }
504 
505  if ( isset( $prop['modules'] ) &&
506  !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
507  $this->addWarning( 'apiwarn-moduleswithoutvars' );
508  }
509 
510  if ( isset( $prop['indicators'] ) ) {
511  if ( $skin ) {
512  $result_array['indicators'] = (array)$outputPage->getIndicators();
513  } else {
514  $result_array['indicators'] = (array)$p_result->getIndicators();
515  }
516  ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
517  }
518 
519  if ( isset( $prop['iwlinks'] ) ) {
520  $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
521  }
522 
523  if ( isset( $prop['wikitext'] ) ) {
524  $result_array['wikitext'] = $this->content->serialize( $format );
525  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
526  if ( $this->pstContent !== null ) {
527  $result_array['psttext'] = $this->pstContent->serialize( $format );
528  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
529  }
530  }
531  if ( isset( $prop['properties'] ) ) {
532  $result_array['properties'] = (array)$p_result->getProperties();
533  ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
534  }
535 
536  if ( isset( $prop['limitreportdata'] ) ) {
537  $result_array['limitreportdata'] =
538  $this->formatLimitReportData( $p_result->getLimitReportData() );
539  }
540  if ( isset( $prop['limitreporthtml'] ) ) {
541  $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
542  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
543  }
544 
545  if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
546  if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
547  $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
548  }
549 
550  $parser = MediaWikiServices::getInstance()->getParser();
551  $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
552  // @phan-suppress-next-line PhanUndeclaredMethod
553  $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
554  $result_array['parsetree'] = $xml;
555  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
556  }
557 
558  $result_mapping = [
559  'redirects' => 'r',
560  'langlinks' => 'll',
561  'categories' => 'cl',
562  'links' => 'pl',
563  'templates' => 'tl',
564  'images' => 'img',
565  'externallinks' => 'el',
566  'iwlinks' => 'iw',
567  'sections' => 's',
568  'headitems' => 'hi',
569  'modules' => 'm',
570  'indicators' => 'ind',
571  'modulescripts' => 'm',
572  'modulestyles' => 'm',
573  'properties' => 'pp',
574  'limitreportdata' => 'lr',
575  'parsewarnings' => 'pw'
576  ];
577  $this->setIndexedTagNames( $result_array, $result_mapping );
578  $result->addValue( null, $this->getModuleName(), $result_array );
579  }
580 
589  protected function makeParserOptions( WikiPage $pageObj, array $params ) {
590  $popts = $pageObj->makeParserOptions( $this->getContext() );
591  $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
592  $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
593  $popts->setIsSectionPreview( $params['sectionpreview'] );
594 
595  if ( $params['wrapoutputclass'] !== '' ) {
596  $popts->setWrapOutputClass( $params['wrapoutputclass'] );
597  }
598 
599  $reset = null;
600  $suppressCache = false;
601  $this->getHookRunner()->onApiMakeParserOptions( $popts, $pageObj->getTitle(),
602  $params, $this, $reset, $suppressCache );
603 
604  // Force cache suppression when $popts aren't cacheable.
605  $suppressCache = $suppressCache || !$popts->isSafeToCache();
606 
607  return [ $popts, $reset, $suppressCache ];
608  }
609 
619  private function getParsedContent(
620  WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
621  ) {
622  $revId = $rev ? $rev->getId() : null;
623  $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
624 
625  if ( $getContent || $this->section !== false || $isDeleted ) {
626  if ( $rev ) {
627  $this->content = $rev->getContent(
628  SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getUser()
629  );
630  if ( !$this->content ) {
631  $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
632  }
633  } else {
634  $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getUser() );
635  if ( !$this->content ) {
636  $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
637  }
638  }
639  $this->contentIsDeleted = $isDeleted;
640  $this->contentIsSuppressed = $rev &&
641  $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
642  }
643 
644  if ( $this->section !== false ) {
645  $this->content = $this->getSectionContent(
646  $this->content,
647  $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
648  );
649  return $this->getContentParserOutput( $this->content, $page->getTitle(), $revId, $popts );
650  }
651 
652  if ( $isDeleted ) {
653  // getParserOutput can't do revdeled revisions
654 
655  $pout = $this->getContentParserOutput( $this->content, $page->getTitle(), $revId, $popts );
656  } else {
657  // getParserOutput will save to Parser cache if able
658  $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
659  }
660  if ( !$pout ) {
661  // @codeCoverageIgnoreStart
662  $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
663  // @codeCoverageIgnoreEnd
664  }
665 
666  return $pout;
667  }
668 
676  private function getSectionContent( Content $content, $what ) {
677  // Not cached (save or load)
678  $section = $content->getSection( $this->section );
679  if ( $section === false ) {
680  $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
681  }
682  if ( $section === null ) {
683  $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
684  $section = false;
685  }
686 
687  return $section;
688  }
689 
697  private function formatSummary( $title, $params ) {
698  $summary = $params['summary'] ?? '';
699  $sectionTitle = $params['sectiontitle'] ?? '';
700 
701  if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
702  if ( $sectionTitle !== '' ) {
703  $summary = $params['sectiontitle'];
704  }
705  if ( $summary !== '' ) {
706  $summary = wfMessage( 'newsectionsummary' )
707  ->rawParams( MediaWikiServices::getInstance()->getParser()
708  ->stripSectionName( $summary ) )
709  ->inContentLanguage()->text();
710  }
711  }
712  return Linker::formatComment( $summary, $title, $this->section === 'new' );
713  }
714 
715  private function formatLangLinks( $links ) {
716  $languageNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
717  $result = [];
718  foreach ( $links as $link ) {
719  $entry = [];
720  $bits = explode( ':', $link, 2 );
721  $title = Title::newFromText( $link );
722 
723  $entry['lang'] = $bits[0];
724  if ( $title ) {
725  $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
726  // localised language name in 'uselang' language
727  $entry['langname'] = $languageNameUtils->getLanguageName(
728  $title->getInterwiki(),
729  $this->getLanguage()->getCode()
730  );
731 
732  // native language name
733  $entry['autonym'] = $languageNameUtils->getLanguageName( $title->getInterwiki() );
734  }
735  ApiResult::setContentValue( $entry, 'title', $bits[1] );
736  $result[] = $entry;
737  }
738 
739  return $result;
740  }
741 
742  private function formatCategoryLinks( $links ) {
743  $result = [];
744 
745  if ( !$links ) {
746  return $result;
747  }
748 
749  // Fetch hiddencat property
750  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
751  $lb = $linkBatchFactory->newLinkBatch();
752  $lb->setArray( [ NS_CATEGORY => $links ] );
753  $db = $this->getDB();
754  $res = $db->select( [ 'page', 'page_props' ],
755  [ 'page_title', 'pp_propname' ],
756  $lb->constructSet( 'page', $db ),
757  __METHOD__,
758  [],
759  [ 'page_props' => [
760  'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
761  ] ]
762  );
763  $hiddencats = [];
764  foreach ( $res as $row ) {
765  $hiddencats[$row->page_title] = isset( $row->pp_propname );
766  }
767 
768  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
769 
770  foreach ( $links as $link => $sortkey ) {
771  $entry = [];
772  $entry['sortkey'] = $sortkey;
773  // array keys will cast numeric category names to ints, so cast back to string
774  ApiResult::setContentValue( $entry, 'category', (string)$link );
775  if ( !isset( $hiddencats[$link] ) ) {
776  $entry['missing'] = true;
777 
778  // We already know the link doesn't exist in the database, so
779  // tell LinkCache that before calling $title->isKnown().
780  $title = Title::makeTitle( NS_CATEGORY, $link );
781  $linkCache->addBadLinkObj( $title );
782  if ( $title->isKnown() ) {
783  $entry['known'] = true;
784  }
785  } elseif ( $hiddencats[$link] ) {
786  $entry['hidden'] = true;
787  }
788  $result[] = $entry;
789  }
790 
791  return $result;
792  }
793 
794  private function formatLinks( $links ) {
795  $result = [];
796  foreach ( $links as $ns => $nslinks ) {
797  foreach ( $nslinks as $title => $id ) {
798  $entry = [];
799  $entry['ns'] = $ns;
800  ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
801  $entry['exists'] = $id != 0;
802  $result[] = $entry;
803  }
804  }
805 
806  return $result;
807  }
808 
809  private function formatIWLinks( $iw ) {
810  $result = [];
811  foreach ( $iw as $prefix => $titles ) {
812  foreach ( array_keys( $titles ) as $title ) {
813  $entry = [];
814  $entry['prefix'] = $prefix;
815 
816  $title = Title::newFromText( "{$prefix}:{$title}" );
817  if ( $title ) {
818  $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
819  }
820 
821  ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
822  $result[] = $entry;
823  }
824  }
825 
826  return $result;
827  }
828 
829  private function formatHeadItems( $headItems ) {
830  $result = [];
831  foreach ( $headItems as $tag => $content ) {
832  $entry = [];
833  $entry['tag'] = $tag;
834  ApiResult::setContentValue( $entry, 'content', $content );
835  $result[] = $entry;
836  }
837 
838  return $result;
839  }
840 
841  private function formatLimitReportData( $limitReportData ) {
842  $result = [];
843 
844  foreach ( $limitReportData as $name => $value ) {
845  $entry = [];
846  $entry['name'] = $name;
847  if ( !is_array( $value ) ) {
848  $value = [ $value ];
849  }
850  ApiResult::setIndexedTagNameRecursive( $value, 'param' );
851  $entry = array_merge( $entry, $value );
852  $result[] = $entry;
853  }
854 
855  return $result;
856  }
857 
858  private function setIndexedTagNames( &$array, $mapping ) {
859  foreach ( $mapping as $key => $name ) {
860  if ( isset( $array[$key] ) ) {
861  ApiResult::setIndexedTagName( $array[$key], $name );
862  }
863  }
864  }
865 
866  public function getAllowedParams() {
867  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
868 
869  return [
870  'title' => null,
871  'text' => [
872  ApiBase::PARAM_TYPE => 'text',
873  ],
874  'revid' => [
875  ApiBase::PARAM_TYPE => 'integer',
876  ],
877  'summary' => null,
878  'page' => null,
879  'pageid' => [
880  ApiBase::PARAM_TYPE => 'integer',
881  ],
882  'redirects' => false,
883  'oldid' => [
884  ApiBase::PARAM_TYPE => 'integer',
885  ],
886  'prop' => [
887  ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
888  'images|externallinks|sections|revid|displaytitle|iwlinks|' .
889  'properties|parsewarnings',
890  ApiBase::PARAM_ISMULTI => true,
892  'text',
893  'langlinks',
894  'categories',
895  'categorieshtml',
896  'links',
897  'templates',
898  'images',
899  'externallinks',
900  'sections',
901  'revid',
902  'displaytitle',
903  'headhtml',
904  'modules',
905  'jsconfigvars',
906  'encodedjsconfigvars',
907  'indicators',
908  'iwlinks',
909  'wikitext',
910  'properties',
911  'limitreportdata',
912  'limitreporthtml',
913  'parsetree',
914  'parsewarnings',
915  'headitems',
916  ],
918  'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
919  ],
921  'headitems' => 'apiwarn-deprecation-parse-headitems',
922  ],
923  ],
924  'wrapoutputclass' => 'mw-parser-output',
925  'pst' => false,
926  'onlypst' => false,
927  'effectivelanglinks' => [
928  ApiBase::PARAM_DFLT => false,
930  ],
931  'section' => null,
932  'sectiontitle' => [
933  ApiBase::PARAM_TYPE => 'string',
934  ],
935  'disablepp' => [
936  ApiBase::PARAM_DFLT => false,
938  ],
939  'disablelimitreport' => false,
940  'disableeditsection' => false,
941  'disablestylededuplication' => false,
942  'generatexml' => [
943  ApiBase::PARAM_DFLT => false,
945  'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
946  ],
948  ],
949  'preview' => false,
950  'sectionpreview' => false,
951  'disabletoc' => false,
952  'useskin' => [
953  ApiBase::PARAM_TYPE => array_keys( $skinFactory->getAllowedSkins() ),
954  ],
955  'contentformat' => [
956  ApiBase::PARAM_TYPE => $this->getContentHandlerFactory()->getAllContentFormats(),
957  ],
958  'contentmodel' => [
959  ApiBase::PARAM_TYPE => $this->getContentHandlerFactory()->getContentModels(),
960  ],
961  ];
962  }
963 
964  protected function getExamplesMessages() {
965  return [
966  'action=parse&page=Project:Sandbox'
967  => 'apihelp-parse-example-page',
968  'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
969  => 'apihelp-parse-example-text',
970  'action=parse&text={{PAGENAME}}&title=Test'
971  => 'apihelp-parse-example-texttitle',
972  'action=parse&summary=Some+[[link]]&prop='
973  => 'apihelp-parse-example-summary',
974  ];
975  }
976 
977  public function getHelpUrls() {
978  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
979  }
980 
982  return MediaWikiServices::getInstance()->getContentHandlerFactory();
983  }
984 }
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:48
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:34
ApiParse\formatIWLinks
formatIWLinks( $iw)
Definition: ApiParse.php:809
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:35
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:328
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:42
ApiParse\getParsedContent
getParsedContent(WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent)
Definition: ApiParse.php:619
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:1300
WikiMap\getCurrentWikiDbDomain
static getCurrentWikiDbDomain()
Definition: WikiMap.php:293
IContextSource\getSkin
getSkin()
ApiParse\formatLangLinks
formatLangLinks( $links)
Definition: ApiParse.php:715
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:158
ApiBase\dieWithError
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition: ApiBase.php:1381
ApiBase\PARAM_HELP_MSG
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:106
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:985
ApiBase\PARAM_TYPE
const PARAM_TYPE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:70
ApiParse\formatLinks
formatLinks( $links)
Definition: ApiParse.php:794
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:564
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:53
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2001
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
ApiBase\getDB
getDB()
Gets a default replica DB connection object Stable to override.
Definition: ApiBase.php:594
PoolCounterWorkViaCallback
Convenience class for dealing with PoolCounters using callbacks.
Definition: PoolCounterWorkViaCallback.php:31
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()
Stable to override.
Definition: ContextSource.php:131
ApiBase\PARAM_DEPRECATED_VALUES
const PARAM_DEPRECATED_VALUES
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:82
$wgTitle
$wgTitle
Definition: Setup.php:794
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:52
ApiParse\getPageParserOutput
getPageParserOutput(WikiPage $page, $revId, ParserOptions $popts, bool $suppressCache)
Definition: ApiParse.php:74
ApiParse\getContentHandlerFactory
getContentHandlerFactory()
Definition: ApiParse.php:981
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:75
DerivativeContext
An IContextSource implementation which will inherit context from another source but allow individual ...
Definition: DerivativeContext.php:31
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:977
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:157
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:101
ApiParse\getAllowedParams
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiParse.php:866
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:1187
WikiPage\getId
getId()
Definition: WikiPage.php:542
ApiParse\formatLimitReportData
formatLimitReportData( $limitReportData)
Definition: ApiParse.php:841
EditPage\getPreviewLimitReport
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3924
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:282
MWContentSerializationException
Exception representing a failure to serialize or unserialize a content object.
Definition: MWContentSerializationException.php:8
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:716
$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:591
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:660
ApiMessage\create
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:42
ApiParse\formatSummary
formatSummary( $title, $params)
This mimicks the behavior of EditPage in formatting a summary.
Definition: ApiParse.php:697
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:142
ApiParse\formatCategoryLinks
formatCategoryLinks( $links)
Definition: ApiParse.php:742
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:184
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:888
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
ApiParse\setIndexedTagNames
setIndexedTagNames(&$array, $mapping)
Definition: ApiParse.php:858
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1494
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:280
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:35
ApiParse\$section
string false null $section
Definition: ApiParse.php:34
Title
Represents a title within MediaWiki.
Definition: Title.php:41
ApiParse\getPoolKey
getPoolKey()
Definition: ApiParse.php:45
ApiBase\PARAM_DFLT
const PARAM_DFLT
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:68
ApiParse\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiParse.php:964
ApiBase\dieWithException
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1393
ApiBase\getModuleName
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:443
ApiBase\PARAM_ISMULTI
const PARAM_ISMULTI
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:69
ApiParse\getSectionContent
getSectionContent(Content $content, $what)
Extract the requested section from the given Content.
Definition: ApiParse.php:676
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:459
ApiBase\checkTitleUserPermissions
checkTitleUserPermissions(LinkTarget $linkTarget, $actions, array $options=[])
Helper function for permission-denied errors.
Definition: ApiBase.php:1508
Content\getParserOutput
getParserOutput(Title $title, $revId=null, ParserOptions $options=null, $generateHtml=true)
Parse the Content object and generate a ParserOutput from the result.
ApiParse\formatHeadItems
formatHeadItems( $headItems)
Definition: ApiParse.php:829
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:139
ApiBase\getHookRunner
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:661
ApiParse\makeParserOptions
makeParserOptions(WikiPage $pageObj, array $params)
Constructs a ParserOptions object.
Definition: ApiParse.php:589
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:767
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