MediaWiki master
ApiParse.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
54use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
55use Wikimedia\Parsoid\Core\TOCData;
56
60class ApiParse extends ApiBase {
61
63 private $section = null;
64
66 private $content = null;
67
69 private $pstContent = null;
70
71 private bool $contentIsDeleted = false;
72 private bool $contentIsSuppressed = false;
73
74 public function __construct(
75 ApiMain $main,
76 string $action,
77 private readonly RevisionLookup $revisionLookup,
78 private readonly SkinFactory $skinFactory,
79 private readonly LanguageNameUtils $languageNameUtils,
80 private readonly LinkBatchFactory $linkBatchFactory,
81 private readonly LinkCache $linkCache,
82 private readonly IContentHandlerFactory $contentHandlerFactory,
83 private readonly ParserFactory $parserFactory,
84 private readonly WikiPageFactory $wikiPageFactory,
85 private readonly ContentRenderer $contentRenderer,
86 private readonly ContentTransformer $contentTransformer,
87 private readonly CommentFormatter $commentFormatter,
88 private readonly TempUserCreator $tempUserCreator,
89 private readonly UserFactory $userFactory,
90 private readonly UrlUtils $urlUtils,
91 private readonly TitleFormatter $titleFormatter,
92 private readonly JsonCodec $jsonCodec,
93 ) {
94 parent::__construct( $main, $action );
95 }
96
97 private function getPoolKey(): string {
98 $poolKey = WikiMap::getCurrentWikiDbDomain() . ':ApiParse:';
99 if ( !$this->getUser()->isRegistered() ) {
100 $poolKey .= 'a:' . $this->getUser()->getName();
101 } else {
102 $poolKey .= 'u:' . $this->getUser()->getId();
103 }
104 return $poolKey;
105 }
106
107 private function getContentParserOutput(
108 Content $content,
109 PageReference $page,
110 ?RevisionRecord $revision,
111 ParserOptions $popts
112 ): ParserOutput {
113 $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(),
114 [
115 'doWork' => function () use ( $content, $page, $revision, $popts ) {
116 return $this->contentRenderer->getParserOutput(
117 $content, $page, $revision, $popts
118 );
119 },
120 'error' => function (): never {
121 $this->dieWithError( 'apierror-concurrency-limit' );
122 },
123 ]
124 );
125 return $worker->execute();
126 }
127
128 private function getUserForPreview(): UserIdentity {
129 $user = $this->getUser();
130 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
131 return $this->userFactory->newUnsavedTempUser(
132 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
133 );
134 }
135 return $user;
136 }
137
139 private function getPageParserOutput(
140 WikiPage $page,
141 ?int $revId,
142 ParserOptions $popts,
143 bool $suppressCache
144 ) {
145 $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(),
146 [
147 'doWork' => static function () use ( $page, $revId, $popts, $suppressCache ) {
148 return $page->getParserOutput( $popts, $revId, $suppressCache );
149 },
150 'error' => function (): never {
151 $this->dieWithError( 'apierror-concurrency-limit' );
152 },
153 ]
154 );
155 return $worker->execute();
156 }
157
158 public function execute() {
159 // The data is hot but user-dependent, like page views, so we set vary cookies
160 $this->getMain()->setCacheMode( 'anon-public-user-private' );
161
162 // Get parameters
163 $params = $this->extractRequestParams();
164
165 // No easy way to say that text and title or revid are allowed together
166 // while the rest aren't, so just do it in three calls.
167 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' );
168 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' );
169 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'revid' );
170
171 $text = $params['text'];
172 $title = $params['title'];
173 if ( $title === null ) {
174 $titleProvided = false;
175 // A title is needed for parsing, so arbitrarily choose one
176 $title = 'API';
177 } else {
178 $titleProvided = true;
179 }
180
181 $page = $params['page'];
182 $pageid = $params['pageid'];
183 $oldid = $params['oldid'];
184
185 $prop = array_fill_keys( $params['prop'], true );
186
187 if ( isset( $params['section'] ) ) {
188 $this->section = $params['section'];
189 if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
190 $this->dieWithError( 'apierror-invalidsection' );
191 }
192 } else {
193 $this->section = false;
194 }
195
196 // The parser needs $wgTitle to be set, apparently the
197 // $title parameter in Parser::parse isn't enough *sigh*
198 // TODO: Does this still need $wgTitle?
199 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
200 global $wgTitle;
201
202 $format = null;
203 $redirValues = null;
204
205 $needContent = isset( $prop['wikitext'] ) ||
206 isset( $prop['parsetree'] ) || $params['generatexml'];
207
208 // Return result
209 $result = $this->getResult();
210
211 if ( $oldid !== null || $pageid !== null || $page !== null ) {
212 if ( $this->section === 'new' ) {
213 $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' );
214 }
215 if ( $oldid !== null ) {
216 // Don't use the parser cache
217 $rev = $this->revisionLookup->getRevisionById( $oldid );
218 if ( !$rev ) {
219 $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] );
220 }
221
222 $this->checkTitleUserPermissions( $rev->getPage(), 'read' );
223
224 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
225 $this->dieWithError(
226 [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ]
227 );
228 }
229
230 $revLinkTarget = $rev->getPageAsLinkTarget();
231 $titleObj = Title::newFromLinkTarget( $revLinkTarget );
232 $wgTitle = $titleObj;
233 $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
234 [ $popts, $reset, $suppressCache ] = $this->makeParserOptions( $pageObj, $params );
235 $p_result = $this->getParsedContent(
236 $pageObj, $popts, $suppressCache, $pageid, $rev, $needContent
237 );
238 } else { // Not $oldid, but $pageid or $page
239 if ( $params['redirects'] ) {
240 $reqParams = [
241 'redirects' => '',
242 ];
243 $pageParams = [];
244 if ( $pageid !== null ) {
245 $reqParams['pageids'] = $pageid;
246 $pageParams['pageid'] = $pageid;
247 } else { // $page
248 $reqParams['titles'] = $page;
249 $pageParams['title'] = $page;
250 }
251 $req = new FauxRequest( $reqParams );
252 $main = new ApiMain( $req );
253 $pageSet = new ApiPageSet( $main );
254 $pageSet->execute();
255 $redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() );
256
257 foreach ( $pageSet->getRedirectTargets() as $redirectTarget ) {
258 $pageParams = [ 'title' => $this->titleFormatter->getFullText( $redirectTarget ) ];
259 }
260 } elseif ( $pageid !== null ) {
261 $pageParams = [ 'pageid' => $pageid ];
262 } else { // $page
263 $pageParams = [ 'title' => $page ];
264 }
265
266 $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
267 $titleObj = $pageObj->getTitle();
268 if ( !$titleObj->exists() ) {
269 $this->dieWithError( 'apierror-missingtitle' );
270 }
271
272 $this->checkTitleUserPermissions( $titleObj, 'read' );
273 $wgTitle = $titleObj;
274
275 if ( isset( $prop['revid'] ) ) {
276 $oldid = $pageObj->getLatest();
277 }
278
279 [ $popts, $reset, $suppressCache ] = $this->makeParserOptions( $pageObj, $params );
280 $p_result = $this->getParsedContent(
281 $pageObj, $popts, $suppressCache, $pageid, null, $needContent
282 );
283 }
284 } else { // Not $oldid, $pageid, $page. Hence based on $text
285 $model = $params['contentmodel'];
286 $format = $params['contentformat'];
287
288 $titleObj = Title::newFromText( $title );
289 if ( !$titleObj || $titleObj->isExternal() ) {
290 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
291 }
292 $revid = $params['revid'];
293 $rev = null;
294 if ( $revid !== null ) {
295 $rev = $this->revisionLookup->getRevisionById( $revid );
296 if ( !$rev ) {
297 $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] );
298 }
299 $pTitleObj = $titleObj;
300 $titleObj = Title::newFromPageIdentity( $rev->getPage() );
301 if ( $titleProvided ) {
302 if ( !$titleObj->equals( $pTitleObj ) ) {
303 $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(),
304 wfEscapeWikiText( $pTitleObj->getPrefixedText() ) ] );
305 }
306 } else {
307 // Consider the title derived from the revid as having
308 // been provided.
309 $titleProvided = true;
310 }
311 }
312 $wgTitle = $titleObj;
313 if ( $titleObj->canExist() ) {
314 $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
315 [ $popts, $reset ] = $this->makeParserOptions( $pageObj, $params );
316 } else {
317 // Allow parsing wikitext in the context of special pages (T51477)
318 $pageObj = null;
319 $popts = ParserOptions::newFromContext( $this->getContext() );
320 [ $popts, $reset ] = $this->tweakParserOptions( $popts, $titleObj, $params );
321 }
322
323 $textProvided = $text !== null;
324
325 if ( !$textProvided ) {
326 if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
327 if ( $revid !== null ) {
328 $this->addWarning( 'apiwarn-parse-revidwithouttext' );
329 } else {
330 $this->addWarning( 'apiwarn-parse-titlewithouttext' );
331 }
332 }
333 // Prevent warning from ContentHandler::makeContent()
334 $text = '';
335 }
336
337 // If we are parsing text, do not use the content model of the default
338 // API title, but default to wikitext to keep BC.
339 if ( $textProvided && !$titleProvided && $model === null ) {
340 $model = CONTENT_MODEL_WIKITEXT;
341 $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] );
342 } elseif ( $model === null ) {
343 $model = $titleObj->getContentModel();
344 }
345
346 $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
347 // Not in the default format, check supported or not
348 if ( $format && !$contentHandler->isSupportedFormat( $format ) ) {
349 $this->dieWithError( [ 'apierror-badformat-generic', $format, $model ] );
350 }
351
352 try {
353 $this->content = $contentHandler->unserializeContent( $text, $format );
354 } catch ( ContentSerializationException $ex ) {
355 $this->dieWithException( $ex, [
356 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
357 ] );
358 }
359
360 if ( $this->section !== false ) {
361 if ( $this->section === 'new' ) {
362 // Insert the section title above the content.
363 if ( $params['sectiontitle'] !== null ) {
364 $this->content = $this->content->addSectionHeader( $params['sectiontitle'] );
365 }
366 } else {
367 $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
368 }
369 }
370
371 if ( $params['pst'] || $params['onlypst'] ) {
372 $this->pstContent = $this->contentTransformer->preSaveTransform(
373 $this->content,
374 $titleObj,
375 $this->getUserForPreview(),
376 $popts
377 );
378 }
379 if ( $params['onlypst'] ) {
380 // Build a result and bail out
381 $result_array = [];
382 $result_array['text'] = $this->pstContent->serialize( $format );
383 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
384 if ( isset( $prop['wikitext'] ) ) {
385 $result_array['wikitext'] = $this->content->serialize( $format );
386 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
387 }
388 if ( $params['summary'] !== null ||
389 ( $params['sectiontitle'] !== null && $this->section === 'new' )
390 ) {
391 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
392 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
393 }
394
395 $result->addValue( null, $this->getModuleName(), $result_array );
396
397 return;
398 }
399
400 // Not cached (save or load)
401 if ( $params['pst'] ) {
402 $p_result = $this->getContentParserOutput( $this->pstContent, $titleObj, $rev, $popts );
403 } else {
404 $p_result = $this->getContentParserOutput( $this->content, $titleObj, $rev, $popts );
405 }
406 }
407
408 $result_array = [];
409
410 $result_array['title'] = $titleObj->getPrefixedText();
411 $result_array['pageid'] = $pageid ?: $titleObj->getArticleID();
412 if ( $this->contentIsDeleted ) {
413 $result_array['textdeleted'] = true;
414 }
415 if ( $this->contentIsSuppressed ) {
416 $result_array['textsuppressed'] = true;
417 }
418
419 if ( isset( $params['useskin'] ) ) {
420 $skin = $this->skinFactory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
421 } else {
422 $skin = null;
423 }
424
425 $outputPage = null;
426 $context = null;
427 if (
428 $skin || isset( $prop['subtitle'] ) || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ||
429 isset( $params['mobileformat'] )
430 ) {
431 // Enabling the skin via 'useskin', 'subtitle', 'headhtml', or 'categorieshtml'
432 // gets OutputPage and Skin involved, which (among others) applies
433 // these hooks:
434 // - Hook: LanguageLinks
435 // - Hook: SkinSubPageSubtitle
436 // - Hook: OutputPageParserOutput
437 // - Hook: OutputPageRenderCategoryLink
438 // - Hook: OutputPageBeforeHTML
439 // HACK Adding the 'mobileformat' parameter *also* enables the skin, for compatibility with legacy
440 // apps. This behavior should be considered deprecated so new users should not rely on this and
441 // always use the "useskin" parameter to enable "skin mode".
442 // Ideally this would be done with another hook so that MobileFrontend could enable skin mode, but
443 // as this is just for a deprecated feature, we are hard-coding this param into core.
444 $context = new DerivativeContext( $this->getContext() );
445 $context->setTitle( $titleObj );
446
447 if ( $pageObj ) {
448 $context->setWikiPage( $pageObj );
449 }
450 // Some hooks only apply to pages when action=view, which this API
451 // call is simulating.
452 $context->setRequest( new FauxRequest( [ 'action' => 'view' ] ) );
453
454 if ( $skin ) {
455 // Use the skin specified by 'useskin'
456 $context->setSkin( $skin );
457 // Context clones the skin, refetch to stay in sync. (T166022)
458 $skin = $context->getSkin();
459 } else {
460 // Make sure the context's skin refers to the context. Without this,
461 // $outputPage->getSkin()->getOutput() !== $outputPage which
462 // confuses some of the output.
463 $context->setSkin( $context->getSkin() );
464 }
465
466 $outputPage = new OutputPage( $context );
467 // Required for subtitle to appear
468 $outputPage->setArticleFlag( true );
469
470 // Some hooks are conditional on categories
471 $outputPage->addCategoryLinks( $p_result->getCategoryMap() );
472
473 if ( $this->content ) {
474 $outputPage->addContentOverride( $titleObj, $this->content );
475 }
476 $context->setOutput( $outputPage );
477
478 if ( $skin ) {
479 // Based on OutputPage::output()
480 $outputPage->loadSkinModules( $skin );
481 }
482 }
483
484 if ( $oldid !== null ) {
485 $result_array['revid'] = (int)$oldid;
486 }
487
488 if ( $params['redirects'] && $redirValues !== null ) {
489 $result_array['redirects'] = $redirValues;
490 }
491
492 // Parsoid displaytitle is set during the postprocessing pipeline
493 if ( isset( $prop['text'] ) || isset( $prop['displaytitle'] ) ) {
494 $skin = $context ? $context->getSkin() : null;
495 $skinOptions = $skin ? $skin->getOptions() : [
496 'toc' => true,
497 ];
498 // TODO T371004 move runOutputPipeline out of $parserOutput
499 // TODO T371022 it should be reasonably straightforward to move this to a clone, but it requires
500 // careful checking of the clone and of what happens on the boundary of OutputPage. Leaving this as
501 // "getText-equivalent" for now; will fix in a later, independent patch.
502 $oldText = $p_result->getContentHolderText();
503 $newText = $p_result->runOutputPipeline( $popts, [
504 // This will have side effects on $p_result (T371022)
505 'allowClone' => false,
506 'allowTOC' => !$params['disabletoc'],
507 'injectTOC' => $skinOptions['toc'],
508 'wrapperDivClass' => $params['wrapoutputclass'],
509 'deduplicateStyles' => !$params['disablestylededuplication'],
510 'userLang' => $context ? $context->getLanguage() : null,
511 'skin' => $skin,
512 'includeDebugInfo' => !$params['disablepp'] && !$params['disablelimitreport']
513 ] )->getContentHolderText();
514 $p_result->setContentHolderText( $oldText );
515 if ( isset( $prop['text'] ) ) {
516 $result_array['text'] = $newText;
517 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
518 if ( $context ) {
519 $this->getHookRunner()->onOutputPageBeforeHTML( $context->getOutput(), $result_array['text'] );
520 }
521 }
522 }
523
524 if ( $outputPage ) {
525 // This needs to happen after running the OutputTransform pipeline so that the metadata inserted by
526 // the pipeline is also added to the OutputPage
527 $outputPage->addParserOutputMetadata( $p_result );
528
529 $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
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 ( $outputPage ) {
541 $langlinks = $outputPage->getLanguageLinks();
542 } else {
543 $langlinks = array_map(
544 static fn ( $item ) => $item['link'],
545 $p_result->getLinkList( ParserOutputLinkTypes::LANGUAGE )
546 );
547 // The deprecated 'effectivelanglinks' option pre-dates OutputPage
548 // support via 'useskin'. If not already applied, then run just this
549 // one hook of OutputPage::addParserOutputMetadata here.
550 if ( $params['effectivelanglinks'] ) {
551 # for compatibility with old hook, convert to string[]
552 $compat = [];
553 foreach ( $langlinks as $link ) {
554 $s = $link->getInterwiki() . ':' . $link->getText();
555 if ( $link->hasFragment() ) {
556 $s .= '#' . $link->getFragment();
557 }
558 $compat[] = $s;
559 }
560 $langlinks = $compat;
561 $linkFlags = [];
562 sort( $langlinks );
563 $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
564 }
565 }
566
567 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
568 }
569 if ( isset( $prop['categories'] ) ) {
570 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategoryMap() );
571 }
572 if ( isset( $prop['categorieshtml'] ) ) {
573 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
574 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
575 }
576 if ( isset( $prop['links'] ) ) {
577
578 $result_array['links'] = $this->formatLinks( $p_result->getLinkList( ParserOutputLinkTypes::LOCAL ) );
579 }
580 if ( isset( $prop['templates'] ) ) {
581 $result_array['templates'] = $this->formatLinks(
582 $p_result->getLinkList( ParserOutputLinkTypes::TEMPLATE )
583 );
584 }
585 if ( isset( $prop['images'] ) ) {
586 $result_array['images'] = array_map(
587 static fn ( $item ) => $item['link']->getDBkey(),
588 $p_result->getLinkList( ParserOutputLinkTypes::MEDIA )
589 );
590 }
591 if ( isset( $prop['externallinks'] ) ) {
592 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
593 }
594 if ( isset( $prop['sections'] ) ) {
595 $result_array['sections'] = $p_result->getSections();
596 }
597 if ( isset( $prop['tocdata'] ) ) {
598 $result_array['tocdata'] = $this->jsonCodec->toJsonArray(
599 $p_result->getTOCData(), TOCData::class
600 );
601 }
602 if ( isset( $prop['parseroutput'] ) ) {
603 $result_array['parseroutput'] = $this->jsonCodec->toJsonArray(
604 $p_result, ParserOutput::class
605 );
606 }
607 if ( isset( $prop['sections'] ) || isset( $prop['tocdata'] ) ) {
608 $result_array['showtoc'] = $p_result->getOutputFlag( ParserOutputFlags::SHOW_TOC );
609 }
610 if ( isset( $prop['parsewarnings'] ) || isset( $prop['parsewarningshtml'] ) ) {
611 $warningMsgs = array_map(
612 static fn ( $mv ) => Message::newFromSpecifier( $mv )
613 ->page( $titleObj )
614 // Note that we use ContentLanguage here
615 ->inContentLanguage(),
616 $p_result->getWarningMsgs()
617 );
618 if ( $warningMsgs === [] ) {
619 // Backward compatibility with cached ParserOutput from
620 // MW <= 1.45 which didn't store the MessageValues (T343048)
621 $warningMsgs = array_map(
622 static fn ( $warning ) => new RawMessage( '$1', [ $warning ] ),
623 $p_result->getWarnings()
624 );
625 }
626 if ( isset( $prop['parsewarnings'] ) ) {
627 $warnings = array_map( static fn ( $msg ) => $msg->text(), $warningMsgs );
628 $result_array['parsewarnings'] = $warnings;
629 }
630 if ( isset( $prop['parsewarningshtml'] ) ) {
631 $warningsHtml = array_map( static fn ( $msg ) => $msg->parse(), $warningMsgs );
632 $result_array['parsewarningshtml'] = $warningsHtml;
633 }
634 }
635
636 if ( isset( $prop['displaytitle'] ) ) {
637 $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
638 ? $p_result->getDisplayTitle()
639 : htmlspecialchars( $titleObj->getPrefixedText(), ENT_NOQUOTES );
640 }
641
642 if ( isset( $prop['subtitle'] ) ) {
643 // Get the subtitle without its container element to support UI refreshing
644 $result_array['subtitle'] = $context->getSkin()->prepareSubtitle( false );
645 }
646
647 if ( isset( $prop['headitems'] ) ) {
648 if ( $outputPage ) {
649 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
650 } else {
651 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
652 }
653 }
654
655 if ( isset( $prop['headhtml'] ) ) {
656 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
657 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
658 }
659
660 if ( isset( $prop['modules'] ) ) {
661 if ( $outputPage ) {
662 $result_array['modules'] = $outputPage->getModules();
663 // Deprecated since 1.32 (T188689)
664 $result_array['modulescripts'] = [];
665 $result_array['modulestyles'] = $outputPage->getModuleStyles();
666 } else {
667 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
668 // Deprecated since 1.32 (T188689)
669 $result_array['modulescripts'] = [];
670 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
671 }
672 }
673
674 if ( isset( $prop['jsconfigvars'] ) ) {
675 $showStrategyKeys = (bool)( $params['showstrategykeys'] );
676 $jsconfigvars = $outputPage ? $outputPage->getJsConfigVars() :
677 $p_result->getJsConfigVars( $showStrategyKeys );
678 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
679 }
680
681 if ( isset( $prop['encodedjsconfigvars'] ) ) {
682 $jsconfigvars = $outputPage ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
683 $result_array['encodedjsconfigvars'] = FormatJson::encode(
684 $jsconfigvars,
685 false,
686 FormatJson::ALL_OK
687 );
688 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
689 }
690
691 if ( isset( $prop['modules'] ) &&
692 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
693 $this->addWarning( 'apiwarn-moduleswithoutvars' );
694 }
695
696 if ( isset( $prop['indicators'] ) ) {
697 if ( $skin ) {
698 $result_array['indicators'] = $outputPage->getIndicators();
699 } else {
700 $result_array['indicators'] = $p_result->getIndicators();
701 }
702 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
703 }
704
705 if ( isset( $prop['iwlinks'] ) ) {
706 $links = array_map(
707 static fn ( $item ) => $item['link'],
708 $p_result->getLinkList( ParserOutputLinkTypes::INTERWIKI )
709 );
710 $result_array['iwlinks'] = $this->formatIWLinks( $links );
711 }
712
713 if ( isset( $prop['wikitext'] ) ) {
714 $result_array['wikitext'] = $this->content->serialize( $format );
715 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
716 // @phan-suppress-next-line PhanImpossibleTypeComparison
717 if ( $this->pstContent !== null ) {
718 $result_array['psttext'] = $this->pstContent->serialize( $format );
719 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
720 }
721 }
722 if ( isset( $prop['properties'] ) ) {
723 $result_array['properties'] = $p_result->getPageProperties();
724 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
725 }
726
727 if ( isset( $prop['limitreportdata'] ) ) {
728 $result_array['limitreportdata'] =
729 $this->formatLimitReportData( $p_result->getLimitReportData() );
730 }
731 if ( isset( $prop['limitreporthtml'] ) ) {
732 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
733 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
734 }
735
736 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
737 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
738 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
739 }
740
741 $parser = $this->parserFactory->getInstance();
742 $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
743 // @phan-suppress-next-line PhanUndeclaredMethod
744 $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
745 $result_array['parsetree'] = $xml;
746 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
747 }
748
749 $result_mapping = [
750 'redirects' => 'r',
751 'langlinks' => 'll',
752 'categories' => 'cl',
753 'links' => 'pl',
754 'templates' => 'tl',
755 'images' => 'img',
756 'externallinks' => 'el',
757 'iwlinks' => 'iw',
758 'sections' => 's',
759 'tocdata' => 'toc',
760 'headitems' => 'hi',
761 'modules' => 'm',
762 'indicators' => 'ind',
763 'modulescripts' => 'm',
764 'modulestyles' => 'm',
765 'properties' => 'pp',
766 'limitreportdata' => 'lr',
767 'parsewarnings' => 'pw',
768 'parsewarningshtml' => 'pw',
769 ];
770 $this->setIndexedTagNames( $result_array, $result_mapping );
771 $result->addValue( null, $this->getModuleName(), $result_array );
772 }
773
782 private function makeParserOptions( WikiPage $pageObj, array $params ) {
783 if ( $params['usearticle'] ) {
784 # T349037: The ArticleParserOptions hook should be broadened to take
785 # a WikiPage (aka $pageObj) instead of an Article. But for now
786 # fake the Article.
787 $article = Article::newFromWikiPage( $pageObj, $this->getContext() );
788 # Allow extensions to vary parser options used for article rendering,
789 # in the same way Article does
790 $popts = $article->getParserOptions();
791 } else {
792 $popts = $pageObj->makeParserOptions( $this->getContext() );
793 }
794 // Overwrite render reason
795 $popts->setRenderReason( 'api-parse' );
796 return $this->tweakParserOptions( $popts, $pageObj->getTitle(), $params );
797 }
798
808 private function tweakParserOptions( ParserOptions $popts, Title $title, array $params ) {
809 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
810 $popts->setIsSectionPreview( $params['sectionpreview'] );
811
812 if ( $params['wrapoutputclass'] !== '' ) {
813 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
814 }
815 if ( $params['parsoid'] || $params['parser'] === 'parsoid' ) {
816 $popts->setUseParsoid( true );
817 }
818 if ( $params['parser'] === 'legacy' ) {
819 $popts->setUseParsoid( false );
820 }
821 if ( $params['disableeditsection'] ) {
822 $popts->setSuppressSectionEditLinks();
823 }
824
825 $reset = null;
826 $suppressCache = false;
827 $this->getHookRunner()->onApiMakeParserOptions( $popts, $title,
828 $params, $this, $reset, $suppressCache );
829
830 return [ $popts, $reset, $suppressCache ];
831 }
832
842 private function getParsedContent(
843 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
844 ) {
845 $revId = $rev ? $rev->getId() : null;
846 $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
847
848 if ( $getContent || $this->section !== false || $isDeleted ) {
849 if ( $rev ) {
850 $this->content = $rev->getContent(
851 SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getAuthority()
852 );
853 if ( !$this->content ) {
854 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
855 }
856 } else {
857 $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
858 if ( !$this->content ) {
859 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
860 }
861 }
862 $this->contentIsDeleted = $isDeleted;
863 $this->contentIsSuppressed = $rev &&
864 $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
865 }
866
867 if ( $this->section !== false ) {
868 $this->content = $this->getSectionContent(
869 $this->content,
870 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
871 );
872 return $this->getContentParserOutput(
873 $this->content, $page->getTitle(),
874 $rev,
875 $popts
876 );
877 }
878
879 if ( $isDeleted ) {
880 // getParserOutput can't do revdeled revisions
881
882 $pout = $this->getContentParserOutput(
883 $this->content, $page->getTitle(),
884 $rev,
885 $popts
886 );
887 } else {
888 // getParserOutput will save to Parser cache if able
889 $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
890 }
891 if ( !$pout ) {
892 // @codeCoverageIgnoreStart
893 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
894 // @codeCoverageIgnoreEnd
895 }
896
897 return $pout;
898 }
899
907 private function getSectionContent( Content $content, $what ) {
908 // Not cached (save or load)
909 $section = $content->getSection( $this->section );
910 if ( $section === false ) {
911 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
912 }
913 if ( $section === null ) {
914 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
915 }
916
917 return $section;
918 }
919
927 private function formatSummary( $title, $params ) {
928 $summary = $params['summary'] ?? '';
929 $sectionTitle = $params['sectiontitle'] ?? '';
930
931 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
932 if ( $sectionTitle !== '' ) {
933 $summary = $params['sectiontitle'];
934 }
935 if ( $summary !== '' ) {
936 $summary = $this->msg( 'newsectionsummary' )
937 ->plaintextParams( $this->parserFactory->getMainInstance()->stripSectionName( $summary ) )
938 ->inContentLanguage()->text();
939 }
940 }
941 return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
942 }
943
948 private function formatLangLinks( $links ): array {
949 $result = [];
950 foreach ( $links as $link ) {
951 $entry = [];
952 if ( is_string( $link ) ) {
953 [ $lang, $titleWithFrag ] = explode( ':', $link, 2 );
954 [ $title, $frag ] = array_pad( explode( '#', $titleWithFrag, 2 ), 2, '' );
955 $title = TitleValue::tryNew( NS_MAIN, $title, $frag, $lang );
956 } else {
957 $title = $link;
958 $lang = $link->getInterwiki();
959 $titleWithFrag = $link->getText();
960 if ( $link->hasFragment() ) {
961 $titleWithFrag .= '#' . $link->getFragment();
962 }
963 }
964 $title = Title::castFromLinkTarget( $title );
965
966 $entry['lang'] = $lang;
967 if ( $title ) {
968 $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
969 // title validity implies language code validity
970 // localised language name in 'uselang' language
971 $entry['langname'] = $this->languageNameUtils->getLanguageName(
972 $lang,
973 $this->getLanguage()->getCode()
974 );
975
976 // native language name
977 $entry['autonym'] = $this->languageNameUtils->getLanguageName( $lang );
978 }
979 ApiResult::setContentValue( $entry, 'title', $titleWithFrag );
980 $result[] = $entry;
981 }
982
983 return $result;
984 }
985
986 private function formatCategoryLinks( array $links ): array {
987 $result = [];
988
989 if ( !$links ) {
990 return $result;
991 }
992
993 // Fetch hiddencat property
994 $lb = $this->linkBatchFactory->newLinkBatch();
995 $lb->setArray( [ NS_CATEGORY => $links ] );
996 $db = $this->getDB();
997 $res = $db->newSelectQueryBuilder()
998 ->select( [ 'page_title', 'pp_propname' ] )
999 ->from( 'page' )
1000 ->where( $lb->constructSet( 'page', $db ) )
1001 ->leftJoin( 'page_props', null, [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] )
1002 ->caller( __METHOD__ )
1003 ->fetchResultSet();
1004 $hiddencats = [];
1005 foreach ( $res as $row ) {
1006 $hiddencats[$row->page_title] = isset( $row->pp_propname );
1007 }
1008
1009 foreach ( $links as $link => $sortkey ) {
1010 $entry = [];
1011 $entry['sortkey'] = $sortkey;
1012 // array keys will cast numeric category names to ints, so cast back to string
1013 ApiResult::setContentValue( $entry, 'category', (string)$link );
1014 if ( !isset( $hiddencats[$link] ) ) {
1015 $entry['missing'] = true;
1016
1017 // We already know the link doesn't exist in the database, so
1018 // tell LinkCache that before calling $title->isKnown().
1019 $title = Title::makeTitle( NS_CATEGORY, $link );
1020 $this->linkCache->addBadLinkObj( $title );
1021 if ( $title->isKnown() ) {
1022 $entry['known'] = true;
1023 }
1024 } elseif ( $hiddencats[$link] ) {
1025 $entry['hidden'] = true;
1026 }
1027 $result[] = $entry;
1028 }
1029
1030 return $result;
1031 }
1032
1037 private function formatLinks( array $links ): array {
1038 $result = [];
1039 foreach ( $links as [ 'link' => $link, 'pageid' => $id ] ) {
1040 $entry = [];
1041 $entry['ns'] = $link->getNamespace();
1042 ApiResult::setContentValue( $entry, 'title', Title::newFromLinkTarget( $link )->getFullText() );
1043 $entry['exists'] = $id != 0;
1044 $result[] = $entry;
1045 }
1046
1047 return $result;
1048 }
1049
1050 private function formatIWLinks( array $iw ): array {
1051 $result = [];
1052 foreach ( $iw as $linkTarget ) {
1053 $entry = [];
1054 $entry['prefix'] = $linkTarget->getInterwiki();
1055 $title = Title::newFromLinkTarget( $linkTarget );
1056 if ( $title ) {
1057 $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
1058
1059 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
1060 }
1061 $result[] = $entry;
1062 }
1063
1064 return $result;
1065 }
1066
1067 private function formatHeadItems( array $headItems ): array {
1068 $result = [];
1069 foreach ( $headItems as $tag => $content ) {
1070 $entry = [];
1071 $entry['tag'] = $tag;
1072 ApiResult::setContentValue( $entry, 'content', $content );
1073 $result[] = $entry;
1074 }
1075
1076 return $result;
1077 }
1078
1079 private function formatLimitReportData( array $limitReportData ): array {
1080 $result = [];
1081
1082 foreach ( $limitReportData as $name => $value ) {
1083 $entry = [];
1084 $entry['name'] = $name;
1085 if ( !is_array( $value ) ) {
1086 $value = [ $value ];
1087 }
1088 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
1089 $entry = array_merge( $entry, $value );
1090 $result[] = $entry;
1091 }
1092
1093 return $result;
1094 }
1095
1096 private function setIndexedTagNames( array &$array, array $mapping ) {
1097 foreach ( $mapping as $key => $name ) {
1098 if ( isset( $array[$key] ) ) {
1099 ApiResult::setIndexedTagName( $array[$key], $name );
1100 }
1101 }
1102 }
1103
1105 public function getAllowedParams() {
1106 return [
1107 'title' => null,
1108 'text' => [
1109 ParamValidator::PARAM_TYPE => 'text',
1110 ],
1111 'revid' => [
1112 ParamValidator::PARAM_TYPE => 'integer',
1113 ],
1114 'summary' => null,
1115 'page' => null,
1116 'pageid' => [
1117 ParamValidator::PARAM_TYPE => 'integer',
1118 ],
1119 'redirects' => false,
1120 'oldid' => [
1121 ParamValidator::PARAM_TYPE => 'integer',
1122 ],
1123 'prop' => [
1124 ParamValidator::PARAM_DEFAULT => 'text|langlinks|categories|links|templates|' .
1125 'images|externallinks|sections|tocdata|revid|displaytitle|iwlinks|' .
1126 'properties|parsewarnings',
1127 ParamValidator::PARAM_ISMULTI => true,
1128 ParamValidator::PARAM_TYPE => [
1129 'text',
1130 'langlinks',
1131 'categories',
1132 'categorieshtml',
1133 'links',
1134 'templates',
1135 'images',
1136 'externallinks',
1137 'sections',
1138 'tocdata',
1139 'revid',
1140 'displaytitle',
1141 'subtitle',
1142 'headhtml',
1143 'modules',
1144 'jsconfigvars',
1145 'encodedjsconfigvars',
1146 'indicators',
1147 'iwlinks',
1148 'wikitext',
1149 'properties',
1150 'limitreportdata',
1151 'limitreporthtml',
1152 'parseroutput',
1153 'parsetree',
1154 'parsewarnings',
1155 'parsewarningshtml',
1156 'headitems',
1157 ],
1158 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
1159 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
1160 ],
1161 EnumDef::PARAM_DEPRECATED_VALUES => [
1162 'headitems' => 'apiwarn-deprecation-parse-headitems',
1163 // deprecated since 1.46: T319141
1164 'sections' => [ 'apiwarn-deprecation-withreplacement', 'prop=sections', 'prop=tocdata' ],
1165 ],
1166 EnumDef::PARAM_INTERNAL_VALUES => [
1167 'parseroutput' => true, // since 1.47: T428786
1168 ],
1169 ],
1170 'wrapoutputclass' => 'mw-parser-output',
1171 'usearticle' => false, // since 1.43
1172 'parsoid' => [ // since 1.41
1173 ParamValidator::PARAM_TYPE => 'boolean',
1174 ParamValidator::PARAM_DEFAULT => false,
1175 ParamValidator::PARAM_DEPRECATED => true,
1176 ],
1177 'parser' => [ // since 1.45
1178 ParamValidator::PARAM_TYPE => [
1179 'parsoid',
1180 'default',
1181 'legacy'
1182 ],
1183 ParamValidator::PARAM_DEFAULT => 'default',
1184 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1185 ],
1186 'pst' => false,
1187 'onlypst' => false,
1188 'effectivelanglinks' => [
1189 ParamValidator::PARAM_DEFAULT => false,
1190 ParamValidator::PARAM_DEPRECATED => true,
1191 ],
1192 'section' => null,
1193 'sectiontitle' => [
1194 ParamValidator::PARAM_TYPE => 'string',
1195 ],
1196 'disablepp' => [
1197 ParamValidator::PARAM_DEFAULT => false,
1198 ParamValidator::PARAM_DEPRECATED => true,
1199 ],
1200 'disablelimitreport' => false,
1201 'disableeditsection' => false,
1202 'disablestylededuplication' => false,
1203 'showstrategykeys' => false,
1204 'generatexml' => [
1205 ParamValidator::PARAM_DEFAULT => false,
1206 ApiBase::PARAM_HELP_MSG => [
1207 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
1208 ],
1209 ParamValidator::PARAM_DEPRECATED => true,
1210 ],
1211 'preview' => false,
1212 'sectionpreview' => false,
1213 'disabletoc' => false,
1214 'useskin' => [
1215 // T237856; We use all installed skins here to allow hidden (but usable) skins
1216 // to continue working correctly with some features such as Live Preview
1217 ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
1218 ],
1219 'contentformat' => [
1220 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
1221 ],
1222 'contentmodel' => [
1223 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
1224 ],
1225 ];
1226 }
1227
1229 protected function getExamplesMessages() {
1230 return [
1231 'action=parse&page=Project:Sandbox'
1232 => 'apihelp-parse-example-page',
1233 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
1234 => 'apihelp-parse-example-text',
1235 'action=parse&text={{PAGENAME}}&title=Test'
1236 => 'apihelp-parse-example-texttitle',
1237 'action=parse&summary=Some+[[link]]&prop='
1238 => 'apihelp-parse-example-summary',
1239 ];
1240 }
1241
1243 public function getHelpUrls() {
1244 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext';
1245 }
1246}
1247
1249class_alias( ApiParse::class, 'ApiParse' );
const PROTO_CURRENT
Definition Defines.php:222
const NS_MAIN
Definition Defines.php:51
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:235
const NS_CATEGORY
Definition Defines.php:65
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli') global $wgTitle
Definition Setup.php:527
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:60
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:66
This class contains a list of pages that the client has requested.
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition ApiParse.php:158
__construct(ApiMain $main, string $action, private readonly RevisionLookup $revisionLookup, private readonly SkinFactory $skinFactory, private readonly LanguageNameUtils $languageNameUtils, private readonly LinkBatchFactory $linkBatchFactory, private readonly LinkCache $linkCache, private readonly IContentHandlerFactory $contentHandlerFactory, private readonly ParserFactory $parserFactory, private readonly WikiPageFactory $wikiPageFactory, private readonly ContentRenderer $contentRenderer, private readonly ContentTransformer $contentTransformer, private readonly CommentFormatter $commentFormatter, private readonly TempUserCreator $tempUserCreator, private readonly UserFactory $userFactory, private readonly UrlUtils $urlUtils, private readonly TitleFormatter $titleFormatter, private readonly JsonCodec $jsonCodec,)
Definition ApiParse.php:74
This is the main service interface for converting single-line comments from various DB comment fields...
Exception representing a failure to serialize or unserialize a content object.
An IContextSource implementation which will inherit context from another source but allow individual ...
The HTML user interface for page editing.
Definition EditPage.php:132
JSON formatter wrapper class.
A service that provides utilities to do with language names and codes.
Variant of the Message class.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
This is one of the Core classes and should be read at least once by any new developers.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:67
Factory for LinkBatch objects to batch query page metadata.
Page existence and metadata cache.
Definition LinkCache.php:52
Service for creating WikiPage objects.
Base representation for an editable wiki page.
Definition WikiPage.php:83
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition WikiPage.php:695
getTitle()
Get the title object of the article.
Definition WikiPage.php:251
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Set options of the Parser.
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:138
Convenience class for dealing with PoolCounter using callbacks.
WebRequest clone which takes values from a provided array.
Page revision base class.
Value object representing a content slot associated with a page revision.
Factory class to create Skin objects.
The base class for all skins.
Definition Skin.php:54
A title formatter service for MediaWiki.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Service for temporary user creation.
Create 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:19
Service for formatting and validating API parameters.
Type definition for enumeration types.
Definition EnumDef.php:32
Content objects represent page content, e.g.
Definition Content.php:28
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Service for looking up page revisions.
Interface for objects representing user identity.
array $params
The job parameters.
msg( $key,... $params)