Merge "Add SPARQL client to core"
[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 $popts->setEditSection( !$params['disableeditsection'] );
540 if ( $params['disabletidy'] ) {
541 $popts->setTidy( false );
542 }
543 if ( $params['wrapoutputclass'] !== '' ) {
544 $popts->setWrapOutputClass( $params['wrapoutputclass'] );
545 }
546
547 $reset = null;
548 $suppressCache = false;
549 Hooks::run( 'ApiMakeParserOptions',
550 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
551
552 // Force cache suppression when $popts aren't cacheable.
553 $suppressCache = $suppressCache || !$popts->isSafeToCache();
554
555 return [ $popts, $reset, $suppressCache ];
556 }
557
558 /**
559 * @param WikiPage $page
560 * @param ParserOptions $popts
561 * @param bool $suppressCache
562 * @param int $pageId
563 * @param Revision|null $rev
564 * @param bool $getContent
565 * @return ParserOutput
566 */
567 private function getParsedContent(
568 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
569 ) {
570 $revId = $rev ? $rev->getId() : null;
571 $isDeleted = $rev && $rev->isDeleted( Revision::DELETED_TEXT );
572
573 if ( $getContent || $this->section !== false || $isDeleted ) {
574 if ( $rev ) {
575 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
576 if ( !$this->content ) {
577 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
578 }
579 } else {
580 $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() );
581 if ( !$this->content ) {
582 $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] );
583 }
584 }
585 $this->contentIsDeleted = $isDeleted;
586 $this->contentIsSuppressed = $rev &&
587 $rev->isDeleted( Revision::DELETED_TEXT | Revision::DELETED_RESTRICTED );
588 }
589
590 if ( $this->section !== false ) {
591 $this->content = $this->getSectionContent(
592 $this->content,
593 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
594 );
595 return $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
596 }
597
598 if ( $isDeleted ) {
599 // getParserOutput can't do revdeled revisions
600 $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
601 } else {
602 // getParserOutput will save to Parser cache if able
603 $pout = $page->getParserOutput( $popts, $revId, $suppressCache );
604 }
605 if ( !$pout ) {
606 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
607 }
608
609 return $pout;
610 }
611
612 /**
613 * Extract the requested section from the given Content
614 *
615 * @param Content $content
616 * @param string|Message $what Identifies the content in error messages, e.g. page title.
617 * @return Content
618 */
619 private function getSectionContent( Content $content, $what ) {
620 // Not cached (save or load)
621 $section = $content->getSection( $this->section );
622 if ( $section === false ) {
623 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
624 }
625 if ( $section === null ) {
626 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
627 $section = false;
628 }
629
630 return $section;
631 }
632
633 /**
634 * This mimicks the behavior of EditPage in formatting a summary
635 *
636 * @param Title $title of the page being parsed
637 * @param Array $params the API parameters of the request
638 * @return Content|bool
639 */
640 private function formatSummary( $title, $params ) {
641 global $wgParser;
642 $summary = !is_null( $params['summary'] ) ? $params['summary'] : '';
643 $sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
644
645 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
646 if ( $sectionTitle !== '' ) {
647 $summary = $params['sectiontitle'];
648 }
649 if ( $summary !== '' ) {
650 $summary = wfMessage( 'newsectionsummary' )
651 ->rawParams( $wgParser->stripSectionName( $summary ) )
652 ->inContentLanguage()->text();
653 }
654 }
655 return Linker::formatComment( $summary, $title, $this->section === 'new' );
656 }
657
658 private function formatLangLinks( $links ) {
659 $result = [];
660 foreach ( $links as $link ) {
661 $entry = [];
662 $bits = explode( ':', $link, 2 );
663 $title = Title::newFromText( $link );
664
665 $entry['lang'] = $bits[0];
666 if ( $title ) {
667 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
668 // localised language name in 'uselang' language
669 $entry['langname'] = Language::fetchLanguageName(
670 $title->getInterwiki(),
671 $this->getLanguage()->getCode()
672 );
673
674 // native language name
675 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
676 }
677 ApiResult::setContentValue( $entry, 'title', $bits[1] );
678 $result[] = $entry;
679 }
680
681 return $result;
682 }
683
684 private function formatCategoryLinks( $links ) {
685 $result = [];
686
687 if ( !$links ) {
688 return $result;
689 }
690
691 // Fetch hiddencat property
692 $lb = new LinkBatch;
693 $lb->setArray( [ NS_CATEGORY => $links ] );
694 $db = $this->getDB();
695 $res = $db->select( [ 'page', 'page_props' ],
696 [ 'page_title', 'pp_propname' ],
697 $lb->constructSet( 'page', $db ),
698 __METHOD__,
699 [],
700 [ 'page_props' => [
701 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
702 ] ]
703 );
704 $hiddencats = [];
705 foreach ( $res as $row ) {
706 $hiddencats[$row->page_title] = isset( $row->pp_propname );
707 }
708
709 $linkCache = LinkCache::singleton();
710
711 foreach ( $links as $link => $sortkey ) {
712 $entry = [];
713 $entry['sortkey'] = $sortkey;
714 // array keys will cast numeric category names to ints, so cast back to string
715 ApiResult::setContentValue( $entry, 'category', (string)$link );
716 if ( !isset( $hiddencats[$link] ) ) {
717 $entry['missing'] = true;
718
719 // We already know the link doesn't exist in the database, so
720 // tell LinkCache that before calling $title->isKnown().
721 $title = Title::makeTitle( NS_CATEGORY, $link );
722 $linkCache->addBadLinkObj( $title );
723 if ( $title->isKnown() ) {
724 $entry['known'] = true;
725 }
726 } elseif ( $hiddencats[$link] ) {
727 $entry['hidden'] = true;
728 }
729 $result[] = $entry;
730 }
731
732 return $result;
733 }
734
735 private function formatLinks( $links ) {
736 $result = [];
737 foreach ( $links as $ns => $nslinks ) {
738 foreach ( $nslinks as $title => $id ) {
739 $entry = [];
740 $entry['ns'] = $ns;
741 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
742 $entry['exists'] = $id != 0;
743 $result[] = $entry;
744 }
745 }
746
747 return $result;
748 }
749
750 private function formatIWLinks( $iw ) {
751 $result = [];
752 foreach ( $iw as $prefix => $titles ) {
753 foreach ( array_keys( $titles ) as $title ) {
754 $entry = [];
755 $entry['prefix'] = $prefix;
756
757 $title = Title::newFromText( "{$prefix}:{$title}" );
758 if ( $title ) {
759 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
760 }
761
762 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
763 $result[] = $entry;
764 }
765 }
766
767 return $result;
768 }
769
770 private function formatHeadItems( $headItems ) {
771 $result = [];
772 foreach ( $headItems as $tag => $content ) {
773 $entry = [];
774 $entry['tag'] = $tag;
775 ApiResult::setContentValue( $entry, 'content', $content );
776 $result[] = $entry;
777 }
778
779 return $result;
780 }
781
782 private function formatLimitReportData( $limitReportData ) {
783 $result = [];
784
785 foreach ( $limitReportData as $name => $value ) {
786 $entry = [];
787 $entry['name'] = $name;
788 if ( !is_array( $value ) ) {
789 $value = [ $value ];
790 }
791 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
792 $entry = array_merge( $entry, $value );
793 $result[] = $entry;
794 }
795
796 return $result;
797 }
798
799 private function setIndexedTagNames( &$array, $mapping ) {
800 foreach ( $mapping as $key => $name ) {
801 if ( isset( $array[$key] ) ) {
802 ApiResult::setIndexedTagName( $array[$key], $name );
803 }
804 }
805 }
806
807 public function getAllowedParams() {
808 return [
809 'title' => null,
810 'text' => [
811 ApiBase::PARAM_TYPE => 'text',
812 ],
813 'revid' => [
814 ApiBase::PARAM_TYPE => 'integer',
815 ],
816 'summary' => null,
817 'page' => null,
818 'pageid' => [
819 ApiBase::PARAM_TYPE => 'integer',
820 ],
821 'redirects' => false,
822 'oldid' => [
823 ApiBase::PARAM_TYPE => 'integer',
824 ],
825 'prop' => [
826 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
827 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
828 'properties|parsewarnings',
829 ApiBase::PARAM_ISMULTI => true,
830 ApiBase::PARAM_TYPE => [
831 'text',
832 'langlinks',
833 'categories',
834 'categorieshtml',
835 'links',
836 'templates',
837 'images',
838 'externallinks',
839 'sections',
840 'revid',
841 'displaytitle',
842 'headhtml',
843 'modules',
844 'jsconfigvars',
845 'encodedjsconfigvars',
846 'indicators',
847 'iwlinks',
848 'wikitext',
849 'properties',
850 'limitreportdata',
851 'limitreporthtml',
852 'parsetree',
853 'parsewarnings',
854 'headitems',
855 ],
856 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
857 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
858 ],
859 ApiBase::PARAM_DEPRECATED_VALUES => [
860 'headitems' => 'apiwarn-deprecation-parse-headitems',
861 ],
862 ],
863 'wrapoutputclass' => 'mw-parser-output',
864 'pst' => false,
865 'onlypst' => false,
866 'effectivelanglinks' => [
867 ApiBase::PARAM_DFLT => false,
868 ApiBase::PARAM_DEPRECATED => true,
869 ],
870 'section' => null,
871 'sectiontitle' => [
872 ApiBase::PARAM_TYPE => 'string',
873 ],
874 'disablepp' => [
875 ApiBase::PARAM_DFLT => false,
876 ApiBase::PARAM_DEPRECATED => true,
877 ],
878 'disablelimitreport' => false,
879 'disableeditsection' => false,
880 'disabletidy' => false,
881 'disablestylededuplication' => 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 }