MediaWiki  master
ApiParse.php
Go to the documentation of this file.
1 <?php
36 
40 class ApiParse extends ApiBase {
41 
43  private $section = null;
44 
46  private $content = null;
47 
49  private $pstContent = null;
50 
52  private $contentIsDeleted = false, $contentIsSuppressed = false;
53 
55  private $revisionLookup;
56 
58  private $skinFactory;
59 
61  private $languageNameUtils;
62 
64  private $linkBatchFactory;
65 
67  private $linkCache;
68 
70  private $contentHandlerFactory;
71 
73  private $parser;
74 
76  private $wikiPageFactory;
77 
79  private $contentTransformer;
80 
82  private $commentFormatter;
83 
85  private $contentRenderer;
86 
102  public function __construct(
103  ApiMain $main,
104  $action,
105  RevisionLookup $revisionLookup,
106  SkinFactory $skinFactory,
107  LanguageNameUtils $languageNameUtils,
108  LinkBatchFactory $linkBatchFactory,
109  LinkCache $linkCache,
110  IContentHandlerFactory $contentHandlerFactory,
111  Parser $parser,
112  WikiPageFactory $wikiPageFactory,
113  ContentRenderer $contentRenderer,
114  ContentTransformer $contentTransformer,
115  CommentFormatter $commentFormatter
116  ) {
117  parent::__construct( $main, $action );
118  $this->revisionLookup = $revisionLookup;
119  $this->skinFactory = $skinFactory;
120  $this->languageNameUtils = $languageNameUtils;
121  $this->linkBatchFactory = $linkBatchFactory;
122  $this->linkCache = $linkCache;
123  $this->contentHandlerFactory = $contentHandlerFactory;
124  $this->parser = $parser;
125  $this->wikiPageFactory = $wikiPageFactory;
126  $this->contentRenderer = $contentRenderer;
127  $this->contentTransformer = $contentTransformer;
128  $this->commentFormatter = $commentFormatter;
129  }
130 
131  private function getPoolKey(): string {
132  $poolKey = WikiMap::getCurrentWikiDbDomain() . ':ApiParse:';
133  if ( !$this->getUser()->isRegistered() ) {
134  $poolKey .= 'a:' . $this->getUser()->getName();
135  } else {
136  $poolKey .= 'u:' . $this->getUser()->getId();
137  }
138  return $poolKey;
139  }
140 
141  private function getContentParserOutput(
143  PageReference $page,
144  $revId,
145  ParserOptions $popts
146  ) {
147  $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(),
148  [
149  'doWork' => function () use ( $content, $page, $revId, $popts ) {
150  return $this->contentRenderer->getParserOutput( $content, $page, $revId, $popts );
151  },
152  'error' => function () {
153  $this->dieWithError( 'apierror-concurrency-limit' );
154  },
155  ]
156  );
157  return $worker->execute();
158  }
159 
160  private function getPageParserOutput(
161  WikiPage $page,
162  $revId,
163  ParserOptions $popts,
164  bool $suppressCache
165  ) {
166  $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(),
167  [
168  'doWork' => static function () use ( $page, $revId, $popts, $suppressCache ) {
169  return $page->getParserOutput( $popts, $revId, $suppressCache );
170  },
171  'error' => function () {
172  $this->dieWithError( 'apierror-concurrency-limit' );
173  },
174  ]
175  );
176  return $worker->execute();
177  }
178 
179  public function execute() {
180  // The data is hot but user-dependent, like page views, so we set vary cookies
181  $this->getMain()->setCacheMode( 'anon-public-user-private' );
182 
183  // Get parameters
184  $params = $this->extractRequestParams();
185 
186  // No easy way to say that text and title or revid are allowed together
187  // while the rest aren't, so just do it in three calls.
188  $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' );
189  $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' );
190  $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'revid' );
191 
192  $text = $params['text'];
193  $title = $params['title'];
194  if ( $title === null ) {
195  $titleProvided = false;
196  // A title is needed for parsing, so arbitrarily choose one
197  $title = 'API';
198  } else {
199  $titleProvided = true;
200  }
201 
202  $page = $params['page'];
203  $pageid = $params['pageid'];
204  $oldid = $params['oldid'];
205 
206  $prop = array_fill_keys( $params['prop'], true );
207 
208  if ( isset( $params['section'] ) ) {
209  $this->section = $params['section'];
210  if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
211  $this->dieWithError( 'apierror-invalidsection' );
212  }
213  } else {
214  $this->section = false;
215  }
216 
217  // The parser needs $wgTitle to be set, apparently the
218  // $title parameter in Parser::parse isn't enough *sigh*
219  // TODO: Does this still need $wgTitle?
220  global $wgTitle;
221 
222  $format = null;
223  $redirValues = null;
224 
225  $needContent = isset( $prop['wikitext'] ) ||
226  isset( $prop['parsetree'] ) || $params['generatexml'];
227 
228  // Return result
229  $result = $this->getResult();
230 
231  if ( $oldid !== null || $pageid !== null || $page !== null ) {
232  if ( $this->section === 'new' ) {
233  $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' );
234  }
235  if ( $oldid !== null ) {
236  // Don't use the parser cache
237  $rev = $this->revisionLookup->getRevisionById( $oldid );
238  if ( !$rev ) {
239  $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] );
240  }
241 
242  $this->checkTitleUserPermissions( $rev->getPage(), 'read' );
243 
244  if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
245  $this->dieWithError(
246  [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ]
247  );
248  }
249 
250  $revLinkTarget = $rev->getPageAsLinkTarget();
251  $titleObj = Title::newFromLinkTarget( $revLinkTarget );
252  $wgTitle = $titleObj;
253  $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
254  list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params );
255  $p_result = $this->getParsedContent(
256  $pageObj, $popts, $suppressCache, $pageid, $rev, $needContent
257  );
258  } else { // Not $oldid, but $pageid or $page
259  if ( $params['redirects'] ) {
260  $reqParams = [
261  'redirects' => '',
262  ];
263  $pageParams = [];
264  if ( $pageid !== null ) {
265  $reqParams['pageids'] = $pageid;
266  $pageParams['pageid'] = $pageid;
267  } else { // $page
268  $reqParams['titles'] = $page;
269  $pageParams['title'] = $page;
270  }
271  $req = new FauxRequest( $reqParams );
272  $main = new ApiMain( $req );
273  $pageSet = new ApiPageSet( $main );
274  $pageSet->execute();
275  $redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() );
276 
277  foreach ( $pageSet->getRedirectTitles() as $title ) {
278  $pageParams = [ 'title' => $title->getFullText() ];
279  }
280  } elseif ( $pageid !== null ) {
281  $pageParams = [ 'pageid' => $pageid ];
282  } else { // $page
283  $pageParams = [ 'title' => $page ];
284  }
285 
286  $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
287  $titleObj = $pageObj->getTitle();
288  if ( !$titleObj->exists() ) {
289  $this->dieWithError( 'apierror-missingtitle' );
290  }
291 
292  $this->checkTitleUserPermissions( $titleObj, 'read' );
293  $wgTitle = $titleObj;
294 
295  if ( isset( $prop['revid'] ) ) {
296  $oldid = $pageObj->getLatest();
297  }
298 
299  list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params );
300  $p_result = $this->getParsedContent(
301  $pageObj, $popts, $suppressCache, $pageid, null, $needContent
302  );
303  }
304  } else { // Not $oldid, $pageid, $page. Hence based on $text
305  $model = $params['contentmodel'];
306  $format = $params['contentformat'];
307 
308  $titleObj = Title::newFromText( $title );
309  if ( !$titleObj || $titleObj->isExternal() ) {
310  $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
311  }
312  $revid = $params['revid'];
313  if ( $revid !== null ) {
314  $rev = $this->revisionLookup->getRevisionById( $revid );
315  if ( !$rev ) {
316  $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] );
317  }
318  $pTitleObj = $titleObj;
319  $titleObj = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
320  if ( $titleProvided ) {
321  if ( !$titleObj->equals( $pTitleObj ) ) {
322  $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(),
323  wfEscapeWikiText( $pTitleObj->getPrefixedText() ) ] );
324  }
325  } else {
326  // Consider the title derived from the revid as having
327  // been provided.
328  $titleProvided = true;
329  }
330  }
331  $wgTitle = $titleObj;
332  if ( $titleObj->canExist() ) {
333  $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
334  list( $popts, $reset ) = $this->makeParserOptions( $pageObj, $params );
335  } else { // A special page, presumably
336  // XXX: Why is this needed at all? Can't we just fail?
337  $pageObj = null;
338  $popts = ParserOptions::newCanonical( $this->getContext() );
339  list( $popts, $reset ) = $this->tweakParserOptions( $popts, $titleObj, $params );
340  }
341 
342  $textProvided = $text !== null;
343 
344  if ( !$textProvided ) {
345  if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
346  if ( $revid !== null ) {
347  $this->addWarning( 'apiwarn-parse-revidwithouttext' );
348  } else {
349  $this->addWarning( 'apiwarn-parse-titlewithouttext' );
350  }
351  }
352  // Prevent warning from ContentHandler::makeContent()
353  $text = '';
354  }
355 
356  // If we are parsing text, do not use the content model of the default
357  // API title, but default to wikitext to keep BC.
358  if ( $textProvided && !$titleProvided && $model === null ) {
359  $model = CONTENT_MODEL_WIKITEXT;
360  $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] );
361  } elseif ( $model === null ) {
362  $model = $titleObj->getContentModel();
363  }
364 
365  $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
366  // Not in the default format, check supported or not
367  if ( $format && !$contentHandler->isSupportedFormat( $format ) ) {
368  $this->dieWithError( [ 'apierror-badformat-generic', $format, $model ] );
369  }
370 
371  try {
372  $this->content = $contentHandler->unserializeContent( $text, $format );
373  } catch ( MWContentSerializationException $ex ) {
374  $this->dieWithException( $ex, [
375  'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
376  ] );
377  }
378 
379  if ( $this->section !== false ) {
380  if ( $this->section === 'new' ) {
381  // Insert the section title above the content.
382  if ( $params['sectiontitle'] !== null ) {
383  $this->content = $this->content->addSectionHeader( $params['sectiontitle'] );
384  }
385  } else {
386  $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
387  }
388  }
389 
390  if ( $params['pst'] || $params['onlypst'] ) {
391  $this->pstContent = $this->contentTransformer->preSaveTransform(
392  $this->content,
393  $titleObj,
394  $this->getUser(),
395  $popts
396  );
397  }
398  if ( $params['onlypst'] ) {
399  // Build a result and bail out
400  $result_array = [];
401  $result_array['text'] = $this->pstContent->serialize( $format );
402  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
403  if ( isset( $prop['wikitext'] ) ) {
404  $result_array['wikitext'] = $this->content->serialize( $format );
405  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
406  }
407  if ( $params['summary'] !== null ||
408  ( $params['sectiontitle'] !== null && $this->section === 'new' )
409  ) {
410  $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
411  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
412  }
413 
414  $result->addValue( null, $this->getModuleName(), $result_array );
415 
416  return;
417  }
418 
419  // Not cached (save or load)
420  if ( $params['pst'] ) {
421  $p_result = $this->getContentParserOutput( $this->pstContent, $titleObj, $revid, $popts );
422  } else {
423  $p_result = $this->getContentParserOutput( $this->content, $titleObj, $revid, $popts );
424  }
425  }
426 
427  $result_array = [];
428 
429  $result_array['title'] = $titleObj->getPrefixedText();
430  $result_array['pageid'] = $pageid ?: $titleObj->getArticleID();
431  if ( $this->contentIsDeleted ) {
432  $result_array['textdeleted'] = true;
433  }
434  if ( $this->contentIsSuppressed ) {
435  $result_array['textsuppressed'] = true;
436  }
437 
438  if ( isset( $params['useskin'] ) ) {
439  $skin = $this->skinFactory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
440  } else {
441  $skin = null;
442  }
443 
444  $outputPage = null;
445  $context = null;
446  if (
447  $skin || isset( $prop['subtitle'] ) || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ||
448  isset( $params['mobileformat'] )
449  ) {
450  // Enabling the skin via 'useskin', 'subtitle', 'headhtml', or 'categorieshtml'
451  // gets OutputPage and Skin involved, which (among others) applies
452  // these hooks:
453  // - ParserOutputHooks
454  // - Hook: LanguageLinks
455  // - Hook: SkinSubPageSubtitle
456  // - Hook: OutputPageParserOutput
457  // - Hook: OutputPageMakeCategoryLinks
458  // - Hook: OutputPageBeforeHTML
459  // HACK Adding the 'mobileformat' parameter *also* enables the skin, for compatibility with legacy
460  // apps. This behavior should be considered deprecated so new users should not rely on this and
461  // always use the "useskin" parameter to enable "skin mode".
462  // Ideally this would be done with another hook so that MobileFrontend could enable skin mode, but
463  // as this is just for a deprecated feature, we are hard-coding this param into core.
464  $context = new DerivativeContext( $this->getContext() );
465  $context->setTitle( $titleObj );
466 
467  if ( $pageObj ) {
468  $context->setWikiPage( $pageObj );
469  }
470  // Some hooks only apply to pages when action=view, which this API
471  // call is simulating.
472  $context->setRequest( new FauxRequest( [ 'action' => 'view' ] ) );
473 
474  if ( $skin ) {
475  // Use the skin specified by 'useskin'
476  $context->setSkin( $skin );
477  // Context clones the skin, refetch to stay in sync. (T166022)
478  $skin = $context->getSkin();
479  } else {
480  // Make sure the context's skin refers to the context. Without this,
481  // $outputPage->getSkin()->getOutput() !== $outputPage which
482  // confuses some of the output.
483  $context->setSkin( $context->getSkin() );
484  }
485 
486  $outputPage = new OutputPage( $context );
487  // Required for subtitle to appear
488  $outputPage->setArticleFlag( true );
489 
490  $outputPage->addParserOutputMetadata( $p_result );
491  if ( $this->content ) {
492  $outputPage->addContentOverride( $titleObj, $this->content );
493  }
494  $context->setOutput( $outputPage );
495 
496  if ( $skin ) {
497  // Based on OutputPage::output()
498  $outputPage->loadSkinModules( $skin );
499  }
500 
501  $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
502  }
503 
504  if ( $oldid !== null ) {
505  $result_array['revid'] = (int)$oldid;
506  }
507 
508  if ( $params['redirects'] && $redirValues !== null ) {
509  $result_array['redirects'] = $redirValues;
510  }
511 
512  if ( isset( $prop['text'] ) ) {
513  $skin = $context ? $context->getSkin() : null;
514  $skinOptions = $skin ? $skin->getOptions() : [
515  'toc' => true,
516  ];
517  $result_array['text'] = $p_result->getText( [
518  'allowTOC' => !$params['disabletoc'],
519  'injectTOC' => $skinOptions['toc'],
520  'enableSectionEditLinks' => !$params['disableeditsection'],
521  'wrapperDivClass' => $params['wrapoutputclass'],
522  'deduplicateStyles' => !$params['disablestylededuplication'],
523  'skin' => $skin,
524  'includeDebugInfo' => !$params['disablepp'] && !$params['disablelimitreport']
525  ] );
526  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
527  if ( $context ) {
528  $this->getHookRunner()->onOutputPageBeforeHTML( $context->getOutput(), $result_array['text'] );
529  }
530  }
531 
532  if ( $params['summary'] !== null ||
533  ( $params['sectiontitle'] !== null && $this->section === 'new' )
534  ) {
535  $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
536  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
537  }
538 
539  if ( isset( $prop['langlinks'] ) ) {
540  if ( $skin ) {
541  $langlinks = $outputPage->getLanguageLinks();
542  } else {
543  $langlinks = $p_result->getLanguageLinks();
544  // The deprecated 'effectivelanglinks' option depredates OutputPage
545  // support via 'useskin'. If not already applied, then run just this
546  // one hook of OutputPage::addParserOutputMetadata here.
547  if ( $params['effectivelanglinks'] ) {
548  $linkFlags = [];
549  $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
550  }
551  }
552 
553  $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
554  }
555  if ( isset( $prop['categories'] ) ) {
556  $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
557  }
558  if ( isset( $prop['categorieshtml'] ) ) {
559  $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
560  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
561  }
562  if ( isset( $prop['links'] ) ) {
563  $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
564  }
565  if ( isset( $prop['templates'] ) ) {
566  $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
567  }
568  if ( isset( $prop['images'] ) ) {
569  $result_array['images'] = array_keys( $p_result->getImages() );
570  }
571  if ( isset( $prop['externallinks'] ) ) {
572  $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
573  }
574  if ( isset( $prop['sections'] ) ) {
575  $result_array['sections'] = $p_result->getSections();
576  $result_array['showtoc'] = (bool)$p_result->getTOCHTML();
577  }
578  if ( isset( $prop['parsewarnings'] ) ) {
579  $result_array['parsewarnings'] = $p_result->getWarnings();
580  }
581  if ( isset( $prop['parsewarningshtml'] ) ) {
582  $warnings = $p_result->getWarnings();
583  $warningsHtml = array_map( static function ( $warning ) {
584  return ( new RawMessage( '$1', [ $warning ] ) )->parse();
585  }, $warnings );
586  $result_array['parsewarningshtml'] = $warningsHtml;
587  }
588 
589  if ( isset( $prop['displaytitle'] ) ) {
590  $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
591  ? $p_result->getDisplayTitle()
592  : htmlspecialchars( $titleObj->getPrefixedText(), ENT_NOQUOTES );
593  }
594 
595  if ( isset( $prop['subtitle'] ) ) {
596  $result_array['subtitle'] = $context->getSkin()->prepareSubtitle();
597  }
598 
599  if ( isset( $prop['headitems'] ) ) {
600  if ( $skin ) {
601  $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
602  } else {
603  $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
604  }
605  }
606 
607  if ( isset( $prop['headhtml'] ) ) {
608  $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
609  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
610  }
611 
612  if ( isset( $prop['modules'] ) ) {
613  if ( $skin ) {
614  $result_array['modules'] = $outputPage->getModules();
615  // Deprecated since 1.32 (T188689)
616  $result_array['modulescripts'] = [];
617  $result_array['modulestyles'] = $outputPage->getModuleStyles();
618  } else {
619  $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
620  // Deprecated since 1.32 (T188689)
621  $result_array['modulescripts'] = [];
622  $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
623  }
624  }
625 
626  if ( isset( $prop['jsconfigvars'] ) ) {
627  $showStrategyKeys = (bool)( $params['showstrategykeys'] );
628  $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars( $showStrategyKeys );
629  $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
630  }
631 
632  if ( isset( $prop['encodedjsconfigvars'] ) ) {
633  $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
634  $result_array['encodedjsconfigvars'] = FormatJson::encode(
635  $jsconfigvars,
636  false,
638  );
639  $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
640  }
641 
642  if ( isset( $prop['modules'] ) &&
643  !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
644  $this->addWarning( 'apiwarn-moduleswithoutvars' );
645  }
646 
647  if ( isset( $prop['indicators'] ) ) {
648  if ( $skin ) {
649  $result_array['indicators'] = (array)$outputPage->getIndicators();
650  } else {
651  $result_array['indicators'] = (array)$p_result->getIndicators();
652  }
653  ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
654  }
655 
656  if ( isset( $prop['iwlinks'] ) ) {
657  $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
658  }
659 
660  if ( isset( $prop['wikitext'] ) ) {
661  $result_array['wikitext'] = $this->content->serialize( $format );
662  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
663  // @phan-suppress-next-line PhanImpossibleTypeComparison
664  if ( $this->pstContent !== null ) {
665  $result_array['psttext'] = $this->pstContent->serialize( $format );
666  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
667  }
668  }
669  if ( isset( $prop['properties'] ) ) {
670  $result_array['properties'] = $p_result->getPageProperties();
671  ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
672  }
673 
674  if ( isset( $prop['limitreportdata'] ) ) {
675  $result_array['limitreportdata'] =
676  $this->formatLimitReportData( $p_result->getLimitReportData() );
677  }
678  if ( isset( $prop['limitreporthtml'] ) ) {
679  $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
680  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
681  }
682 
683  if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
684  if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
685  $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
686  }
687 
688  $this->parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
689  // @phan-suppress-next-line PhanUndeclaredMethod
690  $xml = $this->parser->preprocessToDom( $this->content->getText() )->__toString();
691  $result_array['parsetree'] = $xml;
692  $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
693  }
694 
695  $result_mapping = [
696  'redirects' => 'r',
697  'langlinks' => 'll',
698  'categories' => 'cl',
699  'links' => 'pl',
700  'templates' => 'tl',
701  'images' => 'img',
702  'externallinks' => 'el',
703  'iwlinks' => 'iw',
704  'sections' => 's',
705  'headitems' => 'hi',
706  'modules' => 'm',
707  'indicators' => 'ind',
708  'modulescripts' => 'm',
709  'modulestyles' => 'm',
710  'properties' => 'pp',
711  'limitreportdata' => 'lr',
712  'parsewarnings' => 'pw',
713  'parsewarningshtml' => 'pw',
714  ];
715  $this->setIndexedTagNames( $result_array, $result_mapping );
716  $result->addValue( null, $this->getModuleName(), $result_array );
717  }
718 
727  private function makeParserOptions( WikiPage $pageObj, array $params ) {
728  $popts = $pageObj->makeParserOptions( $this->getContext() );
729  return $this->tweakParserOptions( $popts, $pageObj->getTitle(), $params );
730  }
731 
741  private function tweakParserOptions( ParserOptions $popts, Title $title, array $params ) {
742  $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
743  $popts->setIsSectionPreview( $params['sectionpreview'] );
744 
745  if ( $params['wrapoutputclass'] !== '' ) {
746  $popts->setWrapOutputClass( $params['wrapoutputclass'] );
747  }
748 
749  $reset = null;
750  $suppressCache = false;
751  $this->getHookRunner()->onApiMakeParserOptions( $popts, $title,
752  $params, $this, $reset, $suppressCache );
753 
754  return [ $popts, $reset, $suppressCache ];
755  }
756 
766  private function getParsedContent(
767  WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
768  ) {
769  $revId = $rev ? $rev->getId() : null;
770  $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
771 
772  if ( $getContent || $this->section !== false || $isDeleted ) {
773  if ( $rev ) {
774  $this->content = $rev->getContent(
775  SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getAuthority()
776  );
777  if ( !$this->content ) {
778  $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
779  }
780  } else {
781  $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
782  if ( !$this->content ) {
783  $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
784  }
785  }
786  $this->contentIsDeleted = $isDeleted;
787  $this->contentIsSuppressed = $rev &&
788  $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
789  }
790 
791  if ( $this->section !== false ) {
792  $this->content = $this->getSectionContent(
793  $this->content,
794  $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
795  );
796  return $this->getContentParserOutput( $this->content, $page->getTitle(), $revId, $popts );
797  }
798 
799  if ( $isDeleted ) {
800  // getParserOutput can't do revdeled revisions
801 
802  $pout = $this->getContentParserOutput( $this->content, $page->getTitle(), $revId, $popts );
803  } else {
804  // getParserOutput will save to Parser cache if able
805  $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
806  }
807  if ( !$pout ) {
808  // @codeCoverageIgnoreStart
809  $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
810  // @codeCoverageIgnoreEnd
811  }
812 
813  return $pout;
814  }
815 
823  private function getSectionContent( Content $content, $what ) {
824  // Not cached (save or load)
825  $section = $content->getSection( $this->section );
826  if ( $section === false ) {
827  $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
828  }
829  if ( $section === null ) {
830  $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
831  }
832 
833  // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
834  return $section;
835  }
836 
844  private function formatSummary( $title, $params ) {
845  $summary = $params['summary'] ?? '';
846  $sectionTitle = $params['sectiontitle'] ?? '';
847 
848  if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
849  if ( $sectionTitle !== '' ) {
850  $summary = $params['sectiontitle'];
851  }
852  if ( $summary !== '' ) {
853  $summary = wfMessage( 'newsectionsummary' )
854  ->rawParams( $this->parser->stripSectionName( $summary ) )
855  ->inContentLanguage()->text();
856  }
857  }
858  return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
859  }
860 
861  private function formatLangLinks( $links ) {
862  $result = [];
863  foreach ( $links as $link ) {
864  $entry = [];
865  $bits = explode( ':', $link, 2 );
866  $title = Title::newFromText( $link );
867 
868  $entry['lang'] = $bits[0];
869  if ( $title ) {
870  $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
871  // localised language name in 'uselang' language
872  $entry['langname'] = $this->languageNameUtils->getLanguageName(
873  $title->getInterwiki(),
874  $this->getLanguage()->getCode()
875  );
876 
877  // native language name
878  $entry['autonym'] = $this->languageNameUtils->getLanguageName( $title->getInterwiki() );
879  }
880  ApiResult::setContentValue( $entry, 'title', $bits[1] );
881  $result[] = $entry;
882  }
883 
884  return $result;
885  }
886 
887  private function formatCategoryLinks( $links ) {
888  $result = [];
889 
890  if ( !$links ) {
891  return $result;
892  }
893 
894  // Fetch hiddencat property
895  $lb = $this->linkBatchFactory->newLinkBatch();
896  $lb->setArray( [ NS_CATEGORY => $links ] );
897  $db = $this->getDB();
898  $res = $db->select( [ 'page', 'page_props' ],
899  [ 'page_title', 'pp_propname' ],
900  $lb->constructSet( 'page', $db ),
901  __METHOD__,
902  [],
903  [ 'page_props' => [
904  'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
905  ] ]
906  );
907  $hiddencats = [];
908  foreach ( $res as $row ) {
909  $hiddencats[$row->page_title] = isset( $row->pp_propname );
910  }
911 
912  foreach ( $links as $link => $sortkey ) {
913  $entry = [];
914  $entry['sortkey'] = $sortkey;
915  // array keys will cast numeric category names to ints, so cast back to string
916  ApiResult::setContentValue( $entry, 'category', (string)$link );
917  if ( !isset( $hiddencats[$link] ) ) {
918  $entry['missing'] = true;
919 
920  // We already know the link doesn't exist in the database, so
921  // tell LinkCache that before calling $title->isKnown().
922  $title = Title::makeTitle( NS_CATEGORY, $link );
923  $this->linkCache->addBadLinkObj( $title );
924  if ( $title->isKnown() ) {
925  $entry['known'] = true;
926  }
927  } elseif ( $hiddencats[$link] ) {
928  $entry['hidden'] = true;
929  }
930  $result[] = $entry;
931  }
932 
933  return $result;
934  }
935 
936  private function formatLinks( $links ) {
937  $result = [];
938  foreach ( $links as $ns => $nslinks ) {
939  foreach ( $nslinks as $title => $id ) {
940  $entry = [];
941  $entry['ns'] = $ns;
942  ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
943  $entry['exists'] = $id != 0;
944  $result[] = $entry;
945  }
946  }
947 
948  return $result;
949  }
950 
951  private function formatIWLinks( $iw ) {
952  $result = [];
953  foreach ( $iw as $prefix => $titles ) {
954  foreach ( array_keys( $titles ) as $title ) {
955  $entry = [];
956  $entry['prefix'] = $prefix;
957 
958  $title = Title::newFromText( "{$prefix}:{$title}" );
959  if ( $title ) {
960  $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
961  }
962 
963  ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
964  $result[] = $entry;
965  }
966  }
967 
968  return $result;
969  }
970 
971  private function formatHeadItems( $headItems ) {
972  $result = [];
973  foreach ( $headItems as $tag => $content ) {
974  $entry = [];
975  $entry['tag'] = $tag;
976  ApiResult::setContentValue( $entry, 'content', $content );
977  $result[] = $entry;
978  }
979 
980  return $result;
981  }
982 
983  private function formatLimitReportData( $limitReportData ) {
984  $result = [];
985 
986  foreach ( $limitReportData as $name => $value ) {
987  $entry = [];
988  $entry['name'] = $name;
989  if ( !is_array( $value ) ) {
990  $value = [ $value ];
991  }
992  ApiResult::setIndexedTagNameRecursive( $value, 'param' );
993  $entry = array_merge( $entry, $value );
994  $result[] = $entry;
995  }
996 
997  return $result;
998  }
999 
1000  private function setIndexedTagNames( &$array, $mapping ) {
1001  foreach ( $mapping as $key => $name ) {
1002  if ( isset( $array[$key] ) ) {
1003  ApiResult::setIndexedTagName( $array[$key], $name );
1004  }
1005  }
1006  }
1007 
1008  public function getAllowedParams() {
1009  return [
1010  'title' => null,
1011  'text' => [
1012  ParamValidator::PARAM_TYPE => 'text',
1013  ],
1014  'revid' => [
1015  ParamValidator::PARAM_TYPE => 'integer',
1016  ],
1017  'summary' => null,
1018  'page' => null,
1019  'pageid' => [
1020  ParamValidator::PARAM_TYPE => 'integer',
1021  ],
1022  'redirects' => false,
1023  'oldid' => [
1024  ParamValidator::PARAM_TYPE => 'integer',
1025  ],
1026  'prop' => [
1027  ParamValidator::PARAM_DEFAULT => 'text|langlinks|categories|links|templates|' .
1028  'images|externallinks|sections|revid|displaytitle|iwlinks|' .
1029  'properties|parsewarnings',
1030  ParamValidator::PARAM_ISMULTI => true,
1031  ParamValidator::PARAM_TYPE => [
1032  'text',
1033  'langlinks',
1034  'categories',
1035  'categorieshtml',
1036  'links',
1037  'templates',
1038  'images',
1039  'externallinks',
1040  'sections',
1041  'revid',
1042  'displaytitle',
1043  'subtitle',
1044  'headhtml',
1045  'modules',
1046  'jsconfigvars',
1047  'encodedjsconfigvars',
1048  'indicators',
1049  'iwlinks',
1050  'wikitext',
1051  'properties',
1052  'limitreportdata',
1053  'limitreporthtml',
1054  'parsetree',
1055  'parsewarnings',
1056  'parsewarningshtml',
1057  'headitems',
1058  ],
1060  'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
1061  ],
1062  EnumDef::PARAM_DEPRECATED_VALUES => [
1063  'headitems' => 'apiwarn-deprecation-parse-headitems',
1064  ],
1065  ],
1066  'wrapoutputclass' => 'mw-parser-output',
1067  'pst' => false,
1068  'onlypst' => false,
1069  'effectivelanglinks' => [
1070  ParamValidator::PARAM_DEFAULT => false,
1071  ParamValidator::PARAM_DEPRECATED => true,
1072  ],
1073  'section' => null,
1074  'sectiontitle' => [
1075  ParamValidator::PARAM_TYPE => 'string',
1076  ],
1077  'disablepp' => [
1078  ParamValidator::PARAM_DEFAULT => false,
1079  ParamValidator::PARAM_DEPRECATED => true,
1080  ],
1081  'disablelimitreport' => false,
1082  'disableeditsection' => false,
1083  'disablestylededuplication' => false,
1084  'showstrategykeys' => false,
1085  'generatexml' => [
1086  ParamValidator::PARAM_DEFAULT => false,
1088  'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
1089  ],
1090  ParamValidator::PARAM_DEPRECATED => true,
1091  ],
1092  'preview' => false,
1093  'sectionpreview' => false,
1094  'disabletoc' => false,
1095  'useskin' => [
1096  // T237856; We use all installed skins here to allow hidden (but usable) skins
1097  // to continue working correctly with some features such as Live Preview
1098  ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
1099  ],
1100  'contentformat' => [
1101  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
1102  ],
1103  'contentmodel' => [
1104  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
1105  ],
1106  ];
1107  }
1108 
1109  protected function getExamplesMessages() {
1110  return [
1111  'action=parse&page=Project:Sandbox'
1112  => 'apihelp-parse-example-page',
1113  'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
1114  => 'apihelp-parse-example-text',
1115  'action=parse&text={{PAGENAME}}&title=Test'
1116  => 'apihelp-parse-example-texttitle',
1117  'action=parse&summary=Some+[[link]]&prop='
1118  => 'apihelp-parse-example-summary',
1119  ];
1120  }
1121 
1122  public function getHelpUrls() {
1123  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
1124  }
1125 }
getUser()
getAuthority()
getDB()
const PROTO_CURRENT
Definition: Defines.php:198
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:211
const NS_CATEGORY
Definition: Defines.php:78
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition: Setup.php:497
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:56
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:196
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:163
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:52
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:43
This class contains a list of pages that the client has requested.
Definition: ApiPageSet.php:50
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiParse.php:1008
__construct(ApiMain $main, $action, RevisionLookup $revisionLookup, SkinFactory $skinFactory, LanguageNameUtils $languageNameUtils, LinkBatchFactory $linkBatchFactory, LinkCache $linkCache, IContentHandlerFactory $contentHandlerFactory, Parser $parser, WikiPageFactory $wikiPageFactory, ContentRenderer $contentRenderer, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter)
Definition: ApiParse.php:102
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiParse.php:1109
bool $contentIsSuppressed
Definition: ApiParse.php:52
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiParse.php:1122
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiParse.php:179
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Definition: ApiResult.php:716
static addMetadataToResultVars( $vars, $forceHash=true)
Add the correct metadata to an array of vars we want to export through the API.
Definition: ApiResult.php:1144
const META_SUBELEMENTS
Key for the 'subelements' metadata item.
Definition: ApiResult.php:78
const META_BC_SUBELEMENTS
Key for the 'BC subelements' metadata item.
Definition: ApiResult.php:143
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
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:467
static setIndexedTagNameRecursive(array &$arr, $tag)
Set indexed tag name on $arr and all subarrays.
Definition: ApiResult.php:629
An IContextSource implementation which will inherit context from another source but allow individual ...
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:4070
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:37
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
const ALL_OK
Skip escaping as many characters as reasonably possible.
Definition: FormatJson.php:55
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition: LinkCache.php:42
Exception representing a failure to serialize or unserialize a content object.
This is the main service interface for converting single-line comments from various DB comment fields...
A service that provides utilities to do with language names and codes.
Service for creating WikiPage objects.
Page revision base class.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:54
Set options of the Parser.
setIsSectionPreview( $x)
Parsing the page for a "preview" operation on a single section?
setIsPreview( $x)
Parsing the page for a "preview" operation?
static newCanonical( $context, $userLang=null)
Creates a "canonical" ParserOptions object.
setWrapOutputClass( $className)
CSS class to use to wrap output from Parser::parse()
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:95
const OT_PREPROCESS
Definition: Parser.php:131
Convenience class for dealing with PoolCounters using callbacks.
execute( $skipcache=false)
Get the result of the work (whatever it is), or the result of the error() function.
Variant of the Message class.
Definition: RawMessage.php:35
Factory class to create Skin objects.
Definition: SkinFactory.php:31
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:192
Represents a title within MediaWiki.
Definition: Title.php:49
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:282
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
Helper tools for dealing with other locally-hosted wikis.
Definition: WikiMap.php:29
Base representation for an editable wiki page.
Definition: WikiPage.php:62
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:825
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1934
getParserOutput(?ParserOptions $parserOptions=null, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1270
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition: WikiPage.php:740
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:572
getTitle()
Get the title object of the article.
Definition: WikiPage.php:303
Service for formatting and validating API parameters.
Type definition for enumeration types.
Definition: EnumDef.php:32
Base interface for content objects.
Definition: Content.php:35
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Service for looking up page revisions.
$content
Definition: router.php:76