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