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