MediaWiki master
ApiParse.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Api;
24
25use Article;
61use Skin;
62use SkinFactory;
65use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
66use WikiPage;
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::newFromLinkTarget( $rev->getPageAsLinkTarget() );
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: OutputPageMakeCategoryLinks
478 // - Hook: OutputPageRenderCategoryLink
479 // - Hook: OutputPageBeforeHTML
480 // HACK Adding the 'mobileformat' parameter *also* enables the skin, for compatibility with legacy
481 // apps. This behavior should be considered deprecated so new users should not rely on this and
482 // always use the "useskin" parameter to enable "skin mode".
483 // Ideally this would be done with another hook so that MobileFrontend could enable skin mode, but
484 // as this is just for a deprecated feature, we are hard-coding this param into core.
485 $context = new DerivativeContext( $this->getContext() );
486 $context->setTitle( $titleObj );
487
488 if ( $pageObj ) {
489 $context->setWikiPage( $pageObj );
490 }
491 // Some hooks only apply to pages when action=view, which this API
492 // call is simulating.
493 $context->setRequest( new FauxRequest( [ 'action' => 'view' ] ) );
494
495 if ( $skin ) {
496 // Use the skin specified by 'useskin'
497 $context->setSkin( $skin );
498 // Context clones the skin, refetch to stay in sync. (T166022)
499 $skin = $context->getSkin();
500 } else {
501 // Make sure the context's skin refers to the context. Without this,
502 // $outputPage->getSkin()->getOutput() !== $outputPage which
503 // confuses some of the output.
504 $context->setSkin( $context->getSkin() );
505 }
506
507 $outputPage = new OutputPage( $context );
508 // Required for subtitle to appear
509 $outputPage->setArticleFlag( true );
510
511 $outputPage->addParserOutputMetadata( $p_result );
512 if ( $this->content ) {
513 $outputPage->addContentOverride( $titleObj, $this->content );
514 }
515 $context->setOutput( $outputPage );
516
517 if ( $skin ) {
518 // Based on OutputPage::output()
519 $outputPage->loadSkinModules( $skin );
520 }
521
522 $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
523 }
524
525 if ( $oldid !== null ) {
526 $result_array['revid'] = (int)$oldid;
527 }
528
529 if ( $params['redirects'] && $redirValues !== null ) {
530 $result_array['redirects'] = $redirValues;
531 }
532
533 if ( isset( $prop['text'] ) ) {
534 $skin = $context ? $context->getSkin() : null;
535 $skinOptions = $skin ? $skin->getOptions() : [
536 'toc' => true,
537 ];
538 // TODO T371004 move runOutputPipeline out of $parserOutput
539 // TODO T371022 it should be reasonably straightforward to move this to a clone, but it requires
540 // careful checking of the clone and of what happens on the boundary of OutputPage. Leaving this as
541 // "getText-equivalent" for now; will fix in a later, independent patch.
542 $oldText = $p_result->getRawText();
543 $result_array['text'] = $p_result->runOutputPipeline( $popts, [
544 'allowClone' => false,
545 'allowTOC' => !$params['disabletoc'],
546 'injectTOC' => $skinOptions['toc'],
547 'enableSectionEditLinks' => !$params['disableeditsection'],
548 'wrapperDivClass' => $params['wrapoutputclass'],
549 'deduplicateStyles' => !$params['disablestylededuplication'],
550 'userLang' => $context ? $context->getLanguage() : null,
551 'skin' => $skin,
552 'includeDebugInfo' => !$params['disablepp'] && !$params['disablelimitreport']
553 ] )->getContentHolderText();
554 $p_result->setRawText( $oldText );
555 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
556 if ( $context ) {
557 $this->getHookRunner()->onOutputPageBeforeHTML( $context->getOutput(), $result_array['text'] );
558 }
559 }
560
561 if ( $params['summary'] !== null ||
562 ( $params['sectiontitle'] !== null && $this->section === 'new' )
563 ) {
564 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
565 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
566 }
567
568 if ( isset( $prop['langlinks'] ) ) {
569 if ( $skin ) {
570 $langlinks = $outputPage->getLanguageLinks();
571 } else {
572 $langlinks = array_map(
573 fn ( $item ) => $item['link'],
574 $p_result->getLinkList( ParserOutputLinkTypes::LANGUAGE )
575 );
576 // The deprecated 'effectivelanglinks' option pre-dates OutputPage
577 // support via 'useskin'. If not already applied, then run just this
578 // one hook of OutputPage::addParserOutputMetadata here.
579 if ( $params['effectivelanglinks'] ) {
580 # for compatibility with old hook, convert to string[]
581 $compat = [];
582 foreach ( $langlinks as $link ) {
583 $s = $link->getInterwiki() . ':' . $link->getText();
584 if ( $link->hasFragment() ) {
585 $s .= '#' . $link->getFragment();
586 }
587 $compat[] = $s;
588 }
589 $langlinks = $compat;
590 $linkFlags = [];
591 $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
592 }
593 }
594
595 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
596 }
597 if ( isset( $prop['categories'] ) ) {
598 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategoryMap() );
599 }
600 if ( isset( $prop['categorieshtml'] ) ) {
601 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
602 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
603 }
604 if ( isset( $prop['links'] ) ) {
605
606 $result_array['links'] = $this->formatLinks( $p_result->getLinkList( ParserOutputLinkTypes::LOCAL ) );
607 }
608 if ( isset( $prop['templates'] ) ) {
609 $result_array['templates'] = $this->formatLinks(
610 $p_result->getLinkList( ParserOutputLinkTypes::TEMPLATE )
611 );
612 }
613 if ( isset( $prop['images'] ) ) {
614 $result_array['images'] = array_map(
615 fn ( $item ) => $item['link']->getDBkey(),
616 $p_result->getLinkList( ParserOutputLinkTypes::MEDIA )
617 );
618 }
619 if ( isset( $prop['externallinks'] ) ) {
620 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
621 }
622 if ( isset( $prop['sections'] ) ) {
623 $result_array['sections'] = $p_result->getSections();
624 $result_array['showtoc'] = $p_result->getOutputFlag( ParserOutputFlags::SHOW_TOC );
625 }
626 if ( isset( $prop['parsewarnings'] ) ) {
627 $result_array['parsewarnings'] = $p_result->getWarnings();
628 }
629 if ( isset( $prop['parsewarningshtml'] ) ) {
630 $warnings = $p_result->getWarnings();
631 $warningsHtml = array_map( static function ( $warning ) {
632 return ( new RawMessage( '$1', [ $warning ] ) )->parse();
633 }, $warnings );
634 $result_array['parsewarningshtml'] = $warningsHtml;
635 }
636
637 if ( isset( $prop['displaytitle'] ) ) {
638 $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
639 ? $p_result->getDisplayTitle()
640 : htmlspecialchars( $titleObj->getPrefixedText(), ENT_NOQUOTES );
641 }
642
643 if ( isset( $prop['subtitle'] ) ) {
644 // Get the subtitle without its container element to support UI refreshing
645 $result_array['subtitle'] = $context->getSkin()->prepareSubtitle( false );
646 }
647
648 if ( isset( $prop['headitems'] ) ) {
649 if ( $skin ) {
650 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
651 } else {
652 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
653 }
654 }
655
656 if ( isset( $prop['headhtml'] ) ) {
657 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
658 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
659 }
660
661 if ( isset( $prop['modules'] ) ) {
662 if ( $skin ) {
663 $result_array['modules'] = $outputPage->getModules();
664 // Deprecated since 1.32 (T188689)
665 $result_array['modulescripts'] = [];
666 $result_array['modulestyles'] = $outputPage->getModuleStyles();
667 } else {
668 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
669 // Deprecated since 1.32 (T188689)
670 $result_array['modulescripts'] = [];
671 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
672 }
673 }
674
675 if ( isset( $prop['jsconfigvars'] ) ) {
676 $showStrategyKeys = (bool)( $params['showstrategykeys'] );
677 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars( $showStrategyKeys );
678 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
679 }
680
681 if ( isset( $prop['encodedjsconfigvars'] ) ) {
682 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
683 $result_array['encodedjsconfigvars'] = FormatJson::encode(
684 $jsconfigvars,
685 false,
686 FormatJson::ALL_OK
687 );
688 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
689 }
690
691 if ( isset( $prop['modules'] ) &&
692 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
693 $this->addWarning( 'apiwarn-moduleswithoutvars' );
694 }
695
696 if ( isset( $prop['indicators'] ) ) {
697 if ( $skin ) {
698 $result_array['indicators'] = $outputPage->getIndicators();
699 } else {
700 $result_array['indicators'] = $p_result->getIndicators();
701 }
702 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
703 }
704
705 if ( isset( $prop['iwlinks'] ) ) {
706 $links = array_map(
707 fn ( $item ) => $item['link'],
708 $p_result->getLinkList( ParserOutputLinkTypes::INTERWIKI )
709 );
710 $result_array['iwlinks'] = $this->formatIWLinks( $links );
711 }
712
713 if ( isset( $prop['wikitext'] ) ) {
714 $result_array['wikitext'] = $this->content->serialize( $format );
715 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
716 // @phan-suppress-next-line PhanImpossibleTypeComparison
717 if ( $this->pstContent !== null ) {
718 $result_array['psttext'] = $this->pstContent->serialize( $format );
719 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
720 }
721 }
722 if ( isset( $prop['properties'] ) ) {
723 $result_array['properties'] = $p_result->getPageProperties();
724 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
725 }
726
727 if ( isset( $prop['limitreportdata'] ) ) {
728 $result_array['limitreportdata'] =
729 $this->formatLimitReportData( $p_result->getLimitReportData() );
730 }
731 if ( isset( $prop['limitreporthtml'] ) ) {
732 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
733 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
734 }
735
736 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
737 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
738 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
739 }
740
741 $parser = $this->parserFactory->getInstance();
742 $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
743 // @phan-suppress-next-line PhanUndeclaredMethod
744 $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
745 $result_array['parsetree'] = $xml;
746 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
747 }
748
749 $result_mapping = [
750 'redirects' => 'r',
751 'langlinks' => 'll',
752 'categories' => 'cl',
753 'links' => 'pl',
754 'templates' => 'tl',
755 'images' => 'img',
756 'externallinks' => 'el',
757 'iwlinks' => 'iw',
758 'sections' => 's',
759 'headitems' => 'hi',
760 'modules' => 'm',
761 'indicators' => 'ind',
762 'modulescripts' => 'm',
763 'modulestyles' => 'm',
764 'properties' => 'pp',
765 'limitreportdata' => 'lr',
766 'parsewarnings' => 'pw',
767 'parsewarningshtml' => 'pw',
768 ];
769 $this->setIndexedTagNames( $result_array, $result_mapping );
770 $result->addValue( null, $this->getModuleName(), $result_array );
771 }
772
781 private function makeParserOptions( WikiPage $pageObj, array $params ) {
782 $popts = $pageObj->makeParserOptions( $this->getContext() );
783 $popts->setRenderReason( 'api-parse' );
784 if ( $params['usearticle'] ) {
785 # T349037: The ArticleParserOptions hook should be broadened to take
786 # a WikiPage (aka $pageObj) instead of an Article. But for now
787 # fake the Article.
788 $article = Article::newFromWikiPage( $pageObj, $this->getContext() );
789 # Allow extensions to vary parser options used for article rendering,
790 # in the same way Article does
791 $this->getHookRunner()->onArticleParserOptions( $article, $popts );
792 }
793 return $this->tweakParserOptions( $popts, $pageObj->getTitle(), $params );
794 }
795
805 private function tweakParserOptions( ParserOptions $popts, Title $title, array $params ) {
806 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
807 $popts->setIsSectionPreview( $params['sectionpreview'] );
808
809 if ( $params['wrapoutputclass'] !== '' ) {
810 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
811 }
812 if ( $params['parsoid'] ) {
813 $popts->setUseParsoid();
814 }
815
816 $reset = null;
817 $suppressCache = false;
818 $this->getHookRunner()->onApiMakeParserOptions( $popts, $title,
819 $params, $this, $reset, $suppressCache );
820
821 return [ $popts, $reset, $suppressCache ];
822 }
823
833 private function getParsedContent(
834 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
835 ) {
836 $revId = $rev ? $rev->getId() : null;
837 $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
838
839 if ( $getContent || $this->section !== false || $isDeleted ) {
840 if ( $rev ) {
841 $this->content = $rev->getContent(
842 SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getAuthority()
843 );
844 if ( !$this->content ) {
845 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
846 }
847 } else {
848 $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
849 if ( !$this->content ) {
850 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
851 }
852 }
853 $this->contentIsDeleted = $isDeleted;
854 $this->contentIsSuppressed = $rev &&
855 $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
856 }
857
858 if ( $this->section !== false ) {
859 $this->content = $this->getSectionContent(
860 $this->content,
861 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
862 );
863 return $this->getContentParserOutput(
864 $this->content, $page->getTitle(),
865 $rev,
866 $popts
867 );
868 }
869
870 if ( $isDeleted ) {
871 // getParserOutput can't do revdeled revisions
872
873 $pout = $this->getContentParserOutput(
874 $this->content, $page->getTitle(),
875 $rev,
876 $popts
877 );
878 } else {
879 // getParserOutput will save to Parser cache if able
880 $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
881 }
882 if ( !$pout ) {
883 // @codeCoverageIgnoreStart
884 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
885 // @codeCoverageIgnoreEnd
886 }
887
888 return $pout;
889 }
890
898 private function getSectionContent( Content $content, $what ) {
899 // Not cached (save or load)
900 $section = $content->getSection( $this->section );
901 if ( $section === false ) {
902 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
903 }
904 if ( $section === null ) {
905 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
906 }
907
908 // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
909 return $section;
910 }
911
919 private function formatSummary( $title, $params ) {
920 $summary = $params['summary'] ?? '';
921 $sectionTitle = $params['sectiontitle'] ?? '';
922
923 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
924 if ( $sectionTitle !== '' ) {
925 $summary = $params['sectiontitle'];
926 }
927 if ( $summary !== '' ) {
928 $summary = $this->msg( 'newsectionsummary' )
929 ->rawParams( $this->parserFactory->getMainInstance()->stripSectionName( $summary ) )
930 ->inContentLanguage()->text();
931 }
932 }
933 return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
934 }
935
940 private function formatLangLinks( $links ): array {
941 $result = [];
942 foreach ( $links as $link ) {
943 $entry = [];
944 if ( is_string( $link ) ) {
945 [ $lang, $titleWithFrag ] = explode( ':', $link, 2 );
946 [ $title, $frag ] = array_pad( explode( '#', $titleWithFrag, 2 ), 2, '' );
947 $title = TitleValue::tryNew( NS_MAIN, $title, $frag, $lang );
948 } else {
949 $title = $link;
950 $lang = $link->getInterwiki();
951 $titleWithFrag = $link->getText();
952 if ( $link->hasFragment() ) {
953 $titleWithFrag .= '#' . $link->getFragment();
954 }
955 }
956 $title = Title::castFromLinkTarget( $title );
957
958 $entry['lang'] = $lang;
959 if ( $title ) {
960 $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
961 // title validity implies language code validity
962 // localised language name in 'uselang' language
963 $entry['langname'] = $this->languageNameUtils->getLanguageName(
964 $lang,
965 $this->getLanguage()->getCode()
966 );
967
968 // native language name
969 $entry['autonym'] = $this->languageNameUtils->getLanguageName( $lang );
970 }
971 ApiResult::setContentValue( $entry, 'title', $titleWithFrag );
972 $result[] = $entry;
973 }
974
975 return $result;
976 }
977
978 private function formatCategoryLinks( $links ) {
979 $result = [];
980
981 if ( !$links ) {
982 return $result;
983 }
984
985 // Fetch hiddencat property
986 $lb = $this->linkBatchFactory->newLinkBatch();
987 $lb->setArray( [ NS_CATEGORY => $links ] );
988 $db = $this->getDB();
989 $res = $db->newSelectQueryBuilder()
990 ->select( [ 'page_title', 'pp_propname' ] )
991 ->from( 'page' )
992 ->where( $lb->constructSet( 'page', $db ) )
993 ->leftJoin( 'page_props', null, [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] )
994 ->caller( __METHOD__ )
995 ->fetchResultSet();
996 $hiddencats = [];
997 foreach ( $res as $row ) {
998 $hiddencats[$row->page_title] = isset( $row->pp_propname );
999 }
1000
1001 foreach ( $links as $link => $sortkey ) {
1002 $entry = [];
1003 $entry['sortkey'] = $sortkey;
1004 // array keys will cast numeric category names to ints, so cast back to string
1005 ApiResult::setContentValue( $entry, 'category', (string)$link );
1006 if ( !isset( $hiddencats[$link] ) ) {
1007 $entry['missing'] = true;
1008
1009 // We already know the link doesn't exist in the database, so
1010 // tell LinkCache that before calling $title->isKnown().
1011 $title = Title::makeTitle( NS_CATEGORY, $link );
1012 $this->linkCache->addBadLinkObj( $title );
1013 if ( $title->isKnown() ) {
1014 $entry['known'] = true;
1015 }
1016 } elseif ( $hiddencats[$link] ) {
1017 $entry['hidden'] = true;
1018 }
1019 $result[] = $entry;
1020 }
1021
1022 return $result;
1023 }
1024
1029 private function formatLinks( array $links ): array {
1030 $result = [];
1031 foreach ( $links as [ 'link' => $link, 'pageid' => $id ] ) {
1032 $entry = [];
1033 $entry['ns'] = $link->getNamespace();
1034 ApiResult::setContentValue( $entry, 'title', Title::newFromLinkTarget( $link )->getFullText() );
1035 $entry['exists'] = $id != 0;
1036 $result[] = $entry;
1037 }
1038
1039 return $result;
1040 }
1041
1042 private function formatIWLinks( $iw ) {
1043 $result = [];
1044 foreach ( $iw as $linkTarget ) {
1045 $entry = [];
1046 $entry['prefix'] = $linkTarget->getInterwiki();
1047 $title = Title::newFromLinkTarget( $linkTarget );
1048 if ( $title ) {
1049 $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
1050
1051 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
1052 }
1053 $result[] = $entry;
1054 }
1055
1056 return $result;
1057 }
1058
1059 private function formatHeadItems( $headItems ) {
1060 $result = [];
1061 foreach ( $headItems as $tag => $content ) {
1062 $entry = [];
1063 $entry['tag'] = $tag;
1064 ApiResult::setContentValue( $entry, 'content', $content );
1065 $result[] = $entry;
1066 }
1067
1068 return $result;
1069 }
1070
1071 private function formatLimitReportData( $limitReportData ) {
1072 $result = [];
1073
1074 foreach ( $limitReportData as $name => $value ) {
1075 $entry = [];
1076 $entry['name'] = $name;
1077 if ( !is_array( $value ) ) {
1078 $value = [ $value ];
1079 }
1080 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
1081 $entry = array_merge( $entry, $value );
1082 $result[] = $entry;
1083 }
1084
1085 return $result;
1086 }
1087
1088 private function setIndexedTagNames( &$array, $mapping ) {
1089 foreach ( $mapping as $key => $name ) {
1090 if ( isset( $array[$key] ) ) {
1091 ApiResult::setIndexedTagName( $array[$key], $name );
1092 }
1093 }
1094 }
1095
1096 public function getAllowedParams() {
1097 return [
1098 'title' => null,
1099 'text' => [
1100 ParamValidator::PARAM_TYPE => 'text',
1101 ],
1102 'revid' => [
1103 ParamValidator::PARAM_TYPE => 'integer',
1104 ],
1105 'summary' => null,
1106 'page' => null,
1107 'pageid' => [
1108 ParamValidator::PARAM_TYPE => 'integer',
1109 ],
1110 'redirects' => false,
1111 'oldid' => [
1112 ParamValidator::PARAM_TYPE => 'integer',
1113 ],
1114 'prop' => [
1115 ParamValidator::PARAM_DEFAULT => 'text|langlinks|categories|links|templates|' .
1116 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
1117 'properties|parsewarnings',
1118 ParamValidator::PARAM_ISMULTI => true,
1119 ParamValidator::PARAM_TYPE => [
1120 'text',
1121 'langlinks',
1122 'categories',
1123 'categorieshtml',
1124 'links',
1125 'templates',
1126 'images',
1127 'externallinks',
1128 'sections',
1129 'revid',
1130 'displaytitle',
1131 'subtitle',
1132 'headhtml',
1133 'modules',
1134 'jsconfigvars',
1135 'encodedjsconfigvars',
1136 'indicators',
1137 'iwlinks',
1138 'wikitext',
1139 'properties',
1140 'limitreportdata',
1141 'limitreporthtml',
1142 'parsetree',
1143 'parsewarnings',
1144 'parsewarningshtml',
1145 'headitems',
1146 ],
1147 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
1148 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
1149 ],
1150 EnumDef::PARAM_DEPRECATED_VALUES => [
1151 'headitems' => 'apiwarn-deprecation-parse-headitems',
1152 ],
1153 ],
1154 'wrapoutputclass' => 'mw-parser-output',
1155 'usearticle' => false, // since 1.43
1156 'parsoid' => false, // since 1.41
1157 'pst' => false,
1158 'onlypst' => false,
1159 'effectivelanglinks' => [
1160 ParamValidator::PARAM_DEFAULT => false,
1161 ParamValidator::PARAM_DEPRECATED => true,
1162 ],
1163 'section' => null,
1164 'sectiontitle' => [
1165 ParamValidator::PARAM_TYPE => 'string',
1166 ],
1167 'disablepp' => [
1168 ParamValidator::PARAM_DEFAULT => false,
1169 ParamValidator::PARAM_DEPRECATED => true,
1170 ],
1171 'disablelimitreport' => false,
1172 'disableeditsection' => false,
1173 'disablestylededuplication' => false,
1174 'showstrategykeys' => false,
1175 'generatexml' => [
1176 ParamValidator::PARAM_DEFAULT => false,
1177 ApiBase::PARAM_HELP_MSG => [
1178 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
1179 ],
1180 ParamValidator::PARAM_DEPRECATED => true,
1181 ],
1182 'preview' => false,
1183 'sectionpreview' => false,
1184 'disabletoc' => false,
1185 'useskin' => [
1186 // T237856; We use all installed skins here to allow hidden (but usable) skins
1187 // to continue working correctly with some features such as Live Preview
1188 ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
1189 ],
1190 'contentformat' => [
1191 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
1192 ],
1193 'contentmodel' => [
1194 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
1195 ],
1196 ];
1197 }
1198
1199 protected function getExamplesMessages() {
1200 return [
1201 'action=parse&page=Project:Sandbox'
1202 => 'apihelp-parse-example-page',
1203 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
1204 => 'apihelp-parse-example-text',
1205 'action=parse&text={{PAGENAME}}&title=Test'
1206 => 'apihelp-parse-example-texttitle',
1207 'action=parse&summary=Some+[[link]]&prop='
1208 => 'apihelp-parse-example-summary',
1209 ];
1210 }
1211
1212 public function getHelpUrls() {
1213 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext';
1214 }
1215}
1216
1218class_alias( ApiParse::class, 'ApiParse' );
const PROTO_CURRENT
Definition Defines.php:209
const NS_MAIN
Definition Defines.php:65
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:222
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:541
array $params
The job parameters.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:72
Exception representing a failure to serialize or unserialize a content object.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:76
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:148
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.
Service for creating WikiPage objects.
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:145
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.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:78
Service for temporary user creation.
Creates 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
Factory class to create Skin objects.
The base class for all skins.
Definition Skin.php:65
static normalizeKey(string $key)
Normalize a skin preference value to a form that can be loaded.
Definition Skin.php:229
Base representation for an editable wiki page.
Definition WikiPage.php:85
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
getParserOutput(?ParserOptions $parserOptions=null, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition WikiPage.php:689
getId( $wikiId=self::LOCAL)
Definition WikiPage.php:535
getTitle()
Get the title object of the article.
Definition WikiPage.php:259
getContent( $audience=RevisionRecord::FOR_PUBLIC, ?Authority $performer=null)
Get the content of the current revision.
Definition WikiPage.php:771
Service for formatting and validating API parameters.
Type definition for enumeration types.
Definition EnumDef.php:32
Base interface for representing page content.
Definition Content.php:39
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Service for looking up page revisions.
A title formatter service for MediaWiki.