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