Merge "Improve docs for Title::getInternalURL/getCanonicalURL"
[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 $wgParser, $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 $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
492 $xml = $wgParser->preprocessToDom( $this->content->getText() )->__toString();
493 $result_array['parsetree'] = $xml;
494 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
495 }
496
497 $result_mapping = [
498 'redirects' => 'r',
499 'langlinks' => 'll',
500 'categories' => 'cl',
501 'links' => 'pl',
502 'templates' => 'tl',
503 'images' => 'img',
504 'externallinks' => 'el',
505 'iwlinks' => 'iw',
506 'sections' => 's',
507 'headitems' => 'hi',
508 'modules' => 'm',
509 'indicators' => 'ind',
510 'modulescripts' => 'm',
511 'modulestyles' => 'm',
512 'properties' => 'pp',
513 'limitreportdata' => 'lr',
514 'parsewarnings' => 'pw'
515 ];
516 $this->setIndexedTagNames( $result_array, $result_mapping );
517 $result->addValue( null, $this->getModuleName(), $result_array );
518 }
519
520 /**
521 * Constructs a ParserOptions object
522 *
523 * @param WikiPage $pageObj
524 * @param array $params
525 *
526 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
527 */
528 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
529 $popts = $pageObj->makeParserOptions( $this->getContext() );
530 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
531 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
532 $popts->setIsSectionPreview( $params['sectionpreview'] );
533 if ( $params['disabletidy'] ) {
534 $popts->setTidy( false );
535 }
536 if ( $params['wrapoutputclass'] !== '' ) {
537 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
538 }
539
540 $reset = null;
541 $suppressCache = false;
542 Hooks::run( 'ApiMakeParserOptions',
543 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
544
545 // Force cache suppression when $popts aren't cacheable.
546 $suppressCache = $suppressCache || !$popts->isSafeToCache();
547
548 return [ $popts, $reset, $suppressCache ];
549 }
550
551 /**
552 * @param WikiPage $page
553 * @param ParserOptions $popts
554 * @param bool $suppressCache
555 * @param int $pageId
556 * @param Revision|null $rev
557 * @param bool $getContent
558 * @return ParserOutput
559 */
560 private function getParsedContent(
561 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
562 ) {
563 $revId = $rev ? $rev->getId() : null;
564 $isDeleted = $rev && $rev->isDeleted( Revision::DELETED_TEXT );
565
566 if ( $getContent || $this->section !== false || $isDeleted ) {
567 if ( $rev ) {
568 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
569 if ( !$this->content ) {
570 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
571 }
572 } else {
573 $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() );
574 if ( !$this->content ) {
575 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
576 }
577 }
578 $this->contentIsDeleted = $isDeleted;
579 $this->contentIsSuppressed = $rev &&
580 $rev->isDeleted( Revision::DELETED_TEXT | Revision::DELETED_RESTRICTED );
581 }
582
583 if ( $this->section !== false ) {
584 $this->content = $this->getSectionContent(
585 $this->content,
586 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
587 );
588 return $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
589 }
590
591 if ( $isDeleted ) {
592 // getParserOutput can't do revdeled revisions
593 $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
594 } else {
595 // getParserOutput will save to Parser cache if able
596 $pout = $page->getParserOutput( $popts, $revId, $suppressCache );
597 }
598 if ( !$pout ) {
599 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] ); // @codeCoverageIgnore
600 }
601
602 return $pout;
603 }
604
605 /**
606 * Extract the requested section from the given Content
607 *
608 * @param Content $content
609 * @param string|Message $what Identifies the content in error messages, e.g. page title.
610 * @return Content
611 */
612 private function getSectionContent( Content $content, $what ) {
613 // Not cached (save or load)
614 $section = $content->getSection( $this->section );
615 if ( $section === false ) {
616 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
617 }
618 if ( $section === null ) {
619 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
620 $section = false;
621 }
622
623 return $section;
624 }
625
626 /**
627 * This mimicks the behavior of EditPage in formatting a summary
628 *
629 * @param Title $title of the page being parsed
630 * @param array $params The API parameters of the request
631 * @return Content|bool
632 */
633 private function formatSummary( $title, $params ) {
634 global $wgParser;
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( $wgParser->stripSectionName( $summary ) )
645 ->inContentLanguage()->text();
646 }
647 }
648 return Linker::formatComment( $summary, $title, $this->section === 'new' );
649 }
650
651 private function formatLangLinks( $links ) {
652 $result = [];
653 foreach ( $links as $link ) {
654 $entry = [];
655 $bits = explode( ':', $link, 2 );
656 $title = Title::newFromText( $link );
657
658 $entry['lang'] = $bits[0];
659 if ( $title ) {
660 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
661 // localised language name in 'uselang' language
662 $entry['langname'] = Language::fetchLanguageName(
663 $title->getInterwiki(),
664 $this->getLanguage()->getCode()
665 );
666
667 // native language name
668 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
669 }
670 ApiResult::setContentValue( $entry, 'title', $bits[1] );
671 $result[] = $entry;
672 }
673
674 return $result;
675 }
676
677 private function formatCategoryLinks( $links ) {
678 $result = [];
679
680 if ( !$links ) {
681 return $result;
682 }
683
684 // Fetch hiddencat property
685 $lb = new LinkBatch;
686 $lb->setArray( [ NS_CATEGORY => $links ] );
687 $db = $this->getDB();
688 $res = $db->select( [ 'page', 'page_props' ],
689 [ 'page_title', 'pp_propname' ],
690 $lb->constructSet( 'page', $db ),
691 __METHOD__,
692 [],
693 [ 'page_props' => [
694 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
695 ] ]
696 );
697 $hiddencats = [];
698 foreach ( $res as $row ) {
699 $hiddencats[$row->page_title] = isset( $row->pp_propname );
700 }
701
702 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
703
704 foreach ( $links as $link => $sortkey ) {
705 $entry = [];
706 $entry['sortkey'] = $sortkey;
707 // array keys will cast numeric category names to ints, so cast back to string
708 ApiResult::setContentValue( $entry, 'category', (string)$link );
709 if ( !isset( $hiddencats[$link] ) ) {
710 $entry['missing'] = true;
711
712 // We already know the link doesn't exist in the database, so
713 // tell LinkCache that before calling $title->isKnown().
714 $title = Title::makeTitle( NS_CATEGORY, $link );
715 $linkCache->addBadLinkObj( $title );
716 if ( $title->isKnown() ) {
717 $entry['known'] = true;
718 }
719 } elseif ( $hiddencats[$link] ) {
720 $entry['hidden'] = true;
721 }
722 $result[] = $entry;
723 }
724
725 return $result;
726 }
727
728 private function formatLinks( $links ) {
729 $result = [];
730 foreach ( $links as $ns => $nslinks ) {
731 foreach ( $nslinks as $title => $id ) {
732 $entry = [];
733 $entry['ns'] = $ns;
734 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
735 $entry['exists'] = $id != 0;
736 $result[] = $entry;
737 }
738 }
739
740 return $result;
741 }
742
743 private function formatIWLinks( $iw ) {
744 $result = [];
745 foreach ( $iw as $prefix => $titles ) {
746 foreach ( array_keys( $titles ) as $title ) {
747 $entry = [];
748 $entry['prefix'] = $prefix;
749
750 $title = Title::newFromText( "{$prefix}:{$title}" );
751 if ( $title ) {
752 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
753 }
754
755 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
756 $result[] = $entry;
757 }
758 }
759
760 return $result;
761 }
762
763 private function formatHeadItems( $headItems ) {
764 $result = [];
765 foreach ( $headItems as $tag => $content ) {
766 $entry = [];
767 $entry['tag'] = $tag;
768 ApiResult::setContentValue( $entry, 'content', $content );
769 $result[] = $entry;
770 }
771
772 return $result;
773 }
774
775 private function formatLimitReportData( $limitReportData ) {
776 $result = [];
777
778 foreach ( $limitReportData as $name => $value ) {
779 $entry = [];
780 $entry['name'] = $name;
781 if ( !is_array( $value ) ) {
782 $value = [ $value ];
783 }
784 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
785 $entry = array_merge( $entry, $value );
786 $result[] = $entry;
787 }
788
789 return $result;
790 }
791
792 private function setIndexedTagNames( &$array, $mapping ) {
793 foreach ( $mapping as $key => $name ) {
794 if ( isset( $array[$key] ) ) {
795 ApiResult::setIndexedTagName( $array[$key], $name );
796 }
797 }
798 }
799
800 public function getAllowedParams() {
801 return [
802 'title' => null,
803 'text' => [
804 ApiBase::PARAM_TYPE => 'text',
805 ],
806 'revid' => [
807 ApiBase::PARAM_TYPE => 'integer',
808 ],
809 'summary' => null,
810 'page' => null,
811 'pageid' => [
812 ApiBase::PARAM_TYPE => 'integer',
813 ],
814 'redirects' => false,
815 'oldid' => [
816 ApiBase::PARAM_TYPE => 'integer',
817 ],
818 'prop' => [
819 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
820 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
821 'properties|parsewarnings',
822 ApiBase::PARAM_ISMULTI => true,
823 ApiBase::PARAM_TYPE => [
824 'text',
825 'langlinks',
826 'categories',
827 'categorieshtml',
828 'links',
829 'templates',
830 'images',
831 'externallinks',
832 'sections',
833 'revid',
834 'displaytitle',
835 'headhtml',
836 'modules',
837 'jsconfigvars',
838 'encodedjsconfigvars',
839 'indicators',
840 'iwlinks',
841 'wikitext',
842 'properties',
843 'limitreportdata',
844 'limitreporthtml',
845 'parsetree',
846 'parsewarnings',
847 'headitems',
848 ],
849 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
850 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
851 ],
852 ApiBase::PARAM_DEPRECATED_VALUES => [
853 'headitems' => 'apiwarn-deprecation-parse-headitems',
854 ],
855 ],
856 'wrapoutputclass' => 'mw-parser-output',
857 'pst' => false,
858 'onlypst' => false,
859 'effectivelanglinks' => [
860 ApiBase::PARAM_DFLT => false,
861 ApiBase::PARAM_DEPRECATED => true,
862 ],
863 'section' => null,
864 'sectiontitle' => [
865 ApiBase::PARAM_TYPE => 'string',
866 ],
867 'disablepp' => [
868 ApiBase::PARAM_DFLT => false,
869 ApiBase::PARAM_DEPRECATED => true,
870 ],
871 'disablelimitreport' => false,
872 'disableeditsection' => false,
873 'disabletidy' => [
874 ApiBase::PARAM_DFLT => false,
875 ApiBase::PARAM_DEPRECATED => true, // Since 1.32
876 ],
877 'disablestylededuplication' => false,
878 'generatexml' => [
879 ApiBase::PARAM_DFLT => false,
880 ApiBase::PARAM_HELP_MSG => [
881 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
882 ],
883 ApiBase::PARAM_DEPRECATED => true,
884 ],
885 'preview' => false,
886 'sectionpreview' => false,
887 'disabletoc' => false,
888 'useskin' => [
889 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ),
890 ],
891 'contentformat' => [
892 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
893 ],
894 'contentmodel' => [
895 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
896 ]
897 ];
898 }
899
900 protected function getExamplesMessages() {
901 return [
902 'action=parse&page=Project:Sandbox'
903 => 'apihelp-parse-example-page',
904 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
905 => 'apihelp-parse-example-text',
906 'action=parse&text={{PAGENAME}}&title=Test'
907 => 'apihelp-parse-example-texttitle',
908 'action=parse&summary=Some+[[link]]&prop='
909 => 'apihelp-parse-example-summary',
910 ];
911 }
912
913 public function getHelpUrls() {
914 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
915 }
916 }