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