MediaWiki master
ApiParse.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
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 readonly RevisionLookup $revisionLookup,
78 private readonly SkinFactory $skinFactory,
79 private readonly LanguageNameUtils $languageNameUtils,
80 private readonly LinkBatchFactory $linkBatchFactory,
81 private readonly LinkCache $linkCache,
82 private readonly IContentHandlerFactory $contentHandlerFactory,
83 private readonly ParserFactory $parserFactory,
84 private readonly WikiPageFactory $wikiPageFactory,
85 private readonly ContentRenderer $contentRenderer,
86 private readonly ContentTransformer $contentTransformer,
87 private readonly CommentFormatter $commentFormatter,
88 private readonly TempUserCreator $tempUserCreator,
89 private readonly UserFactory $userFactory,
90 private readonly UrlUtils $urlUtils,
91 private readonly TitleFormatter $titleFormatter,
92 private readonly 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 ( ContentSerializationException $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 // Parsoid displaytitle is set during the postprocessing pipeline
493 if ( isset( $prop['text'] ) || isset( $prop['displaytitle'] ) ) {
494 $skin = $context ? $context->getSkin() : null;
495 $skinOptions = $skin ? $skin->getOptions() : [
496 'toc' => true,
497 ];
498 // TODO T371004 move runOutputPipeline out of $parserOutput
499 // TODO T371022 it should be reasonably straightforward to move this to a clone, but it requires
500 // careful checking of the clone and of what happens on the boundary of OutputPage. Leaving this as
501 // "getText-equivalent" for now; will fix in a later, independent patch.
502 $oldText = $p_result->getContentHolderText();
503 $newText = $p_result->runOutputPipeline( $popts, [
504 // This will have side effects on $p_result (T371022)
505 'allowClone' => false,
506 'allowTOC' => !$params['disabletoc'],
507 'injectTOC' => $skinOptions['toc'],
508 'wrapperDivClass' => $params['wrapoutputclass'],
509 'deduplicateStyles' => !$params['disablestylededuplication'],
510 'userLang' => $context ? $context->getLanguage() : null,
511 'skin' => $skin,
512 'includeDebugInfo' => !$params['disablepp'] && !$params['disablelimitreport']
513 ] )->getContentHolderText();
514 $p_result->setContentHolderText( $oldText );
515 if ( isset( $prop['text'] ) ) {
516 $result_array['text'] = $newText;
517 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
518 if ( $context ) {
519 $this->getHookRunner()->onOutputPageBeforeHTML( $context->getOutput(), $result_array['text'] );
520 }
521 }
522 }
523
524 if ( $outputPage ) {
525 // This needs to happen after running the OutputTransform pipeline so that the metadata inserted by
526 // the pipeline is also added to the OutputPage
527 $outputPage->addParserOutputMetadata( $p_result );
528
529 $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
530 }
531
532 if ( $params['summary'] !== null ||
533 ( $params['sectiontitle'] !== null && $this->section === 'new' )
534 ) {
535 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
536 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
537 }
538
539 if ( isset( $prop['langlinks'] ) ) {
540 if ( $skin ) {
541 $langlinks = $outputPage->getLanguageLinks();
542 } else {
543 $langlinks = array_map(
544 static fn ( $item ) => $item['link'],
545 $p_result->getLinkList( ParserOutputLinkTypes::LANGUAGE )
546 );
547 // The deprecated 'effectivelanglinks' option pre-dates OutputPage
548 // support via 'useskin'. If not already applied, then run just this
549 // one hook of OutputPage::addParserOutputMetadata here.
550 if ( $params['effectivelanglinks'] ) {
551 # for compatibility with old hook, convert to string[]
552 $compat = [];
553 foreach ( $langlinks as $link ) {
554 $s = $link->getInterwiki() . ':' . $link->getText();
555 if ( $link->hasFragment() ) {
556 $s .= '#' . $link->getFragment();
557 }
558 $compat[] = $s;
559 }
560 $langlinks = $compat;
561 $linkFlags = [];
562 sort( $langlinks );
563 $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
564 }
565 }
566
567 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
568 }
569 if ( isset( $prop['categories'] ) ) {
570 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategoryMap() );
571 }
572 if ( isset( $prop['categorieshtml'] ) ) {
573 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
574 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
575 }
576 if ( isset( $prop['links'] ) ) {
577
578 $result_array['links'] = $this->formatLinks( $p_result->getLinkList( ParserOutputLinkTypes::LOCAL ) );
579 }
580 if ( isset( $prop['templates'] ) ) {
581 $result_array['templates'] = $this->formatLinks(
582 $p_result->getLinkList( ParserOutputLinkTypes::TEMPLATE )
583 );
584 }
585 if ( isset( $prop['images'] ) ) {
586 $result_array['images'] = array_map(
587 static fn ( $item ) => $item['link']->getDBkey(),
588 $p_result->getLinkList( ParserOutputLinkTypes::MEDIA )
589 );
590 }
591 if ( isset( $prop['externallinks'] ) ) {
592 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
593 }
594 if ( isset( $prop['sections'] ) ) {
595 $result_array['sections'] = $p_result->getSections();
596 }
597 if ( isset( $prop['tocdata'] ) ) {
598 $result_array['tocdata'] = $this->jsonCodec->toJsonArray(
599 $p_result->getTOCData(), TOCData::class
600 );
601 }
602 if ( isset( $prop['sections'] ) || isset( $prop['tocdata'] ) ) {
603 $result_array['showtoc'] = $p_result->getOutputFlag( ParserOutputFlags::SHOW_TOC );
604 }
605 if ( isset( $prop['parsewarnings'] ) || isset( $prop['parsewarningshtml'] ) ) {
606 $warningMsgs = array_map(
607 static fn ( $mv ) => Message::newFromSpecifier( $mv )
608 ->page( $titleObj )
609 // Note that we use ContentLanguage here
610 ->inContentLanguage(),
611 $p_result->getWarningMsgs()
612 );
613 if ( $warningMsgs === [] ) {
614 // Backward compatibility with cached ParserOutput from
615 // MW <= 1.45 which didn't store the MessageValues (T343048)
616 $warningMsgs = array_map(
617 static fn ( $warning ) => new RawMessage( '$1', [ $warning ] ),
618 $p_result->getWarnings()
619 );
620 }
621 if ( isset( $prop['parsewarnings'] ) ) {
622 $warnings = array_map( static fn ( $msg ) => $msg->text(), $warningMsgs );
623 $result_array['parsewarnings'] = $warnings;
624 }
625 if ( isset( $prop['parsewarningshtml'] ) ) {
626 $warningsHtml = array_map( static fn ( $msg ) => $msg->parse(), $warningMsgs );
627 $result_array['parsewarningshtml'] = $warningsHtml;
628 }
629 }
630
631 if ( isset( $prop['displaytitle'] ) ) {
632 $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
633 ? $p_result->getDisplayTitle()
634 : htmlspecialchars( $titleObj->getPrefixedText(), ENT_NOQUOTES );
635 }
636
637 if ( isset( $prop['subtitle'] ) ) {
638 // Get the subtitle without its container element to support UI refreshing
639 $result_array['subtitle'] = $context->getSkin()->prepareSubtitle( false );
640 }
641
642 if ( isset( $prop['headitems'] ) ) {
643 if ( $skin ) {
644 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
645 } else {
646 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
647 }
648 }
649
650 if ( isset( $prop['headhtml'] ) ) {
651 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
652 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
653 }
654
655 if ( isset( $prop['modules'] ) ) {
656 if ( $skin ) {
657 $result_array['modules'] = $outputPage->getModules();
658 // Deprecated since 1.32 (T188689)
659 $result_array['modulescripts'] = [];
660 $result_array['modulestyles'] = $outputPage->getModuleStyles();
661 } else {
662 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
663 // Deprecated since 1.32 (T188689)
664 $result_array['modulescripts'] = [];
665 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
666 }
667 }
668
669 if ( isset( $prop['jsconfigvars'] ) ) {
670 $showStrategyKeys = (bool)( $params['showstrategykeys'] );
671 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars( $showStrategyKeys );
672 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
673 }
674
675 if ( isset( $prop['encodedjsconfigvars'] ) ) {
676 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
677 $result_array['encodedjsconfigvars'] = FormatJson::encode(
678 $jsconfigvars,
679 false,
680 FormatJson::ALL_OK
681 );
682 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
683 }
684
685 if ( isset( $prop['modules'] ) &&
686 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
687 $this->addWarning( 'apiwarn-moduleswithoutvars' );
688 }
689
690 if ( isset( $prop['indicators'] ) ) {
691 if ( $skin ) {
692 $result_array['indicators'] = $outputPage->getIndicators();
693 } else {
694 $result_array['indicators'] = $p_result->getIndicators();
695 }
696 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
697 }
698
699 if ( isset( $prop['iwlinks'] ) ) {
700 $links = array_map(
701 static fn ( $item ) => $item['link'],
702 $p_result->getLinkList( ParserOutputLinkTypes::INTERWIKI )
703 );
704 $result_array['iwlinks'] = $this->formatIWLinks( $links );
705 }
706
707 if ( isset( $prop['wikitext'] ) ) {
708 $result_array['wikitext'] = $this->content->serialize( $format );
709 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
710 // @phan-suppress-next-line PhanImpossibleTypeComparison
711 if ( $this->pstContent !== null ) {
712 $result_array['psttext'] = $this->pstContent->serialize( $format );
713 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
714 }
715 }
716 if ( isset( $prop['properties'] ) ) {
717 $result_array['properties'] = $p_result->getPageProperties();
718 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
719 }
720
721 if ( isset( $prop['limitreportdata'] ) ) {
722 $result_array['limitreportdata'] =
723 $this->formatLimitReportData( $p_result->getLimitReportData() );
724 }
725 if ( isset( $prop['limitreporthtml'] ) ) {
726 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
727 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
728 }
729
730 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
731 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
732 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
733 }
734
735 $parser = $this->parserFactory->getInstance();
736 $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
737 // @phan-suppress-next-line PhanUndeclaredMethod
738 $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
739 $result_array['parsetree'] = $xml;
740 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
741 }
742
743 $result_mapping = [
744 'redirects' => 'r',
745 'langlinks' => 'll',
746 'categories' => 'cl',
747 'links' => 'pl',
748 'templates' => 'tl',
749 'images' => 'img',
750 'externallinks' => 'el',
751 'iwlinks' => 'iw',
752 'sections' => 's',
753 'tocdata' => 'toc',
754 'headitems' => 'hi',
755 'modules' => 'm',
756 'indicators' => 'ind',
757 'modulescripts' => 'm',
758 'modulestyles' => 'm',
759 'properties' => 'pp',
760 'limitreportdata' => 'lr',
761 'parsewarnings' => 'pw',
762 'parsewarningshtml' => 'pw',
763 ];
764 $this->setIndexedTagNames( $result_array, $result_mapping );
765 $result->addValue( null, $this->getModuleName(), $result_array );
766 }
767
776 private function makeParserOptions( WikiPage $pageObj, array $params ) {
777 if ( $params['usearticle'] ) {
778 # T349037: The ArticleParserOptions hook should be broadened to take
779 # a WikiPage (aka $pageObj) instead of an Article. But for now
780 # fake the Article.
781 $article = Article::newFromWikiPage( $pageObj, $this->getContext() );
782 # Allow extensions to vary parser options used for article rendering,
783 # in the same way Article does
784 $popts = $article->getParserOptions();
785 } else {
786 $popts = $pageObj->makeParserOptions( $this->getContext() );
787 }
788 // Overwrite render reason
789 $popts->setRenderReason( 'api-parse' );
790 return $this->tweakParserOptions( $popts, $pageObj->getTitle(), $params );
791 }
792
802 private function tweakParserOptions( ParserOptions $popts, Title $title, array $params ) {
803 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
804 $popts->setIsSectionPreview( $params['sectionpreview'] );
805
806 if ( $params['wrapoutputclass'] !== '' ) {
807 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
808 }
809 if ( $params['parsoid'] || $params['parser'] === 'parsoid' ) {
810 $popts->setUseParsoid( true );
811 }
812 if ( $params['parser'] === 'legacy' ) {
813 $popts->setUseParsoid( false );
814 }
815 if ( $params['disableeditsection'] ) {
816 $popts->setSuppressSectionEditLinks();
817 }
818
819 $reset = null;
820 $suppressCache = false;
821 $this->getHookRunner()->onApiMakeParserOptions( $popts, $title,
822 $params, $this, $reset, $suppressCache );
823
824 return [ $popts, $reset, $suppressCache ];
825 }
826
836 private function getParsedContent(
837 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
838 ) {
839 $revId = $rev ? $rev->getId() : null;
840 $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
841
842 if ( $getContent || $this->section !== false || $isDeleted ) {
843 if ( $rev ) {
844 $this->content = $rev->getContent(
845 SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getAuthority()
846 );
847 if ( !$this->content ) {
848 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
849 }
850 } else {
851 $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
852 if ( !$this->content ) {
853 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
854 }
855 }
856 $this->contentIsDeleted = $isDeleted;
857 $this->contentIsSuppressed = $rev &&
858 $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
859 }
860
861 if ( $this->section !== false ) {
862 $this->content = $this->getSectionContent(
863 $this->content,
864 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
865 );
866 return $this->getContentParserOutput(
867 $this->content, $page->getTitle(),
868 $rev,
869 $popts
870 );
871 }
872
873 if ( $isDeleted ) {
874 // getParserOutput can't do revdeled revisions
875
876 $pout = $this->getContentParserOutput(
877 $this->content, $page->getTitle(),
878 $rev,
879 $popts
880 );
881 } else {
882 // getParserOutput will save to Parser cache if able
883 $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
884 }
885 if ( !$pout ) {
886 // @codeCoverageIgnoreStart
887 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
888 // @codeCoverageIgnoreEnd
889 }
890
891 return $pout;
892 }
893
901 private function getSectionContent( Content $content, $what ) {
902 // Not cached (save or load)
903 $section = $content->getSection( $this->section );
904 if ( $section === false ) {
905 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
906 }
907 if ( $section === null ) {
908 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
909 }
910
911 return $section;
912 }
913
921 private function formatSummary( $title, $params ) {
922 $summary = $params['summary'] ?? '';
923 $sectionTitle = $params['sectiontitle'] ?? '';
924
925 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
926 if ( $sectionTitle !== '' ) {
927 $summary = $params['sectiontitle'];
928 }
929 if ( $summary !== '' ) {
930 $summary = $this->msg( 'newsectionsummary' )
931 ->rawParams( $this->parserFactory->getMainInstance()->stripSectionName( $summary ) )
932 ->inContentLanguage()->text();
933 }
934 }
935 return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
936 }
937
942 private function formatLangLinks( $links ): array {
943 $result = [];
944 foreach ( $links as $link ) {
945 $entry = [];
946 if ( is_string( $link ) ) {
947 [ $lang, $titleWithFrag ] = explode( ':', $link, 2 );
948 [ $title, $frag ] = array_pad( explode( '#', $titleWithFrag, 2 ), 2, '' );
949 $title = TitleValue::tryNew( NS_MAIN, $title, $frag, $lang );
950 } else {
951 $title = $link;
952 $lang = $link->getInterwiki();
953 $titleWithFrag = $link->getText();
954 if ( $link->hasFragment() ) {
955 $titleWithFrag .= '#' . $link->getFragment();
956 }
957 }
958 $title = Title::castFromLinkTarget( $title );
959
960 $entry['lang'] = $lang;
961 if ( $title ) {
962 $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
963 // title validity implies language code validity
964 // localised language name in 'uselang' language
965 $entry['langname'] = $this->languageNameUtils->getLanguageName(
966 $lang,
967 $this->getLanguage()->getCode()
968 );
969
970 // native language name
971 $entry['autonym'] = $this->languageNameUtils->getLanguageName( $lang );
972 }
973 ApiResult::setContentValue( $entry, 'title', $titleWithFrag );
974 $result[] = $entry;
975 }
976
977 return $result;
978 }
979
980 private function formatCategoryLinks( array $links ): array {
981 $result = [];
982
983 if ( !$links ) {
984 return $result;
985 }
986
987 // Fetch hiddencat property
988 $lb = $this->linkBatchFactory->newLinkBatch();
989 $lb->setArray( [ NS_CATEGORY => $links ] );
990 $db = $this->getDB();
991 $res = $db->newSelectQueryBuilder()
992 ->select( [ 'page_title', 'pp_propname' ] )
993 ->from( 'page' )
994 ->where( $lb->constructSet( 'page', $db ) )
995 ->leftJoin( 'page_props', null, [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] )
996 ->caller( __METHOD__ )
997 ->fetchResultSet();
998 $hiddencats = [];
999 foreach ( $res as $row ) {
1000 $hiddencats[$row->page_title] = isset( $row->pp_propname );
1001 }
1002
1003 foreach ( $links as $link => $sortkey ) {
1004 $entry = [];
1005 $entry['sortkey'] = $sortkey;
1006 // array keys will cast numeric category names to ints, so cast back to string
1007 ApiResult::setContentValue( $entry, 'category', (string)$link );
1008 if ( !isset( $hiddencats[$link] ) ) {
1009 $entry['missing'] = true;
1010
1011 // We already know the link doesn't exist in the database, so
1012 // tell LinkCache that before calling $title->isKnown().
1013 $title = Title::makeTitle( NS_CATEGORY, $link );
1014 $this->linkCache->addBadLinkObj( $title );
1015 if ( $title->isKnown() ) {
1016 $entry['known'] = true;
1017 }
1018 } elseif ( $hiddencats[$link] ) {
1019 $entry['hidden'] = true;
1020 }
1021 $result[] = $entry;
1022 }
1023
1024 return $result;
1025 }
1026
1031 private function formatLinks( array $links ): array {
1032 $result = [];
1033 foreach ( $links as [ 'link' => $link, 'pageid' => $id ] ) {
1034 $entry = [];
1035 $entry['ns'] = $link->getNamespace();
1036 ApiResult::setContentValue( $entry, 'title', Title::newFromLinkTarget( $link )->getFullText() );
1037 $entry['exists'] = $id != 0;
1038 $result[] = $entry;
1039 }
1040
1041 return $result;
1042 }
1043
1044 private function formatIWLinks( array $iw ): array {
1045 $result = [];
1046 foreach ( $iw as $linkTarget ) {
1047 $entry = [];
1048 $entry['prefix'] = $linkTarget->getInterwiki();
1049 $title = Title::newFromLinkTarget( $linkTarget );
1050 if ( $title ) {
1051 $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
1052
1053 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
1054 }
1055 $result[] = $entry;
1056 }
1057
1058 return $result;
1059 }
1060
1061 private function formatHeadItems( array $headItems ): array {
1062 $result = [];
1063 foreach ( $headItems as $tag => $content ) {
1064 $entry = [];
1065 $entry['tag'] = $tag;
1066 ApiResult::setContentValue( $entry, 'content', $content );
1067 $result[] = $entry;
1068 }
1069
1070 return $result;
1071 }
1072
1073 private function formatLimitReportData( array $limitReportData ): array {
1074 $result = [];
1075
1076 foreach ( $limitReportData as $name => $value ) {
1077 $entry = [];
1078 $entry['name'] = $name;
1079 if ( !is_array( $value ) ) {
1080 $value = [ $value ];
1081 }
1082 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
1083 $entry = array_merge( $entry, $value );
1084 $result[] = $entry;
1085 }
1086
1087 return $result;
1088 }
1089
1090 private function setIndexedTagNames( array &$array, array $mapping ) {
1091 foreach ( $mapping as $key => $name ) {
1092 if ( isset( $array[$key] ) ) {
1093 ApiResult::setIndexedTagName( $array[$key], $name );
1094 }
1095 }
1096 }
1097
1099 public function getAllowedParams() {
1100 return [
1101 'title' => null,
1102 'text' => [
1103 ParamValidator::PARAM_TYPE => 'text',
1104 ],
1105 'revid' => [
1106 ParamValidator::PARAM_TYPE => 'integer',
1107 ],
1108 'summary' => null,
1109 'page' => null,
1110 'pageid' => [
1111 ParamValidator::PARAM_TYPE => 'integer',
1112 ],
1113 'redirects' => false,
1114 'oldid' => [
1115 ParamValidator::PARAM_TYPE => 'integer',
1116 ],
1117 'prop' => [
1118 ParamValidator::PARAM_DEFAULT => 'text|langlinks|categories|links|templates|' .
1119 'images|externallinks|sections|tocdata|revid|displaytitle|iwlinks|' .
1120 'properties|parsewarnings',
1121 ParamValidator::PARAM_ISMULTI => true,
1122 ParamValidator::PARAM_TYPE => [
1123 'text',
1124 'langlinks',
1125 'categories',
1126 'categorieshtml',
1127 'links',
1128 'templates',
1129 'images',
1130 'externallinks',
1131 'sections',
1132 'tocdata',
1133 'revid',
1134 'displaytitle',
1135 'subtitle',
1136 'headhtml',
1137 'modules',
1138 'jsconfigvars',
1139 'encodedjsconfigvars',
1140 'indicators',
1141 'iwlinks',
1142 'wikitext',
1143 'properties',
1144 'limitreportdata',
1145 'limitreporthtml',
1146 'parsetree',
1147 'parsewarnings',
1148 'parsewarningshtml',
1149 'headitems',
1150 ],
1151 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
1152 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
1153 ],
1154 EnumDef::PARAM_DEPRECATED_VALUES => [
1155 'headitems' => 'apiwarn-deprecation-parse-headitems',
1156 // deprecated since 1.46: T319141
1157 'sections' => [ 'apiwarn-deprecation-withreplacement', 'prop=sections', 'prop=tocdata' ],
1158 ],
1159 ],
1160 'wrapoutputclass' => 'mw-parser-output',
1161 'usearticle' => false, // since 1.43
1162 'parsoid' => [ // since 1.41
1163 ParamValidator::PARAM_TYPE => 'boolean',
1164 ParamValidator::PARAM_DEFAULT => false,
1165 ParamValidator::PARAM_DEPRECATED => true,
1166 ],
1167 'parser' => [ // since 1.45
1168 ParamValidator::PARAM_TYPE => [
1169 'parsoid',
1170 'default',
1171 'legacy'
1172 ],
1173 ParamValidator::PARAM_DEFAULT => 'default',
1174 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1175 ],
1176 'pst' => false,
1177 'onlypst' => false,
1178 'effectivelanglinks' => [
1179 ParamValidator::PARAM_DEFAULT => false,
1180 ParamValidator::PARAM_DEPRECATED => true,
1181 ],
1182 'section' => null,
1183 'sectiontitle' => [
1184 ParamValidator::PARAM_TYPE => 'string',
1185 ],
1186 'disablepp' => [
1187 ParamValidator::PARAM_DEFAULT => false,
1188 ParamValidator::PARAM_DEPRECATED => true,
1189 ],
1190 'disablelimitreport' => false,
1191 'disableeditsection' => false,
1192 'disablestylededuplication' => false,
1193 'showstrategykeys' => false,
1194 'generatexml' => [
1195 ParamValidator::PARAM_DEFAULT => false,
1196 ApiBase::PARAM_HELP_MSG => [
1197 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
1198 ],
1199 ParamValidator::PARAM_DEPRECATED => true,
1200 ],
1201 'preview' => false,
1202 'sectionpreview' => false,
1203 'disabletoc' => false,
1204 'useskin' => [
1205 // T237856; We use all installed skins here to allow hidden (but usable) skins
1206 // to continue working correctly with some features such as Live Preview
1207 ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
1208 ],
1209 'contentformat' => [
1210 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
1211 ],
1212 'contentmodel' => [
1213 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
1214 ],
1215 ];
1216 }
1217
1219 protected function getExamplesMessages() {
1220 return [
1221 'action=parse&page=Project:Sandbox'
1222 => 'apihelp-parse-example-page',
1223 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
1224 => 'apihelp-parse-example-text',
1225 'action=parse&text={{PAGENAME}}&title=Test'
1226 => 'apihelp-parse-example-texttitle',
1227 'action=parse&summary=Some+[[link]]&prop='
1228 => 'apihelp-parse-example-summary',
1229 ];
1230 }
1231
1233 public function getHelpUrls() {
1234 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext';
1235 }
1236}
1237
1239class_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:549
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:60
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...
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
__construct(ApiMain $main, string $action, private readonly RevisionLookup $revisionLookup, private readonly SkinFactory $skinFactory, private readonly LanguageNameUtils $languageNameUtils, private readonly LinkBatchFactory $linkBatchFactory, private readonly LinkCache $linkCache, private readonly IContentHandlerFactory $contentHandlerFactory, private readonly ParserFactory $parserFactory, private readonly WikiPageFactory $wikiPageFactory, private readonly ContentRenderer $contentRenderer, private readonly ContentTransformer $contentTransformer, private readonly CommentFormatter $commentFormatter, private readonly TempUserCreator $tempUserCreator, private readonly UserFactory $userFactory, private readonly UrlUtils $urlUtils, private readonly TitleFormatter $titleFormatter, private readonly JsonCodec $jsonCodec,)
Definition ApiParse.php:74
This is the main service interface for converting single-line comments from various DB comment fields...
Exception representing a failure to serialize or unserialize a content object.
An IContextSource implementation which will inherit context from another source but allow individual ...
The HTML user interface for page editing.
Definition EditPage.php:130
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:66
Factory for LinkBatch objects to batch query page metadata.
Page existence and metadata cache.
Definition LinkCache.php:54
Service for creating WikiPage objects.
Base representation for an editable wiki page.
Definition WikiPage.php:83
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition WikiPage.php:695
getTitle()
Get the title object of the article.
Definition WikiPage.php:251
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:138
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:53
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)