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