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