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