Merge "Fix sessionfailure i18n message during authentication"
[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 if ( $this->contentIsDeleted ) {
247 $result_array['textdeleted'] = true;
248 }
249 if ( $this->contentIsSuppressed ) {
250 $result_array['textsuppressed'] = true;
251 }
252 $result_array['text'] = $this->pstContent->serialize( $format );
253 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
254 if ( isset( $prop['wikitext'] ) ) {
255 $result_array['wikitext'] = $this->content->serialize( $format );
256 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
257 }
258 if ( !is_null( $params['summary'] ) ||
259 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
260 ) {
261 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
262 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
263 }
264
265 $result->addValue( null, $this->getModuleName(), $result_array );
266
267 return;
268 }
269
270 // Not cached (save or load)
271 if ( $params['pst'] ) {
272 $p_result = $this->pstContent->getParserOutput( $titleObj, $revid, $popts );
273 } else {
274 $p_result = $this->content->getParserOutput( $titleObj, $revid, $popts );
275 }
276 }
277
278 $result_array = [];
279
280 $result_array['title'] = $titleObj->getPrefixedText();
281 $result_array['pageid'] = $pageid ?: $pageObj->getId();
282 if ( $this->contentIsDeleted ) {
283 $result_array['textdeleted'] = true;
284 }
285 if ( $this->contentIsSuppressed ) {
286 $result_array['textsuppressed'] = true;
287 }
288
289 if ( isset( $params['useskin'] ) ) {
290 $factory = MediaWikiServices::getInstance()->getSkinFactory();
291 $skin = $factory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
292 } else {
293 $skin = null;
294 }
295
296 $outputPage = null;
297 if ( $skin || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ) {
298 // Enabling the skin via 'useskin', 'headhtml', or 'categorieshtml'
299 // gets OutputPage and Skin involved, which (among others) applies
300 // these hooks:
301 // - ParserOutputHooks
302 // - Hook: LanguageLinks
303 // - Hook: OutputPageParserOutput
304 // - Hook: OutputPageMakeCategoryLinks
305 $context = new DerivativeContext( $this->getContext() );
306 $context->setTitle( $titleObj );
307 $context->setWikiPage( $pageObj );
308
309 if ( $skin ) {
310 // Use the skin specified by 'useskin'
311 $context->setSkin( $skin );
312 // Context clones the skin, refetch to stay in sync. (T166022)
313 $skin = $context->getSkin();
314 } else {
315 // Make sure the context's skin refers to the context. Without this,
316 // $outputPage->getSkin()->getOutput() !== $outputPage which
317 // confuses some of the output.
318 $context->setSkin( $context->getSkin() );
319 }
320
321 $outputPage = new OutputPage( $context );
322 $outputPage->addParserOutputMetadata( $p_result );
323 $context->setOutput( $outputPage );
324
325 if ( $skin ) {
326 // Based on OutputPage::headElement()
327 $skin->setupSkinUserCss( $outputPage );
328 // Based on OutputPage::output()
329 foreach ( $skin->getDefaultModules() as $group ) {
330 $outputPage->addModules( $group );
331 }
332 }
333 }
334
335 if ( !is_null( $oldid ) ) {
336 $result_array['revid'] = intval( $oldid );
337 }
338
339 if ( $params['redirects'] && !is_null( $redirValues ) ) {
340 $result_array['redirects'] = $redirValues;
341 }
342
343 if ( isset( $prop['text'] ) ) {
344 $result_array['text'] = $p_result->getText( [
345 'allowTOC' => !$params['disabletoc'],
346 'enableSectionEditLinks' => !$params['disableeditsection'],
347 'unwrap' => $params['wrapoutputclass'] === '',
348 ] );
349 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
350 }
351
352 if ( !is_null( $params['summary'] ) ||
353 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
354 ) {
355 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
356 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
357 }
358
359 if ( isset( $prop['langlinks'] ) ) {
360 if ( $skin ) {
361 $langlinks = $outputPage->getLanguageLinks();
362 } else {
363 $langlinks = $p_result->getLanguageLinks();
364 // The deprecated 'effectivelanglinks' option depredates OutputPage
365 // support via 'useskin'. If not already applied, then run just this
366 // one hook of OutputPage::addParserOutputMetadata here.
367 if ( $params['effectivelanglinks'] ) {
368 $linkFlags = [];
369 Hooks::run( 'LanguageLinks', [ $titleObj, &$langlinks, &$linkFlags ] );
370 }
371 }
372
373 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
374 }
375 if ( isset( $prop['categories'] ) ) {
376 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
377 }
378 if ( isset( $prop['categorieshtml'] ) ) {
379 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
380 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
381 }
382 if ( isset( $prop['links'] ) ) {
383 $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
384 }
385 if ( isset( $prop['templates'] ) ) {
386 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
387 }
388 if ( isset( $prop['images'] ) ) {
389 $result_array['images'] = array_keys( $p_result->getImages() );
390 }
391 if ( isset( $prop['externallinks'] ) ) {
392 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
393 }
394 if ( isset( $prop['sections'] ) ) {
395 $result_array['sections'] = $p_result->getSections();
396 }
397 if ( isset( $prop['parsewarnings'] ) ) {
398 $result_array['parsewarnings'] = $p_result->getWarnings();
399 }
400
401 if ( isset( $prop['displaytitle'] ) ) {
402 $result_array['displaytitle'] = $p_result->getDisplayTitle() ?:
403 $titleObj->getPrefixedText();
404 }
405
406 if ( isset( $prop['headitems'] ) ) {
407 if ( $skin ) {
408 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
409 } else {
410 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
411 }
412 }
413
414 if ( isset( $prop['headhtml'] ) ) {
415 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
416 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
417 }
418
419 if ( isset( $prop['modules'] ) ) {
420 if ( $skin ) {
421 $result_array['modules'] = $outputPage->getModules();
422 $result_array['modulescripts'] = $outputPage->getModuleScripts();
423 $result_array['modulestyles'] = $outputPage->getModuleStyles();
424 } else {
425 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
426 $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
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 $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
493 if ( is_callable( [ $dom, 'saveXML' ] ) ) {
494 $xml = $dom->saveXML();
495 } else {
496 $xml = $dom->__toString();
497 }
498 $result_array['parsetree'] = $xml;
499 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
500 }
501
502 $result_mapping = [
503 'redirects' => 'r',
504 'langlinks' => 'll',
505 'categories' => 'cl',
506 'links' => 'pl',
507 'templates' => 'tl',
508 'images' => 'img',
509 'externallinks' => 'el',
510 'iwlinks' => 'iw',
511 'sections' => 's',
512 'headitems' => 'hi',
513 'modules' => 'm',
514 'indicators' => 'ind',
515 'modulescripts' => 'm',
516 'modulestyles' => 'm',
517 'properties' => 'pp',
518 'limitreportdata' => 'lr',
519 'parsewarnings' => 'pw'
520 ];
521 $this->setIndexedTagNames( $result_array, $result_mapping );
522 $result->addValue( null, $this->getModuleName(), $result_array );
523 }
524
525 /**
526 * Constructs a ParserOptions object
527 *
528 * @param WikiPage $pageObj
529 * @param array $params
530 *
531 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
532 */
533 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
534 $popts = $pageObj->makeParserOptions( $this->getContext() );
535 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
536 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
537 $popts->setIsSectionPreview( $params['sectionpreview'] );
538 $popts->setEditSection( !$params['disableeditsection'] );
539 if ( $params['disabletidy'] ) {
540 $popts->setTidy( false );
541 }
542 if ( $params['wrapoutputclass'] !== '' ) {
543 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
544 }
545
546 $reset = null;
547 $suppressCache = false;
548 Hooks::run( 'ApiMakeParserOptions',
549 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
550
551 // Force cache suppression when $popts aren't cacheable.
552 $suppressCache = $suppressCache || !$popts->isSafeToCache();
553
554 return [ $popts, $reset, $suppressCache ];
555 }
556
557 /**
558 * @param WikiPage $page
559 * @param ParserOptions $popts
560 * @param bool $suppressCache
561 * @param int $pageId
562 * @param Revision|null $rev
563 * @param bool $getContent
564 * @return ParserOutput
565 */
566 private function getParsedContent(
567 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
568 ) {
569 $revId = $rev ? $rev->getId() : null;
570 $isDeleted = $rev && $rev->isDeleted( Revision::DELETED_TEXT );
571
572 if ( $getContent || $this->section !== false || $isDeleted ) {
573 if ( $rev ) {
574 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
575 if ( !$this->content ) {
576 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
577 }
578 } else {
579 $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() );
580 if ( !$this->content ) {
581 $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] );
582 }
583 }
584 $this->contentIsDeleted = $isDeleted;
585 $this->contentIsSuppressed = $rev &&
586 $rev->isDeleted( Revision::DELETED_TEXT | Revision::DELETED_RESTRICTED );
587 }
588
589 if ( $this->section !== false ) {
590 $this->content = $this->getSectionContent(
591 $this->content,
592 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
593 );
594 return $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
595 }
596
597 if ( $isDeleted ) {
598 // getParserOutput can't do revdeled revisions
599 $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
600 } else {
601 // getParserOutput will save to Parser cache if able
602 $pout = $page->getParserOutput( $popts, $revId, $suppressCache );
603 }
604 if ( !$pout ) {
605 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
606 }
607
608 return $pout;
609 }
610
611 /**
612 * Extract the requested section from the given Content
613 *
614 * @param Content $content
615 * @param string|Message $what Identifies the content in error messages, e.g. page title.
616 * @return Content
617 */
618 private function getSectionContent( Content $content, $what ) {
619 // Not cached (save or load)
620 $section = $content->getSection( $this->section );
621 if ( $section === false ) {
622 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
623 }
624 if ( $section === null ) {
625 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
626 $section = false;
627 }
628
629 return $section;
630 }
631
632 /**
633 * This mimicks the behavior of EditPage in formatting a summary
634 *
635 * @param Title $title of the page being parsed
636 * @param Array $params the API parameters of the request
637 * @return Content|bool
638 */
639 private function formatSummary( $title, $params ) {
640 global $wgParser;
641 $summary = !is_null( $params['summary'] ) ? $params['summary'] : '';
642 $sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
643
644 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
645 if ( $sectionTitle !== '' ) {
646 $summary = $params['sectiontitle'];
647 }
648 if ( $summary !== '' ) {
649 $summary = wfMessage( 'newsectionsummary' )
650 ->rawParams( $wgParser->stripSectionName( $summary ) )
651 ->inContentLanguage()->text();
652 }
653 }
654 return Linker::formatComment( $summary, $title, $this->section === 'new' );
655 }
656
657 private function formatLangLinks( $links ) {
658 $result = [];
659 foreach ( $links as $link ) {
660 $entry = [];
661 $bits = explode( ':', $link, 2 );
662 $title = Title::newFromText( $link );
663
664 $entry['lang'] = $bits[0];
665 if ( $title ) {
666 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
667 // localised language name in 'uselang' language
668 $entry['langname'] = Language::fetchLanguageName(
669 $title->getInterwiki(),
670 $this->getLanguage()->getCode()
671 );
672
673 // native language name
674 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
675 }
676 ApiResult::setContentValue( $entry, 'title', $bits[1] );
677 $result[] = $entry;
678 }
679
680 return $result;
681 }
682
683 private function formatCategoryLinks( $links ) {
684 $result = [];
685
686 if ( !$links ) {
687 return $result;
688 }
689
690 // Fetch hiddencat property
691 $lb = new LinkBatch;
692 $lb->setArray( [ NS_CATEGORY => $links ] );
693 $db = $this->getDB();
694 $res = $db->select( [ 'page', 'page_props' ],
695 [ 'page_title', 'pp_propname' ],
696 $lb->constructSet( 'page', $db ),
697 __METHOD__,
698 [],
699 [ 'page_props' => [
700 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
701 ] ]
702 );
703 $hiddencats = [];
704 foreach ( $res as $row ) {
705 $hiddencats[$row->page_title] = isset( $row->pp_propname );
706 }
707
708 $linkCache = LinkCache::singleton();
709
710 foreach ( $links as $link => $sortkey ) {
711 $entry = [];
712 $entry['sortkey'] = $sortkey;
713 // array keys will cast numeric category names to ints, so cast back to string
714 ApiResult::setContentValue( $entry, 'category', (string)$link );
715 if ( !isset( $hiddencats[$link] ) ) {
716 $entry['missing'] = true;
717
718 // We already know the link doesn't exist in the database, so
719 // tell LinkCache that before calling $title->isKnown().
720 $title = Title::makeTitle( NS_CATEGORY, $link );
721 $linkCache->addBadLinkObj( $title );
722 if ( $title->isKnown() ) {
723 $entry['known'] = true;
724 }
725 } elseif ( $hiddencats[$link] ) {
726 $entry['hidden'] = true;
727 }
728 $result[] = $entry;
729 }
730
731 return $result;
732 }
733
734 private function formatLinks( $links ) {
735 $result = [];
736 foreach ( $links as $ns => $nslinks ) {
737 foreach ( $nslinks as $title => $id ) {
738 $entry = [];
739 $entry['ns'] = $ns;
740 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
741 $entry['exists'] = $id != 0;
742 $result[] = $entry;
743 }
744 }
745
746 return $result;
747 }
748
749 private function formatIWLinks( $iw ) {
750 $result = [];
751 foreach ( $iw as $prefix => $titles ) {
752 foreach ( array_keys( $titles ) as $title ) {
753 $entry = [];
754 $entry['prefix'] = $prefix;
755
756 $title = Title::newFromText( "{$prefix}:{$title}" );
757 if ( $title ) {
758 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
759 }
760
761 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
762 $result[] = $entry;
763 }
764 }
765
766 return $result;
767 }
768
769 private function formatHeadItems( $headItems ) {
770 $result = [];
771 foreach ( $headItems as $tag => $content ) {
772 $entry = [];
773 $entry['tag'] = $tag;
774 ApiResult::setContentValue( $entry, 'content', $content );
775 $result[] = $entry;
776 }
777
778 return $result;
779 }
780
781 private function formatLimitReportData( $limitReportData ) {
782 $result = [];
783
784 foreach ( $limitReportData as $name => $value ) {
785 $entry = [];
786 $entry['name'] = $name;
787 if ( !is_array( $value ) ) {
788 $value = [ $value ];
789 }
790 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
791 $entry = array_merge( $entry, $value );
792 $result[] = $entry;
793 }
794
795 return $result;
796 }
797
798 private function setIndexedTagNames( &$array, $mapping ) {
799 foreach ( $mapping as $key => $name ) {
800 if ( isset( $array[$key] ) ) {
801 ApiResult::setIndexedTagName( $array[$key], $name );
802 }
803 }
804 }
805
806 public function getAllowedParams() {
807 return [
808 'title' => null,
809 'text' => [
810 ApiBase::PARAM_TYPE => 'text',
811 ],
812 'revid' => [
813 ApiBase::PARAM_TYPE => 'integer',
814 ],
815 'summary' => null,
816 'page' => null,
817 'pageid' => [
818 ApiBase::PARAM_TYPE => 'integer',
819 ],
820 'redirects' => false,
821 'oldid' => [
822 ApiBase::PARAM_TYPE => 'integer',
823 ],
824 'prop' => [
825 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
826 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
827 'properties|parsewarnings',
828 ApiBase::PARAM_ISMULTI => true,
829 ApiBase::PARAM_TYPE => [
830 'text',
831 'langlinks',
832 'categories',
833 'categorieshtml',
834 'links',
835 'templates',
836 'images',
837 'externallinks',
838 'sections',
839 'revid',
840 'displaytitle',
841 'headhtml',
842 'modules',
843 'jsconfigvars',
844 'encodedjsconfigvars',
845 'indicators',
846 'iwlinks',
847 'wikitext',
848 'properties',
849 'limitreportdata',
850 'limitreporthtml',
851 'parsetree',
852 'parsewarnings',
853 'headitems',
854 ],
855 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
856 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
857 ],
858 ApiBase::PARAM_DEPRECATED_VALUES => [
859 'headitems' => 'apiwarn-deprecation-parse-headitems',
860 ],
861 ],
862 'wrapoutputclass' => 'mw-parser-output',
863 'pst' => false,
864 'onlypst' => false,
865 'effectivelanglinks' => [
866 ApiBase::PARAM_DFLT => false,
867 ApiBase::PARAM_DEPRECATED => true,
868 ],
869 'section' => null,
870 'sectiontitle' => [
871 ApiBase::PARAM_TYPE => 'string',
872 ],
873 'disablepp' => [
874 ApiBase::PARAM_DFLT => false,
875 ApiBase::PARAM_DEPRECATED => true,
876 ],
877 'disablelimitreport' => false,
878 'disableeditsection' => false,
879 'disabletidy' => 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 }