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