Merge "Revert "Log the reason why revision->getContent() returns null""
[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 'deduplicateStyles' => !$params['disablestylededuplication'],
349 ] );
350 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
351 }
352
353 if ( !is_null( $params['summary'] ) ||
354 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
355 ) {
356 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
357 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
358 }
359
360 if ( isset( $prop['langlinks'] ) ) {
361 if ( $skin ) {
362 $langlinks = $outputPage->getLanguageLinks();
363 } else {
364 $langlinks = $p_result->getLanguageLinks();
365 // The deprecated 'effectivelanglinks' option depredates OutputPage
366 // support via 'useskin'. If not already applied, then run just this
367 // one hook of OutputPage::addParserOutputMetadata here.
368 if ( $params['effectivelanglinks'] ) {
369 $linkFlags = [];
370 Hooks::run( 'LanguageLinks', [ $titleObj, &$langlinks, &$linkFlags ] );
371 }
372 }
373
374 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
375 }
376 if ( isset( $prop['categories'] ) ) {
377 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
378 }
379 if ( isset( $prop['categorieshtml'] ) ) {
380 $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
381 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
382 }
383 if ( isset( $prop['links'] ) ) {
384 $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
385 }
386 if ( isset( $prop['templates'] ) ) {
387 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
388 }
389 if ( isset( $prop['images'] ) ) {
390 $result_array['images'] = array_keys( $p_result->getImages() );
391 }
392 if ( isset( $prop['externallinks'] ) ) {
393 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
394 }
395 if ( isset( $prop['sections'] ) ) {
396 $result_array['sections'] = $p_result->getSections();
397 }
398 if ( isset( $prop['parsewarnings'] ) ) {
399 $result_array['parsewarnings'] = $p_result->getWarnings();
400 }
401
402 if ( isset( $prop['displaytitle'] ) ) {
403 $result_array['displaytitle'] = $p_result->getDisplayTitle() ?:
404 $titleObj->getPrefixedText();
405 }
406
407 if ( isset( $prop['headitems'] ) ) {
408 if ( $skin ) {
409 $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
410 } else {
411 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
412 }
413 }
414
415 if ( isset( $prop['headhtml'] ) ) {
416 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
417 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
418 }
419
420 if ( isset( $prop['modules'] ) ) {
421 if ( $skin ) {
422 $result_array['modules'] = $outputPage->getModules();
423 $result_array['modulescripts'] = $outputPage->getModuleScripts();
424 $result_array['modulestyles'] = $outputPage->getModuleStyles();
425 } else {
426 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
427 $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
428 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
429 }
430 }
431
432 if ( isset( $prop['jsconfigvars'] ) ) {
433 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
434 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
435 }
436
437 if ( isset( $prop['encodedjsconfigvars'] ) ) {
438 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
439 $result_array['encodedjsconfigvars'] = FormatJson::encode(
440 $jsconfigvars,
441 false,
442 FormatJson::ALL_OK
443 );
444 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
445 }
446
447 if ( isset( $prop['modules'] ) &&
448 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
449 $this->addWarning( 'apiwarn-moduleswithoutvars' );
450 }
451
452 if ( isset( $prop['indicators'] ) ) {
453 if ( $skin ) {
454 $result_array['indicators'] = (array)$outputPage->getIndicators();
455 } else {
456 $result_array['indicators'] = (array)$p_result->getIndicators();
457 }
458 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
459 }
460
461 if ( isset( $prop['iwlinks'] ) ) {
462 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
463 }
464
465 if ( isset( $prop['wikitext'] ) ) {
466 $result_array['wikitext'] = $this->content->serialize( $format );
467 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
468 if ( !is_null( $this->pstContent ) ) {
469 $result_array['psttext'] = $this->pstContent->serialize( $format );
470 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
471 }
472 }
473 if ( isset( $prop['properties'] ) ) {
474 $result_array['properties'] = (array)$p_result->getProperties();
475 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
476 }
477
478 if ( isset( $prop['limitreportdata'] ) ) {
479 $result_array['limitreportdata'] =
480 $this->formatLimitReportData( $p_result->getLimitReportData() );
481 }
482 if ( isset( $prop['limitreporthtml'] ) ) {
483 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
484 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
485 }
486
487 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
488 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
489 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
490 }
491
492 $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
493 $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
494 if ( is_callable( [ $dom, 'saveXML' ] ) ) {
495 $xml = $dom->saveXML();
496 } else {
497 $xml = $dom->__toString();
498 }
499 $result_array['parsetree'] = $xml;
500 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
501 }
502
503 $result_mapping = [
504 'redirects' => 'r',
505 'langlinks' => 'll',
506 'categories' => 'cl',
507 'links' => 'pl',
508 'templates' => 'tl',
509 'images' => 'img',
510 'externallinks' => 'el',
511 'iwlinks' => 'iw',
512 'sections' => 's',
513 'headitems' => 'hi',
514 'modules' => 'm',
515 'indicators' => 'ind',
516 'modulescripts' => 'm',
517 'modulestyles' => 'm',
518 'properties' => 'pp',
519 'limitreportdata' => 'lr',
520 'parsewarnings' => 'pw'
521 ];
522 $this->setIndexedTagNames( $result_array, $result_mapping );
523 $result->addValue( null, $this->getModuleName(), $result_array );
524 }
525
526 /**
527 * Constructs a ParserOptions object
528 *
529 * @param WikiPage $pageObj
530 * @param array $params
531 *
532 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
533 */
534 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
535 $popts = $pageObj->makeParserOptions( $this->getContext() );
536 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
537 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
538 $popts->setIsSectionPreview( $params['sectionpreview'] );
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 'disablestylededuplication' => false,
881 'generatexml' => [
882 ApiBase::PARAM_DFLT => false,
883 ApiBase::PARAM_HELP_MSG => [
884 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
885 ],
886 ApiBase::PARAM_DEPRECATED => true,
887 ],
888 'preview' => false,
889 'sectionpreview' => false,
890 'disabletoc' => false,
891 'useskin' => [
892 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ),
893 ],
894 'contentformat' => [
895 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
896 ],
897 'contentmodel' => [
898 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
899 ]
900 ];
901 }
902
903 protected function getExamplesMessages() {
904 return [
905 'action=parse&page=Project:Sandbox'
906 => 'apihelp-parse-example-page',
907 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
908 => 'apihelp-parse-example-text',
909 'action=parse&text={{PAGENAME}}&title=Test'
910 => 'apihelp-parse-example-texttitle',
911 'action=parse&summary=Some+[[link]]&prop='
912 => 'apihelp-parse-example-summary',
913 ];
914 }
915
916 public function getHelpUrls() {
917 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
918 }
919 }