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