Merge "Drop wgPasswordSalt, deprecated since 1.24"
[lhc/web/wiklou.git] / includes / api / ApiParse.php
1 <?php
2 /**
3 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23 use MediaWiki\MediaWikiServices;
24 use MediaWiki\Storage\RevisionRecord;
25
26 /**
27 * @ingroup API
28 */
29 class ApiParse extends ApiBase {
30
31 /** @var string $section */
32 private $section = null;
33
34 /** @var Content $content */
35 private $content = null;
36
37 /** @var Content $pstContent */
38 private $pstContent = null;
39
40 /** @var bool */
41 private $contentIsDeleted = false, $contentIsSuppressed = false;
42
43 public function execute() {
44 // The data is hot but user-dependent, like page views, so we set vary cookies
45 $this->getMain()->setCacheMode( 'anon-public-user-private' );
46
47 // Get parameters
48 $params = $this->extractRequestParams();
49
50 // No easy way to say that text and title or revid are allowed together
51 // while the rest aren't, so just do it in three calls.
52 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' );
53 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' );
54 $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'revid' );
55
56 $text = $params['text'];
57 $title = $params['title'];
58 if ( $title === null ) {
59 $titleProvided = false;
60 // A title is needed for parsing, so arbitrarily choose one
61 $title = 'API';
62 } else {
63 $titleProvided = true;
64 }
65
66 $page = $params['page'];
67 $pageid = $params['pageid'];
68 $oldid = $params['oldid'];
69
70 $model = $params['contentmodel'];
71 $format = $params['contentformat'];
72
73 $prop = array_flip( $params['prop'] );
74
75 if ( isset( $params['section'] ) ) {
76 $this->section = $params['section'];
77 if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
78 $this->dieWithError( 'apierror-invalidsection' );
79 }
80 } else {
81 $this->section = false;
82 }
83
84 // The parser needs $wgTitle to be set, apparently the
85 // $title parameter in Parser::parse isn't enough *sigh*
86 // TODO: Does this still need $wgTitle?
87 global $wgTitle;
88
89 $redirValues = null;
90
91 $needContent = isset( $prop['wikitext'] ) ||
92 isset( $prop['parsetree'] ) || $params['generatexml'];
93
94 // Return result
95 $result = $this->getResult();
96
97 if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) {
98 if ( $this->section === 'new' ) {
99 $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' );
100 }
101 if ( !is_null( $oldid ) ) {
102 // Don't use the parser cache
103 $rev = Revision::newFromId( $oldid );
104 if ( !$rev ) {
105 $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] );
106 }
107
108 $this->checkTitleUserPermissions( $rev->getTitle(), 'read' );
109 if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() ) ) {
110 $this->dieWithError(
111 [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ]
112 );
113 }
114
115 $titleObj = $rev->getTitle();
116 $wgTitle = $titleObj;
117 $pageObj = WikiPage::factory( $titleObj );
118 list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params );
119 $p_result = $this->getParsedContent(
120 $pageObj, $popts, $suppressCache, $pageid, $rev, $needContent
121 );
122 } else { // Not $oldid, but $pageid or $page
123 if ( $params['redirects'] ) {
124 $reqParams = [
125 'redirects' => '',
126 ];
127 if ( !is_null( $pageid ) ) {
128 $reqParams['pageids'] = $pageid;
129 } else { // $page
130 $reqParams['titles'] = $page;
131 }
132 $req = new FauxRequest( $reqParams );
133 $main = new ApiMain( $req );
134 $pageSet = new ApiPageSet( $main );
135 $pageSet->execute();
136 $redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() );
137
138 $to = $page;
139 foreach ( $pageSet->getRedirectTitles() as $title ) {
140 $to = $title->getFullText();
141 }
142 $pageParams = [ 'title' => $to ];
143 } elseif ( !is_null( $pageid ) ) {
144 $pageParams = [ 'pageid' => $pageid ];
145 } else { // $page
146 $pageParams = [ 'title' => $page ];
147 }
148
149 $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
150 $titleObj = $pageObj->getTitle();
151 if ( !$titleObj || !$titleObj->exists() ) {
152 $this->dieWithError( 'apierror-missingtitle' );
153 }
154
155 $this->checkTitleUserPermissions( $titleObj, 'read' );
156 $wgTitle = $titleObj;
157
158 if ( isset( $prop['revid'] ) ) {
159 $oldid = $pageObj->getLatest();
160 }
161
162 list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params );
163 $p_result = $this->getParsedContent(
164 $pageObj, $popts, $suppressCache, $pageid, null, $needContent
165 );
166 }
167 } else { // Not $oldid, $pageid, $page. Hence based on $text
168 $titleObj = Title::newFromText( $title );
169 if ( !$titleObj || $titleObj->isExternal() ) {
170 $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
171 }
172 $revid = $params['revid'];
173 if ( $revid !== null ) {
174 $rev = Revision::newFromId( $revid );
175 if ( !$rev ) {
176 $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] );
177 }
178 $pTitleObj = $titleObj;
179 $titleObj = $rev->getTitle();
180 if ( $titleProvided ) {
181 if ( !$titleObj->equals( $pTitleObj ) ) {
182 $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(),
183 wfEscapeWikiText( $pTitleObj->getPrefixedText() ) ] );
184 }
185 } else {
186 // Consider the title derived from the revid as having
187 // been provided.
188 $titleProvided = true;
189 }
190 }
191 $wgTitle = $titleObj;
192 if ( $titleObj->canExist() ) {
193 $pageObj = WikiPage::factory( $titleObj );
194 } else {
195 // Do like MediaWiki::initializeArticle()
196 $article = Article::newFromTitle( $titleObj, $this->getContext() );
197 $pageObj = $article->getPage();
198 }
199
200 list( $popts, $reset ) = $this->makeParserOptions( $pageObj, $params );
201 $textProvided = !is_null( $text );
202
203 if ( !$textProvided ) {
204 if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
205 if ( $revid !== null ) {
206 $this->addWarning( 'apiwarn-parse-revidwithouttext' );
207 } else {
208 $this->addWarning( 'apiwarn-parse-titlewithouttext' );
209 }
210 }
211 // Prevent warning from ContentHandler::makeContent()
212 $text = '';
213 }
214
215 // If we are parsing text, do not use the content model of the default
216 // API title, but default to wikitext to keep BC.
217 if ( $textProvided && !$titleProvided && is_null( $model ) ) {
218 $model = CONTENT_MODEL_WIKITEXT;
219 $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] );
220 }
221
222 try {
223 $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
224 } catch ( MWContentSerializationException $ex ) {
225 $this->dieWithException( $ex, [
226 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
227 ] );
228 }
229
230 if ( $this->section !== false ) {
231 if ( $this->section === 'new' ) {
232 // Insert the section title above the content.
233 if ( !is_null( $params['sectiontitle'] ) && $params['sectiontitle'] !== '' ) {
234 $this->content = $this->content->addSectionHeader( $params['sectiontitle'] );
235 }
236 } else {
237 $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
238 }
239 }
240
241 if ( $params['pst'] || $params['onlypst'] ) {
242 $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
243 }
244 if ( $params['onlypst'] ) {
245 // Build a result and bail out
246 $result_array = [];
247 $result_array['text'] = $this->pstContent->serialize( $format );
248 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
249 if ( isset( $prop['wikitext'] ) ) {
250 $result_array['wikitext'] = $this->content->serialize( $format );
251 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
252 }
253 if ( !is_null( $params['summary'] ) ||
254 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
255 ) {
256 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
257 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
258 }
259
260 $result->addValue( null, $this->getModuleName(), $result_array );
261
262 return;
263 }
264
265 // Not cached (save or load)
266 if ( $params['pst'] ) {
267 $p_result = $this->pstContent->getParserOutput( $titleObj, $revid, $popts );
268 } else {
269 $p_result = $this->content->getParserOutput( $titleObj, $revid, $popts );
270 }
271 }
272
273 $result_array = [];
274
275 $result_array['title'] = $titleObj->getPrefixedText();
276 $result_array['pageid'] = $pageid ?: $pageObj->getId();
277 if ( $this->contentIsDeleted ) {
278 $result_array['textdeleted'] = true;
279 }
280 if ( $this->contentIsSuppressed ) {
281 $result_array['textsuppressed'] = true;
282 }
283
284 if ( isset( $params['useskin'] ) ) {
285 $factory = MediaWikiServices::getInstance()->getSkinFactory();
286 $skin = $factory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
287 } else {
288 $skin = null;
289 }
290
291 $outputPage = null;
292 if ( $skin || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ) {
293 // Enabling the skin via 'useskin', 'headhtml', or 'categorieshtml'
294 // gets OutputPage and Skin involved, which (among others) applies
295 // these hooks:
296 // - ParserOutputHooks
297 // - Hook: LanguageLinks
298 // - Hook: OutputPageParserOutput
299 // - Hook: OutputPageMakeCategoryLinks
300 $context = new DerivativeContext( $this->getContext() );
301 $context->setTitle( $titleObj );
302 $context->setWikiPage( $pageObj );
303
304 if ( $skin ) {
305 // Use the skin specified by 'useskin'
306 $context->setSkin( $skin );
307 // Context clones the skin, refetch to stay in sync. (T166022)
308 $skin = $context->getSkin();
309 } else {
310 // Make sure the context's skin refers to the context. Without this,
311 // $outputPage->getSkin()->getOutput() !== $outputPage which
312 // confuses some of the output.
313 $context->setSkin( $context->getSkin() );
314 }
315
316 $outputPage = new OutputPage( $context );
317 $outputPage->addParserOutputMetadata( $p_result );
318 if ( $this->content ) {
319 $outputPage->addContentOverride( $titleObj, $this->content );
320 }
321 $context->setOutput( $outputPage );
322
323 if ( $skin ) {
324 // Based on OutputPage::headElement()
325 $skin->setupSkinUserCss( $outputPage );
326 // Based on OutputPage::output()
327 $outputPage->loadSkinModules( $skin );
328 }
329
330 Hooks::run( 'ApiParseMakeOutputPage', [ $this, $outputPage ] );
331 }
332
333 if ( !is_null( $oldid ) ) {
334 $result_array['revid'] = (int)$oldid;
335 }
336
337 if ( $params['redirects'] && !is_null( $redirValues ) ) {
338 $result_array['redirects'] = $redirValues;
339 }
340
341 if ( isset( $prop['text'] ) ) {
342 $result_array['text'] = $p_result->getText( [
343 'allowTOC' => !$params['disabletoc'],
344 'enableSectionEditLinks' => !$params['disableeditsection'],
345 'wrapperDivClass' => $params['wrapoutputclass'],
346 'deduplicateStyles' => !$params['disablestylededuplication'],
347 ] );
348 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
349 }
350
351 if ( !is_null( $params['summary'] ) ||
352 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
353 ) {
354 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
355 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
356 }
357
358 if ( isset( $prop['langlinks'] ) ) {
359 if ( $skin ) {
360 $langlinks = $outputPage->getLanguageLinks();
361 } else {
362 $langlinks = $p_result->getLanguageLinks();
363 // The deprecated 'effectivelanglinks' option depredates OutputPage
364 // support via 'useskin'. If not already applied, then run just this
365 // one hook of OutputPage::addParserOutputMetadata here.
366 if ( $params['effectivelanglinks'] ) {
367 $linkFlags = [];
368 Hooks::run( 'LanguageLinks', [ $titleObj, &$langlinks, &$linkFlags ] );
369 }
370 }
371
372 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
373 }
374 if ( isset( $prop['categories'] ) ) {
375 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
376 }
377 if ( isset( $prop['categorieshtml'] ) ) {
378 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
379 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
380 }
381 if ( isset( $prop['links'] ) ) {
382 $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
383 }
384 if ( isset( $prop['templates'] ) ) {
385 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
386 }
387 if ( isset( $prop['images'] ) ) {
388 $result_array['images'] = array_keys( $p_result->getImages() );
389 }
390 if ( isset( $prop['externallinks'] ) ) {
391 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
392 }
393 if ( isset( $prop['sections'] ) ) {
394 $result_array['sections'] = $p_result->getSections();
395 }
396 if ( isset( $prop['parsewarnings'] ) ) {
397 $result_array['parsewarnings'] = $p_result->getWarnings();
398 }
399
400 if ( isset( $prop['displaytitle'] ) ) {
401 $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
402 ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText();
403 }
404
405 if ( isset( $prop['headitems'] ) ) {
406 if ( $skin ) {
407 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
408 } else {
409 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
410 }
411 }
412
413 if ( isset( $prop['headhtml'] ) ) {
414 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
415 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
416 }
417
418 if ( isset( $prop['modules'] ) ) {
419 if ( $skin ) {
420 $result_array['modules'] = $outputPage->getModules();
421 // Deprecated since 1.32 (T188689)
422 $result_array['modulescripts'] = [];
423 $result_array['modulestyles'] = $outputPage->getModuleStyles();
424 } else {
425 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
426 // Deprecated since 1.32 (T188689)
427 $result_array['modulescripts'] = [];
428 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
429 }
430 }
431
432 if ( isset( $prop['jsconfigvars'] ) ) {
433 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
434 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
435 }
436
437 if ( isset( $prop['encodedjsconfigvars'] ) ) {
438 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
439 $result_array['encodedjsconfigvars'] = FormatJson::encode(
440 $jsconfigvars,
441 false,
442 FormatJson::ALL_OK
443 );
444 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
445 }
446
447 if ( isset( $prop['modules'] ) &&
448 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
449 $this->addWarning( 'apiwarn-moduleswithoutvars' );
450 }
451
452 if ( isset( $prop['indicators'] ) ) {
453 if ( $skin ) {
454 $result_array['indicators'] = (array)$outputPage->getIndicators();
455 } else {
456 $result_array['indicators'] = (array)$p_result->getIndicators();
457 }
458 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
459 }
460
461 if ( isset( $prop['iwlinks'] ) ) {
462 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
463 }
464
465 if ( isset( $prop['wikitext'] ) ) {
466 $result_array['wikitext'] = $this->content->serialize( $format );
467 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
468 if ( !is_null( $this->pstContent ) ) {
469 $result_array['psttext'] = $this->pstContent->serialize( $format );
470 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
471 }
472 }
473 if ( isset( $prop['properties'] ) ) {
474 $result_array['properties'] = (array)$p_result->getProperties();
475 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
476 }
477
478 if ( isset( $prop['limitreportdata'] ) ) {
479 $result_array['limitreportdata'] =
480 $this->formatLimitReportData( $p_result->getLimitReportData() );
481 }
482 if ( isset( $prop['limitreporthtml'] ) ) {
483 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
484 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
485 }
486
487 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
488 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
489 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
490 }
491
492 $parser = MediaWikiServices::getInstance()->getParser();
493 $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
494 $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
495 $result_array['parsetree'] = $xml;
496 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
497 }
498
499 $result_mapping = [
500 'redirects' => 'r',
501 'langlinks' => 'll',
502 'categories' => 'cl',
503 'links' => 'pl',
504 'templates' => 'tl',
505 'images' => 'img',
506 'externallinks' => 'el',
507 'iwlinks' => 'iw',
508 'sections' => 's',
509 'headitems' => 'hi',
510 'modules' => 'm',
511 'indicators' => 'ind',
512 'modulescripts' => 'm',
513 'modulestyles' => 'm',
514 'properties' => 'pp',
515 'limitreportdata' => 'lr',
516 'parsewarnings' => 'pw'
517 ];
518 $this->setIndexedTagNames( $result_array, $result_mapping );
519 $result->addValue( null, $this->getModuleName(), $result_array );
520 }
521
522 /**
523 * Constructs a ParserOptions object
524 *
525 * @param WikiPage $pageObj
526 * @param array $params
527 *
528 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
529 */
530 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
531 $popts = $pageObj->makeParserOptions( $this->getContext() );
532 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
533 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
534 $popts->setIsSectionPreview( $params['sectionpreview'] );
535 if ( $params['disabletidy'] ) {
536 $popts->setTidy( false );
537 }
538 if ( $params['wrapoutputclass'] !== '' ) {
539 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
540 }
541
542 $reset = null;
543 $suppressCache = false;
544 Hooks::run( 'ApiMakeParserOptions',
545 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
546
547 // Force cache suppression when $popts aren't cacheable.
548 $suppressCache = $suppressCache || !$popts->isSafeToCache();
549
550 return [ $popts, $reset, $suppressCache ];
551 }
552
553 /**
554 * @param WikiPage $page
555 * @param ParserOptions $popts
556 * @param bool $suppressCache
557 * @param int $pageId
558 * @param Revision|null $rev
559 * @param bool $getContent
560 * @return ParserOutput
561 */
562 private function getParsedContent(
563 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
564 ) {
565 $revId = $rev ? $rev->getId() : null;
566 $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
567
568 if ( $getContent || $this->section !== false || $isDeleted ) {
569 if ( $rev ) {
570 $this->content = $rev->getContent( RevisionRecord::FOR_THIS_USER, $this->getUser() );
571 if ( !$this->content ) {
572 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
573 }
574 } else {
575 $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getUser() );
576 if ( !$this->content ) {
577 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
578 }
579 }
580 $this->contentIsDeleted = $isDeleted;
581 $this->contentIsSuppressed = $rev &&
582 $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
583 }
584
585 if ( $this->section !== false ) {
586 $this->content = $this->getSectionContent(
587 $this->content,
588 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
589 );
590 return $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
591 }
592
593 if ( $isDeleted ) {
594 // getParserOutput can't do revdeled revisions
595 $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
596 } else {
597 // getParserOutput will save to Parser cache if able
598 $pout = $page->getParserOutput( $popts, $revId, $suppressCache );
599 }
600 if ( !$pout ) {
601 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); // @codeCoverageIgnore
602 }
603
604 return $pout;
605 }
606
607 /**
608 * Extract the requested section from the given Content
609 *
610 * @param Content $content
611 * @param string|Message $what Identifies the content in error messages, e.g. page title.
612 * @return Content
613 */
614 private function getSectionContent( Content $content, $what ) {
615 // Not cached (save or load)
616 $section = $content->getSection( $this->section );
617 if ( $section === false ) {
618 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
619 }
620 if ( $section === null ) {
621 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
622 $section = false;
623 }
624
625 return $section;
626 }
627
628 /**
629 * This mimicks the behavior of EditPage in formatting a summary
630 *
631 * @param Title $title of the page being parsed
632 * @param array $params The API parameters of the request
633 * @return Content|bool
634 */
635 private function formatSummary( $title, $params ) {
636 $summary = $params['summary'] ?? '';
637 $sectionTitle = $params['sectiontitle'] ?? '';
638
639 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
640 if ( $sectionTitle !== '' ) {
641 $summary = $params['sectiontitle'];
642 }
643 if ( $summary !== '' ) {
644 $summary = wfMessage( 'newsectionsummary' )
645 ->rawParams( MediaWikiServices::getInstance()->getParser()
646 ->stripSectionName( $summary ) )
647 ->inContentLanguage()->text();
648 }
649 }
650 return Linker::formatComment( $summary, $title, $this->section === 'new' );
651 }
652
653 private function formatLangLinks( $links ) {
654 $result = [];
655 foreach ( $links as $link ) {
656 $entry = [];
657 $bits = explode( ':', $link, 2 );
658 $title = Title::newFromText( $link );
659
660 $entry['lang'] = $bits[0];
661 if ( $title ) {
662 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
663 // localised language name in 'uselang' language
664 $entry['langname'] = Language::fetchLanguageName(
665 $title->getInterwiki(),
666 $this->getLanguage()->getCode()
667 );
668
669 // native language name
670 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
671 }
672 ApiResult::setContentValue( $entry, 'title', $bits[1] );
673 $result[] = $entry;
674 }
675
676 return $result;
677 }
678
679 private function formatCategoryLinks( $links ) {
680 $result = [];
681
682 if ( !$links ) {
683 return $result;
684 }
685
686 // Fetch hiddencat property
687 $lb = new LinkBatch;
688 $lb->setArray( [ NS_CATEGORY => $links ] );
689 $db = $this->getDB();
690 $res = $db->select( [ 'page', 'page_props' ],
691 [ 'page_title', 'pp_propname' ],
692 $lb->constructSet( 'page', $db ),
693 __METHOD__,
694 [],
695 [ 'page_props' => [
696 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
697 ] ]
698 );
699 $hiddencats = [];
700 foreach ( $res as $row ) {
701 $hiddencats[$row->page_title] = isset( $row->pp_propname );
702 }
703
704 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
705
706 foreach ( $links as $link => $sortkey ) {
707 $entry = [];
708 $entry['sortkey'] = $sortkey;
709 // array keys will cast numeric category names to ints, so cast back to string
710 ApiResult::setContentValue( $entry, 'category', (string)$link );
711 if ( !isset( $hiddencats[$link] ) ) {
712 $entry['missing'] = true;
713
714 // We already know the link doesn't exist in the database, so
715 // tell LinkCache that before calling $title->isKnown().
716 $title = Title::makeTitle( NS_CATEGORY, $link );
717 $linkCache->addBadLinkObj( $title );
718 if ( $title->isKnown() ) {
719 $entry['known'] = true;
720 }
721 } elseif ( $hiddencats[$link] ) {
722 $entry['hidden'] = true;
723 }
724 $result[] = $entry;
725 }
726
727 return $result;
728 }
729
730 private function formatLinks( $links ) {
731 $result = [];
732 foreach ( $links as $ns => $nslinks ) {
733 foreach ( $nslinks as $title => $id ) {
734 $entry = [];
735 $entry['ns'] = $ns;
736 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
737 $entry['exists'] = $id != 0;
738 $result[] = $entry;
739 }
740 }
741
742 return $result;
743 }
744
745 private function formatIWLinks( $iw ) {
746 $result = [];
747 foreach ( $iw as $prefix => $titles ) {
748 foreach ( array_keys( $titles ) as $title ) {
749 $entry = [];
750 $entry['prefix'] = $prefix;
751
752 $title = Title::newFromText( "{$prefix}:{$title}" );
753 if ( $title ) {
754 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
755 }
756
757 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
758 $result[] = $entry;
759 }
760 }
761
762 return $result;
763 }
764
765 private function formatHeadItems( $headItems ) {
766 $result = [];
767 foreach ( $headItems as $tag => $content ) {
768 $entry = [];
769 $entry['tag'] = $tag;
770 ApiResult::setContentValue( $entry, 'content', $content );
771 $result[] = $entry;
772 }
773
774 return $result;
775 }
776
777 private function formatLimitReportData( $limitReportData ) {
778 $result = [];
779
780 foreach ( $limitReportData as $name => $value ) {
781 $entry = [];
782 $entry['name'] = $name;
783 if ( !is_array( $value ) ) {
784 $value = [ $value ];
785 }
786 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
787 $entry = array_merge( $entry, $value );
788 $result[] = $entry;
789 }
790
791 return $result;
792 }
793
794 private function setIndexedTagNames( &$array, $mapping ) {
795 foreach ( $mapping as $key => $name ) {
796 if ( isset( $array[$key] ) ) {
797 ApiResult::setIndexedTagName( $array[$key], $name );
798 }
799 }
800 }
801
802 public function getAllowedParams() {
803 return [
804 'title' => null,
805 'text' => [
806 ApiBase::PARAM_TYPE => 'text',
807 ],
808 'revid' => [
809 ApiBase::PARAM_TYPE => 'integer',
810 ],
811 'summary' => null,
812 'page' => null,
813 'pageid' => [
814 ApiBase::PARAM_TYPE => 'integer',
815 ],
816 'redirects' => false,
817 'oldid' => [
818 ApiBase::PARAM_TYPE => 'integer',
819 ],
820 'prop' => [
821 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
822 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
823 'properties|parsewarnings',
824 ApiBase::PARAM_ISMULTI => true,
825 ApiBase::PARAM_TYPE => [
826 'text',
827 'langlinks',
828 'categories',
829 'categorieshtml',
830 'links',
831 'templates',
832 'images',
833 'externallinks',
834 'sections',
835 'revid',
836 'displaytitle',
837 'headhtml',
838 'modules',
839 'jsconfigvars',
840 'encodedjsconfigvars',
841 'indicators',
842 'iwlinks',
843 'wikitext',
844 'properties',
845 'limitreportdata',
846 'limitreporthtml',
847 'parsetree',
848 'parsewarnings',
849 'headitems',
850 ],
851 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
852 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
853 ],
854 ApiBase::PARAM_DEPRECATED_VALUES => [
855 'headitems' => 'apiwarn-deprecation-parse-headitems',
856 ],
857 ],
858 'wrapoutputclass' => 'mw-parser-output',
859 'pst' => false,
860 'onlypst' => false,
861 'effectivelanglinks' => [
862 ApiBase::PARAM_DFLT => false,
863 ApiBase::PARAM_DEPRECATED => true,
864 ],
865 'section' => null,
866 'sectiontitle' => [
867 ApiBase::PARAM_TYPE => 'string',
868 ],
869 'disablepp' => [
870 ApiBase::PARAM_DFLT => false,
871 ApiBase::PARAM_DEPRECATED => true,
872 ],
873 'disablelimitreport' => false,
874 'disableeditsection' => false,
875 'disabletidy' => [
876 ApiBase::PARAM_DFLT => false,
877 ApiBase::PARAM_DEPRECATED => true, // Since 1.32
878 ],
879 'disablestylededuplication' => false,
880 'generatexml' => [
881 ApiBase::PARAM_DFLT => false,
882 ApiBase::PARAM_HELP_MSG => [
883 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
884 ],
885 ApiBase::PARAM_DEPRECATED => true,
886 ],
887 'preview' => false,
888 'sectionpreview' => false,
889 'disabletoc' => false,
890 'useskin' => [
891 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ),
892 ],
893 'contentformat' => [
894 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
895 ],
896 'contentmodel' => [
897 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
898 ]
899 ];
900 }
901
902 protected function getExamplesMessages() {
903 return [
904 'action=parse&page=Project:Sandbox'
905 => 'apihelp-parse-example-page',
906 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
907 => 'apihelp-parse-example-text',
908 'action=parse&text={{PAGENAME}}&title=Test'
909 => 'apihelp-parse-example-texttitle',
910 'action=parse&summary=Some+[[link]]&prop='
911 => 'apihelp-parse-example-summary',
912 ];
913 }
914
915 public function getHelpUrls() {
916 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
917 }
918 }