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