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