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