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