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